Excerpt taken from Win32 API Programming with Visual Basic by Steven Roman, published by O'Reilly and Associates. Due out late summer 1999.

Copyright © 1999 by The Roman Press, Inc. All Rights Reserved. You may view and print this document for your own personal use only. No portion of this document may be sold or incorporated into any other document for any reason.

Is This Application Already Running?

One of the most often asked questions by programmers is "What is the best way to tell whether or not a given application is running?" I can think of several methods for determining whether a particular application is currently running, but I would not be surprised to learn that there are many more. Unfortunately, only one of these methods works for applications that are not created by the programmer in VB.


Using FindWindow
The first method is the simplest, but works only for applications that we create and only if the application has a uniquely identifiable top level window caption that does not change. In this case, we can use the FindWindow function to see if a window with that caption exists. There is, however, a subtlety involved here.


To illustrate, here is some code from the Load event of the Clipboard Viewer application that we will write later in the book:


' Check for running viewer and switch if it exists

hnd = FindWindow("ThunderRT6FormDC", "rpiClipViewer")

If hnd <> 0 And hnd <> Me.hwnd Then

SetForegroundWindow hnd

End

End If

The problem here is that as soon as this program is run, a form with caption rpiClipViewer will be created, so FindWindow will always report that such a form (window) exists. However, this is easily overcome with a bit of prestidigitation. In particular, we change the design time caption for the main form to, say, rpiClipView. Then, in the Activate event for the main form, we change it to the final value


Private Sub Form_Activate()

Me.Caption = "rpiClipViewer"

End Sub

Now, during the Load event for the form, the caption will be rpiClipView and thus will not trigger a positive response from FindWindow. Indeed, FindWindow will only report that such a window exists if there is another running instance of the application, which is precisely what we want!


The SetForegroundWindow Problem
Under Windows 95 and Windows NT 4, the SetForegroundWindow function:


Declare Function SetForegroundWindow Lib "user32" (ByVal hwnd As Long) As Long

will bring the application that owns the window with handle hwnd to the foreground. However, Microsoft has thrown us a curve in Windows 2000 and Windows 98. Here is what the documentation states:


Windows NT 5.0 and later: An application cannot force a window to the foreground while the user is working with another window. Instead, SetForegroundWindow will activate the window (see SetActiveWindow) and call the FlashWindowEx function to notify the user.

Unfortunately, Microsoft has decided to take one more measure of control out of our hands by not allowing us to change which application is in the foreground. (To be sure, abuse of this capability leads to obnoxious behavior, by I was not planning on abusing it!)


Fortunately, SetForegroundWindow does work if called from within an application, that is, it will force its own application to the foreground. This is just enough rope to let us hang ouselves, so-to-speak.


The rpiAccessProcess DLL that we will discuss in the chapter on DLL injection and foreign process access exports a function called rpiSetForegroundWindow. The VB declaration of this function is just like that of Win32's SetForegroundWindow:


Declare Function rpiSetForegroundWindow Lib "rpiAccessProcess.dll" ( _

ByVal hwnd As Long) As Long

The function is designed to work just like SetForegroundWindow works under Windows 95 and Windows NT 4, even under Windows 98 and Windows 2000. It does so by injecting the rpiAccessProcess DLL into the foreign process space so that the SetForegroundWindow function can be run from that process, thus bringing it to the foreground. We will discuss how this is done in the chapter on DLL injection. In any case, you should be able to use this function whenever you need SetForegroundWindow under Windows 98/2000.


Using a Usage Count
Conceptually, the simplest approach to this problem is just to have our VB application maintain a small text file, placed in some fixed directory, such as the Windows directory, that contains a single number that acts as a usage count for the application. The application can, in its main Load event, check the usage count by simply opening the file in the standard way.


If the count is 1, the application terminates abruptly, without firing its Unload event. This can be done by using the much maligned End statement. If the count is 0, the application sets the usage count to 1 and executes normally. Then, in its Unload event, the application sets the usage count to 0. In this way, one and only one instance of the application is allowed to run normally, and it is the only instance that alters the usage count.


Of course, this approach can be made more elegant by using a memory mapped file, but this brings with it considerable additional baggage in the form of extra code.


Here is some pseudocode for the Load and Unload events of the main form:


Private Sub Form_Load()

Dim lUsageCount As Long

' Get the current usage count from the memory-mapped file

lUsageCount = GetUsageCount

If lUsageCount > 0 Then

MsgBox "Application is already running"

End

Else

' Set the usage count to 1

SetUsageCount 1

End If

End Sub

Private Sub Form_Unload()

SetUsageCount 0

End Sub

We will leave the implementation of this approach to the reader and turn to a somewhat simpler implementation along these same lines.


The rpiUsageCount DLL
As we will see when we discuss the rpiAccessProcess DLL for use in allocating foreign memory, an executable file (DLL or EXE) can contain shared memory. This memory is shared by every instance of the executable. Thus, if we place a shared variable in a DLL, every process that uses this DLL will have access to this variable.


To be absolutely clear, a shared variable is not the same as a global variable. Global variables are accessible to the entire DLL, but each process that loads the DLL gets a separate copy of each global variable. Thus, global variables are accessible within a single process. Shared variables are accessible throughout the system.


Now, while VB does not allow us to create shared memory in a VB executable, it is very easy to do in a DLL written in VC++.


On the accompanying CD, you will find a DLL called rpiUsageCount.DLL. Here is the entire VC++ source code:


// rpiUsageCount.cpp

#include <windows.h>

// Set up shared data section in DLL

// MUST INITIALIZE ALL SHARED VARIABLES

#pragma data_seg("Shared")

long giUsageCount = 0;

#pragma data_seg()

// Tell linker to make this section shared and read-write

#pragma comment(linker, "/section:Shared,rws")

////////////////////////////////////////////////////////////

// Prototypes of exported functions

////////////////////////////////////////////////////////////

long WINAPI rpiIncrementUsageCount();

long WINAPI rpiDecrementUsageCount();

long WINAPI rpiGetUsageCount();

long WINAPI rpiSetUsageCount(long lNewCount);

////////////////////////////////////////////////////////////

// DllMain

////////////////////////////////////////////////////////////

HANDLE hDLLInst = NULL;

BOOL WINAPI DllMain (HANDLE hInst, ULONG ul_reason_for_call, LPVOID lpReserved)

{

// Keep the instance handle for later use

hDLLInst = hInst;

switch(ul_reason_for_call)

{

case DLL_PROCESS_ATTACH:

// Initialization here

break;

case DLL_PROCESS_DETACH:

// Clean-up here

break;

}

return TRUE;

}

////////////////////////////////////////////////////////////

// Functions for export

////////////////////////////////////////////////////////////

long WINAPI rpiIncrementUsageCount()

{

return InterlockedIncrement(&giUsageCount);

}

long WINAPI rpiDecrementUsageCount()

{

return InterlockedDecrement(&giUsageCount);

}

long WINAPI rpiGetUsageCount()

{

return giUsageCount;

}

long WINAPI rpiSetUsageCount(long lNewCount)

{

giUsageCount = lNewCount;

return giUsageCount;

}

This DLL has a single shared long variable called lUsageCount. The DLL exports four functions for use with this variable. (This is more than is needed but I got carried away.)


rpiIncrementUsageCount

rpiDecrementUsageCount

rpiGetUsageCount

rpiSetUsageCount

Here are the VB declarations:


Declare Function rpiIncrementUsageCount Lib "rpiUsageCount.dll" () As Long

Declare Function rpiDecrementUsageCount Lib "rpiUsageCount.dll" () As Long

Declare Function rpiGetUsageCount Lib "rpiUsageCount.dll" () As Long

Declare Function rpiSetUsageCount Lib "rpiUsageCount.dll" () As Long

To use this DLL, we just add the following code to the Load and Unload events of the main VB form:


Private Sub Form_Load()

Dim lUsageCount As Long

' Get the current usage count

lUsageCount = rpiGetUsageCount

If lUsageCount > 0 Then

MsgBox "Application is already running"

End

Else

rpiSetUsageCount 1

End If

End Sub

Private Sub Form_Unload()

rpiSetUsageCount 0

End Sub

The downside of using this DLL is that it uses 49,152 bytes of memory. Also, it does not automatically switch to an already running instance of the application. For this, we still need to use FindWindow to get a window handle to use with SetForegroundWindow (or rpiSetForegroundWindow).


Walking the Process List
Our final approach to checking for a running application is the most obvious one that should always work (although for some reason I get a funny feeling saying "always"). Namely, we walk through the list of all current processes to check every EXE file name (and perhaps even complete path). Unfortunately, as we have seen, this requires different code under Windows NT and Windows 95/98. Nevertheless, it is important, so here is a utility that will do the job.


The Windows NT version is GetWinNTProcessID. We feed this function either an EXE file name or a fully qualified EXE name (path and file name). The function walks the process list and tries to do a case-insensitive match of the name. It returns the process ID of the last match and a count of the total number of matches. If the return value is 0, then this application is not running! Here is the code (both versions), including the necessary declarations.


Option Explicit

' *************************

' NOTE: Windows NT 4.0 only

' *************************

Public Const MAX_PATH = 260

Public Declare Function EnumProcesses Lib "PSAPI.DLL" ( _

idProcess As Long, _

ByVal cBytes As Long, _

cbNeeded As Long _

) As Long

Public Declare Function EnumProcessModules Lib "PSAPI.DLL" ( _

ByVal hProcess As Long, _

hModule As Long, _

ByVal cb As Long, _

cbNeeded As Long _

) As Long

Public Declare Function GetModuleBaseName Lib "PSAPI.DLL" Alias "GetModuleBaseNameA" ( _

ByVal hProcess As Long, _

ByVal hModule As Long, _

ByVal lpBaseName As String, _

ByVal nSize As Long _

) As Long

Public Declare Function GetModuleFileNameEx Lib "PSAPI.DLL" Alias "GetModuleFileNameExA" ( _

ByVal hProcess As Long, _

ByVal hModule As Long, _

ByVal lpFilename As String, _

ByVal nSize As Long _

) As Long

Public Const STANDARD_RIGHTS_REQUIRED = &HF0000

Public Const SYNCHRONIZE = &H100000

Public Const PROCESS_VM_READ = &H10

Public Const PROCESS_QUERY_INFORMATION = &H400

Public Const PROCESS_ALL_ACCESS = STANDARD_RIGHTS_REQUIRED Or SYNCHRONIZE Or &HFFF

Declare Function OpenProcess Lib "kernel32" ( _

ByVal dwDesiredAccess As Long, _

ByVal bInheritHandle As Long, _

ByVal dwProcessId As Long _

) As Long

Declare Function CloseHandle Lib "kernel32" (ByVal hObject As Long) As Long

' -------------------

Public Function GetWinNTProcessID(sFQEXEName As String, sEXEName As String, ByRef cMatches As Long) As Long

' Gets the process ID from the EXE name or fully qualified (path/name) EXE name

' If sFQName <> "" then uses this to get matches

' If sName <> "" uses just the name to get matches

' Returns 0 if no such process, else the process ID of the last match

' Returns count of matches in OUT parameter cMatches

' Returns FQName if that is empty

' Returns -1 if both sFQName and sName are empty

' Returns -2 if error getting process list

Dim i As Integer, j As Integer, l As Long

Dim cbNeeded As Long

Dim hEXE As Long

Dim hProcess As Long

Dim lret As Long

Dim cProcesses As Long

Dim lProcessIDs() As Long

Dim sEXENames() As String

Dim sFQEXENames() As String

' ----------------------------------

' First get the array of process IDs

' ----------------------------------

' Initial guess

cProcesses = 25

Do

' Size array

ReDim lProcessIDs(1 To cProcesses)

' Enumerate

lret = EnumProcesses(lProcessIDs(1), cProcesses * 4, cbNeeded)

If lret = 0 Then

GetWinNTProcessID = -2

Exit Function

End If

' Compare needed bytes with array size in bytes.

' If less, then we got them all.

If cbNeeded < cProcesses * 4 Then

Exit Do

Else

cProcesses = cProcesses * 2

End If

Loop

cProcesses = cbNeeded / 4

ReDim Preserve lProcessIDs(1 To cProcesses)

ReDim sEXENames(1 To cProcesses)

ReDim sFQEXENames(1 To cProcesses)

' -------------

' Get EXE names

' -------------

For i = 1 To cProcesses

' Use OpenProcess to get a handle to each process

hProcess = OpenProcess(PROCESS_QUERY_INFORMATION Or PROCESS_VM_READ, 0&, lProcessIDs(i))

' Watch out for special processes

Select Case lProcessIDs(i)

Case 0 ' System Idle Process

sEXENames(i) = "Idle Process"

sFQEXENames(i) = "Idle Process"

Case 2

sEXENames(i) = "System"

sFQEXENames(i) = "System"

Case 28

sEXENames(i) = "csrss.exe"

sFQEXENames(i) = "csrss.exe"

End Select

' If error skip this process

If hProcess = 0 Then

GoTo hpContinue

End If

' Now get the handle of the first module

' in this process, since first module is EXE

hEXE = 0

lret = EnumProcessModules(hProcess, hEXE, 4&, cbNeeded)

If hEXE = 0 Then GoTo hpContinue

' Get the name of the module

sEXENames(i) = String$(MAX_PATH, 0)

lret = GetModuleBaseName(hProcess, hEXE, sEXENames(i), Len(sEXENames(i)))

sEXENames(i) = Trim0(sEXENames(i))

' Get full path name

sFQEXENames(i) = String$(MAX_PATH, 0)

lret = GetModuleFileNameEx(hProcess, hEXE, sFQEXENames(i), Len(sFQEXENames(i)))

sFQEXENames(i) = Trim0(sFQEXENames(i))

hpContinue:

' Close handle

lret = CloseHandle(hProcess)

Next

' ----------------

' Check for match

' ----------------

cMatches = 0

If sFQEXEName <> "" Then

For i = 1 To cProcesses

If LCase$(sFQEXENames(i)) = LCase$(sFQEXEName) Then

cMatches = cMatches + 1

GetWinNTProcessID = lProcessIDs(i)

End If

Next

ElseIf sEXEName <> "" Then

For i = 1 To cProcesses

If LCase$(sEXENames(i)) = LCase$(sEXEName) Then

cMatches = cMatches + 1

GetWinNTProcessID = lProcessIDs(i)

sFQEXEName = sFQEXENames(i)

End If

Next

Else

GetWinNTProcessID = -1

End If

End Function

The Windows 95/98 version uses Toolhelp. The corresponding function (and required declarations) are shown below.


Option Explicit

' ************************

' NOTE: Windows 95/98 only

' ************************

Public Const MAX_MODULE_NAME32 = 255

Public Const MAX_PATH = 260

Public Const TH32CS_SNAPHEAPLIST = &H1

Public Const TH32CS_SNAPPROCESS = &H2

Public Const TH32CS_SNAPTHREAD = &H4

Public Const TH32CS_SNAPMODULE = &H8

Public Const TH32CS_SNAPALL = (TH32CS_SNAPHEAPLIST Or TH32CS_SNAPPROCESS Or TH32CS_SNAPTHREAD Or TH32CS_SNAPMODULE)

Public Const TH32CS_INHERIT = &H80000000

''HANDLE WINAPI CreateToolhelp32Snapshot( DWORD dwFlags,

'' DWORD th32ProcessID );

Public Declare Function CreateToolhelp32Snapshot Lib "kernel32" ( _

ByVal dwFlags As Long, _

ByVal th32ProcessID As Long _

) As Long

Public Declare Function Process32First Lib "kernel32" ( _

ByVal hSnapShot As Long, _

lppe As PROCESSENTRY32 _

) As Long

Public Declare Function Process32Next Lib "kernel32" ( _

ByVal hSnapShot As Long, _

lppe As PROCESSENTRY32 _

) As Long

Public Type PROCESSENTRY32

dwSize As Long

cntUsage As Long

th32ProcessID As Long ' process ID

th32DefaultHeapID As Long

th32ModuleID As Long ' only for Toolhelp functions

cntThreads As Long ' number of threads

th32ParentProcessID As Long ' process ID of parent

pcPriClassBase As Long

dwFlags As Long

szExeFile As String * MAX_PATH ' path/file of EXE file

End Type

Declare Function CloseHandle Lib "kernel32" (ByVal hObject As Long) As Long

' --------------------------

Function GetWin95ProcessID(sFQName As String, sName As String, ByRef cMatches As Long) As Long

' *************************

' NOTE: Windows 95/98 only

' *************************

' Gets the process ID

' If sFQName <> "" then uses this to get matches

' If sName <> "" uses just the name to get matches

' Returns 0 if no such process, else the process ID of the last match

' Returns count of matches in OUT parameter cMatches

' Returns FQName if that is empty

' Returns -1 if could not get snapshot

Dim i As Integer, c As Currency

Dim hSnapShot As Long

Dim lret As Long ' for generic return values

Dim cProcesses As Long

Dim cProcessIDs() As Currency

Dim sEXENames() As String

Dim sFQEXENames() As String

Dim procEntry As PROCESSENTRY32

procEntry.dwSize = LenB(procEntry)

' Scan all the processes.

hSnapShot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0&)

If hSnapShot = -1 Then

GetProcessID = -1

Exit Function

End If

' Initialize

ReDim sFQEXENames(1 To 25)

ReDim sEXENames(1 To 25)

ReDim cProcessIDs(1 To 25)

cProcesses = 0

' Do first process

lret = Process32First(hSnapShot, procEntry)

If lret > 0 Then

cProcesses = cProcesses + 1

sFQEXENames(cProcesses) = Trim0(procEntry.szExeFile)

sEXENames(cProcesses) = GetFileName(sFQEXENames(cProcesses))

If procEntry.th32ProcessID < 0 Then

c = CCur(procEntry.th32ProcessID) + 2 ^ 32

Else

c = CCur(procEntry.th32ProcessID)

End If

cProcessIDs(cProcesses) = c

End If

' Do rest

Do

lret = Process32Next(hSnapShot, procEntry)

If lret = 0 Then Exit Do

cProcesses = cProcesses + 1

If UBound(sFQEXENames) < cProcesses Then

ReDim Preserve sFQEXENames(1 To cProcesses + 10)

ReDim Preserve sEXENames(1 To cProcesses + 10)

ReDim Preserve cProcessIDs(1 To cProcesses + 10)

End If

sFQEXENames(cProcesses) = Trim0(procEntry.szExeFile)

sEXENames(cProcesses) = GetFileName(sFQEXENames(cProcesses))

If procEntry.th32ProcessID < 0 Then

c = CCur(procEntry.th32ProcessID) + 2 ^ 32

Else

c = CCur(procEntry.th32ProcessID)

End If

cProcessIDs(cProcesses) = c

Loop

CloseHandle hSnapShot

' ----------

' Find Match

' ----------

cMatches = 0

If sFQName <> "" Then

For i = 1 To cProcesses

If LCase$(sFQEXENames(i)) = LCase$(sFQName) Then

cMatches = cMatches + 1

GetProcessID = lProcessIDs(i)

End If

Next

ElseIf sName <> "" Then

For i = 1 To cProcesses

If LCase$(sEXENames(i)) = LCase$(sName) Then

cMatches = cMatches + 1

GetProcessID = lProcessIDs(i)

sFQName = sFQEXENames(i)

End If

Next

Else

GetProcessID = -1

End If

End Function