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.
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