As we have seen, a process is a running instance of an application, together with a set of resources that are allocated to the running application. These resources include:
A virtual address space.
System resources, such as bitmaps, files, memory allocations, and so on.
Process modules, that is, executables that are mapped (loaded) into the process's address space. This includes such things as DLLs, DRVs, and OCXs, as well as the main EXE of the process, which is, unfortunately, sometimes referred to as the module. We will elaborate on the issue of modules later in this chapter. However, it is important to understand that the actual module data (and code) either resides on disk or is loaded into physical memory (RAM). However, the term loaded has a different meaning with respect to a process's virtual address space. The term mapped is more appropriate because this mapping is simply an assignment of virtual addresses to physical addresses. Once a module is loaded into physical memory, those physical addresses can be mapped into multiple virtual address spaces, perhaps using different virtual addresses in each process. The mapping does not necessarily require that any actual data (or code) be physically moved (although it might, as we will see).
A unique identification number, called a process ID.
One or more threads of execution.
A thread is an object within a process that is allocated processor time by the operating system. Every process must have at least one thread. More specifically, a thread includes:
Page 161
The current contents of the CPU registers
Two stacks one for the thread to use when running in kernel mode and one for the thread to use when running in user mode
A private storage area for use by subsystems, runtime libraries, and DLLs
A unique identifier called a thread ID
The register contents, stack contents, and storage area contents are referred to as the thread's context.
As mentioned, the purpose of threads is to allow a process to maintain more than one line of execution, that is, to do more than one thing at a time. In a multiprocessor environment (a computer with more than one CPU), Windows NT (but not Windows 9x) can assign different threads to different processors at different times, providing true multiprocessing. In a single-processor environment, the CPU must provide time slices to each thread that is currently running on the system.
We will devote this chapter to a discussion of processes, leaving threads for the next chapter.
Process Handles and IDs
The distinction between process IDs and handles can be gleaned by looking at the CreateProcess API function. One possible VB declaration of this rather complex function is:
Declare Function CreateProcess Lib "kernel32" Alias "CreateProcessA" ( _ ByVal lpApplicationName As String, _ ByVal lpCommandLine As String, _ lpProcessAttributes As SECURITY_ATTRIBUTES, _ lpThreadAttributes As SECURITY_ATTRIBUTES, _ ByVal bInheritHandles As Long, _ ByVal dwCreationFlags As Long, _ lpEnvironment As Any, _ ByVal lpCurrentDirectory As String, _ lpStartupInfo As STARTUPINFO, _ lpProcessInformation As PROCESS_INFORMATION _ ) As Long
Now, the last parameter is a pointer to a PROCESS_INFORMATION structure:
where, to quote the documentation, the members are as follows.
Page 162
hProcess Returns a handle to the newly created process. The handle is used to specify the process in all functions that perform operations on the process object.
hThread Returns a handle to the primary thread of the newly created process. The handle is used to specify the thread in all functions that perform operations on the thread object.
dwProcessId Returns a global process identifier that can be used to identify a process. The value is valid from the time the process is created until the time the process is terminated.
dwThreadId Returns a global thread identifier that can be used to identify a thread. The value is valid from the time the thread is created until the time the thread is terminated.
Thus, the main differences between a process handle and a process ID (or thread handle and thread ID) are:
A process handle is process-specific, whereas a process ID is valid system-wide. Thus, a process handle, whether it be for the current process or a foreign process, is valid only in the process in which it was created.
Each process has one and only one process ID, but a process may have several process handles, even from within a single process.
Some API functions require the process ID; others require a process handle.
We should emphasize that although a process handle is process-specific, one process can get a handle to another process. More specifically, if Process A has a handle to Process B, then that handle identifies Process B, but it is valid only in Process A. The handle can be used in Process A to call some API functions that relate to Process B. (Don't get too excited about this Process B's memory is still inaccessible to Process A.)
Module Handles
Every module (DLL, OCX, DRV, etc.) that is loaded into a process space has a module handle, also called an instance handle. (In 16-bit Windows, these were different objects: in 32-bit Windows, they are the same.)
An executable's module handle is simply the starting or base address of the executable in the process's address space. This makes it rather clear that such a handle has meaning only within the process containing the module.
Page 163
The default base address of an EXE created using Visual C++ is &H400000. Note, however, that the VC++ programmer can change this default address, and there are important reasons to do so in order to avoid conflict with other modules. (As we will see, relocation of a module due to a base address conflict is costly in terms of both time and physical memory.) In addition, modules created by other programming environments may have different default base addresses.
Module handles are often used for calling API functions that allocate resources to the process. For instance, the LoadBitmap function loads a bitmap resource from the process's EXE file. Its declaration is:
HBITMAP LoadBitmap(
HINSTANCE hInstance,
// handle to application instance
LPCTSTR lpBitmapName
// address of bitmap resource name
);
The first parameter is the process's instance handle.
Unfortunately, the module handle for a process's EXE is often confusingly referred to as the module handle for the process. Also, the name of the EXE is sometimes referred to as the process's module name.
Identifying a Process
The following four items occur frequently in process-related API programming:
process ID
process handle
fully qualified filename of EXE
module handle of process EXE
The question is: ''Given one of these items, can we get the others?" Figure 11-1 illustrates the rather complex answer to this question. The arrows indicate the possibilities.
A: Getting a Process Handle from the Process ID
It is relatively easy to get a process handle from the process ID, but doing the reverse does not seem possible (at least in any direct way).
We can get a handle to any process from its process ID using the OpenProcess function:
HANDLE OpenProcess(
DWORD dwDesireAccess,
// access flag
BOOL bInheritHandle,
// handle inheritance flag
DWORD dwProcessId
// process identifier
);
Page 164
Figure 11-1. IDs, handles, and so on
The dwDesiredAccess parameter refers to security rights and can be set to various values, such as:
PROCESS_ALL_ACCESS Equivalent to specifying all access flags
PROCESS_DUP_HANDLE Can use the process handle as either the source or target process in the DuplicateHandle function (discussed later) to duplicate a handle
PROCESS_QUERY_INFORMATION Can use the process handle to read information from the process object
PROCESS_VM_OPERATION Can use the process handle to modify the virtual memory of the process
Page 165
PROCESS_TERMINATE Can use the process handle in the TerminateProcess function to terminate the process
PROCESS_VM_READ Can use the process handle in the ReadProcessMemory function to read from the virtual memory of the process
PROCESS_VM_WRITE Can use the process handle in the WriteProcessMemory function to write to the virtual memory of the process
SYNCHRONIZE (Windows NT) Can use the process handle in any of the wait functions (such as WaitForSingleObject, discussed in Chapter 12, Threads) to wait for the process to terminate.
The bInheritHandle parameter is set to True to allow child processes to inherit the current handle when the current process creates a child process. (The handle value may change, but the point is that the child gets a handle to its parent. We won't be concerned with process inheritance.)
The dwProcessID parameter should be set to the process ID of the process whose handle we desire. The OpenProcess function returns a handle to the process.
The VB declaration of OpenProcess is:
Declare Function OpenProcess Lib "kernel32" ( _ ByVal dwDesiredAccess As Long, _ ByVal bInheritHandle As Long, _ ByVal dwProcessId As Long _ ) As Long
Since getting a process handle from the process ID will come up from time to time, let us create a small function for this purpose. Note that access to the process is for getting information only. The function is:
Public Function ProcHndFromProcID(ByVal lProcID As Long) As Long ' DON'T FORGET TO CLOSE HANDLE ProcHndFromProcID = OpenProcess ( _ PROCESS_QUERY_INFORMATION Or PROCESS_VM_READ, 0&, lProcID) End Function
For Windows NT, we should add an additional flag for use with thread synchronization (discussed in Chapter 12):
Public Function ProcHndFromProcIDSync(ByVal lProcID As Long) As Long ' DON'T FORGET TO CLOSE HANDLE ProcHndFromProcIDSync = OpenProcess( _ PROCESS_QUERY_INFORMATION Or PROCESS_VM_READ Or SYNCHRONIZE, 0&, lProcID) End Function
Page 166
It is very important to remember that the returned handle must be closed after it is no longer needed. This is done by calling the CloseHandle function:
BOOL CloseHandle( HANDLE hObject // handle to object to close );
or, in VB:
Private Declare Function CloseHandle Lib "kernel32" ( _ ByVal hObject As Long _ ) As Long
We will see examples of using OpenProcess a bit later in this chapter.
B: Module Filenames and Module Handles
It is not difficult to go from a module filename to a module handle and vice versa, at least from within the process itself. The GetModuleFileName function
DWORD GetModuleFileName(
HMODULE hModule,
// handle to module
LPTSTR lpFilename,
// pointer to buffer to receive module path
DWORD nSize
// size of buffer, in characters
);
uses the module's handle to retrieve the fully qualified filename (name and path) of the executable file. Note, however, that this function works from within the process in question we cannot use this function from another process.
Conversely, from within a process, the GetModuleHandle function returns the module's handle from its filename (no path here):
HMODULE GetModuleHandle(
LPCTSTR lpModuleName
// module name
);
Again, this does not work for retrieving module handles in foreign processes.
These two function declarations can be translated into VB as follows:
Declare Function GetModuleFileName Lib "kernel32" _ Alias "GetModuleFileNameA" ( _ ByVal hModule As Long, _ ByVal lpFileName As String, _ ByVal nSize As Long _ ) As Long
Declare Function GetModuleHandle Lib "kernel32" _ Alias "GetModuleHandleA" ( _ ByVal lpModuleName As String _ ) As Long
Given either the module's handle, name, or fully qualified name, the function in Example 11-1 will retrieve the other two items in its OUT parameters.
Page 167
Example 11-1. Getting a Module's Handle, Name, or Fully Qualified Name
Public Function Proc_ModuleInfo(ByRef lHnd As Long, _ ByRef sName As String, _ ByRef sFQName As String) As Long
' Algorithm: ' If lHnd = -1 then get EXE handle, name and FQ name ' ElseIf lHnd > 0 then get module name and fully qualified name ' ElseIf sName <> then get handle and FQ name ' ElseIf sFQName <> then get handle and name
'' Required: Public Const MAX_PATH = 260
Dim lret As Long, x as Integer Dim hModule As Long, s As String
Proc_ModuleInfo = 0
If lHnd = -1 Or lHnd > 0 Then hModule = lHnd ' Get handle If lHnd = -1 Then ' Get EXE handle hModule = GetModuleHandle(vbNullString) If hModule = 0 Then Proc_ModuleInfo = 1 Exit Function End If End If ' Get names from handle s = String(MAX_PATH + 1, 0) lret = GetModuleFileName(hModule, s, MAX_PATH) sFQName = Left(s, lret) x = InStrRev(sFQName, \ ) If x > 0 Then sName = Mid$(sFQName, x + 1)
ElseIf sName <> Then
' Get handle and FQ name from name hModule = GetModuleHandle(sName) If hModule = 0 Then Proc_ModuleInfo = 2 Exit Function Else lHnd = hModule s = String(MAX_PATH + 1, 0) lret = GetModuleFileName(hModule, s, MAX_PATH) sFQName = Left(s, lret) End If
ElseIf sFQName <> Then
'Get handle and name from FQ name hModule = GetModuleHandle(sFQName) If hModule = 0 Then
Page 168
Example 11-1. Getting a Module's Handle, Name, or Fully Qualified Name (continued)
Proc_ModuleInfo = 3 Exit Function Else lHnd = hModule x = InStrRev(sFQName, \ ) If x > 0 Then sName = Mid$(sFQName, x + 1) End If End If End Function
Example 11-2 shows some code that exercises this function. The output on my system is shown in Example 11-3.
Example 11-2 Calling the Proc_ModuleInfo Function
Public Sub Proc_ModuleInfoExample
Dim sModName As String, sFQModName As String, hMod As Long Dim lret As Long
To get the process ID of the current process, we just use GetCurrentProcessId, which is declared as:
DWORD GetCurrentProcessId(VOID)
or, in VB:
Declare Function GetCurrentProcessId Lib "kernel32" () As Long
This simply returns the process ID. Note that process IDs can be in the upper range of an unsigned long, so a conversion of the return type may be necessary. Note also that this works only for the current process. There is no way (of which I am aware) to get the process ID of a foreign process, other than possibly listing all processes and picking the one you want using some other criteria.
D: Getting the Process ID from a Window
In Chapter 6, Strings, we discussed the FindWindow function:
HWND FindWindow(
LPCTSTR lpClassName,
// pointer to class name
LPCTSTR lpWindowName
// pointer to window name
);
or, in VB:
Declare Function FindWindow Lib "user32" Alias "FindWindowA" ( _ ByVal lpClassName As String, _ ByVal lpWindowName As String _ ) As Long
This function uses either the class name or the window's caption to retrieve a handle to the window. From this handle, we can call GetWindowThreadProcessId,
Page 170
which retrieves the thread ID of the thread that created the window, as well as the process ID of the process that owns that thread. The syntax is:
DWORD GetWindowThreadProcessId( HWND hWnd, // handle to window LPDWORD lpdwProcessId // address of variable for process identifier );
This function returns the thread ID. Moreover, when the function is passed a pointer to a DWORD in lpdwProcessId (as opposed to being passed 0), the function will return the process ID in the target variable.
We can translate this into VB as follows:
Declare Function GetWindowThreadProcessId Lib "user32" ( _ ByVal hwnd As Long, _ lpdwProcessId As Long _ ) As Long
Here is a short function that returns the process ID from a window handle:
Public Function ProcIDFromhWnd(ByVal hWnd As Long) As Long Dim lret As Long, hProcessID As Long lret = GetWindowThreadProcessId(hWnd, hProcessID) ProcIDFromhWnd = hProcessID End Function
We will discuss window handles in more detail in Chapter 15, Windows: The Basics.
E: Getting Module Names and Handles
A single process generally has many modules loaded in its address space, and this naturally complicates the issue of getting the module handles and names. While it is not difficult to do so, we must unfortunately use entirely different methods in Windows 9x and Windows NT.
Windows NT 4.0 requires the use of a DLL called PSAPI.DLL (which stands for Process Status API). This DLL, which is not compatible with Windows 9x, exports functions to enumerate all of the processes in the system, as well as all of the device drivers. It can also get information about all of the modules running in a given process. There will be an example that makes use of this DLL later in this chapter. To enumerate the threads in a Windows NT system, we need to use the Performance Data Helper library (PDH.DLL) that comes with the NT Resource Toolkit. However, since we don't have any real need for enumerating threads, we will not do so.
On the other hand, Windows 9x supports the Toolhelp functions in the Windows 9x version of KERNEL32.DLL. These functions can be used to take a snapshot of
Page 171
the process space for any process. From this snapshot, we can get all sorts of information, including the current processes as well as the modules and threads for each process. (However, unlike PSAPI.DLL, it does not give device driver information.)
This unfortunate situation requires us to write different code for Windows NT and for Windows 9x. Fortunately, Windows 2000 will support Toolhelp. (I only hope it will also continue to support the perfectly good PSAPI.DLL, so I don't need to rewrite my code!)
Before looking at an example, however, let us complete the process handle story.
Process Pseudohandles
There is yet another wrinkle in the saga of process handles and process IDs. The function GetCurrentProcess:
HANDLE GetCurrentProcess(VOID)
returns a pseudohandle to the current process. A pseudohandle is a kind of lightweight handle. In particular, a pseudohandle is a process-specific number that identifies the process and can be used in calls to API functions that require a process handle.
While pseudohandles sound like ordinary handles, there are some key differences. Pseudohandles cannot be inherited by child processes, as can real handles. Also, a pseudohandle can refer only to the current process, whereas a real handle can refer to a foreign process.
Windows does provide a way to get a real handle from a pseudohandle, by using the DuplicateHandle API function, which we discussed in Chapter 10, Objects and Their Handles. This function is defined as:
BOOL DuplicateHandle(
HANDLE hSourceProcessHandle,
// handle to the source process
HANDLE hSourceHandle,
// handle to duplicate
HANDLE hTargetProcessHandle,
// handle to process to duplicate to
LPHANDLE lpTargetHandle,
// pointer to duplicate handle
DWORD dwDesiredAccess,
// access for duplicate handle
BOOL bInheritHandle,
// handle inheritance flag
DWORD dwOptions
// optional actions
);
Here is some illustrative code, showing how to get process handles and pseudohandles:
Public Const PROCESS_VM_READ = &H10 Public Const PROCESS_QUERY_INFORMATION = &H400 Public Const DUPLICATE_SAME_ACCESS = &H2
Page 172
Public Sub DuplicateHandleExample()
Dim lret As Long Dim hPseudoHandle As Long Dim hProcessID As Long Dim hProcess As Long Dim hDupHandle As Long
' Process ID hProcessID = GetCurrentProcessId Debug.Print "Current process ID: " & hProcessID
This code duplicates the source pseudohandle into the variable hDupHandle. It also gets a real (but different) handle using OpenProcess. Note that the real handles must be closed. However, pseudohandles do not need to be closed. The output on my system is:
Current process ID: 183 Pseudohandle: -1 DuplicateHandle handle: 412 OpenProcess Handle: 392
Now let us consider the problem of enumerating the processes on a system. As we have said, the technique differs under Windows 9x and Windows NT.
Enumerating Processes Under Windows NT
We will use the following functions from PSAPI.DLL:
EnumProcesses Enumerates the process IDs for each process in the system.
EnumProcessModules Enumerates the handle of each module in a given process.
GetModuleBaseName Retrieves the name of a module from its handle.
GetModuleFileNameEx Retrieves the fully qualified path of a module from its handle.
GetModuleInformation Retrieves information about a module.
GetProcessMemoryInfo Retrieves information about the memory usage of a process.
The accompanying CD contains an application called rpiEnumProcsNT that displays each process, along with the EXE filename for that process. In addition, the utility displays the modules that are in the selected process's address space and some information related to memory usage by the process. Figure 11-2 shows the utility's main window. The complete source code is on the CD.
Hitting the MemMap button produces a page-by-page memory map (one page is 4 KB) of the selected process's address space, as shown in Figure 11-3. We will discuss this memory map in Chapter 13, Windows Memory Architecture.
Clicking the Refresh button starts the process of enumerating the system's processes. The EnumProcesses function is:
BOOL EnumProcesses(
DWORD *lpidProcess,
// array to receive the process identifiers
DWORD cb,
// size of the DWORD array in bytes
DWORD *cbNeeded
// receives the number of bytes returned
);
Since the first parameter is a pointer to the first DWORD in an array of DWORDs, we can pass the first DWORD by reference in VB. Now, a DWORD is actually an unsigned long, so we must use a VB long and do any necessary signed-to-unsigned conversion. However, process IDs are very small numbers (they seem to start at 1 and increase consecutively), and cbNeeded is just a count of bytes, so no
Page 174
Figure 11-2. Enumerating processes under Windows NT
conversion will be needed for either DWORD* parameter. Hence, one possible VB declaration is:
Public Declare Function EnumProcesses Lib "PSAPI.DLL" ( _ idProcess As Long, _ ByVal cBytes As Long, _ cbNeeded As Long _ ) As Long
Note that Win32 supplies no direct way to tell how large to make the idProcess array. The only technique is to guess, try the function, and then compare the returned value of cbNeeded, which contains the number of bytes returned (that is, four times the number of longs returned), with the size of our array. If the number of longs returned is equal to the size of our array (it is guaranteed never to be bigger), then we need to increase the size of the array and try again!
The GetProcesses procedure shown in Example 11-4 begins with this sort of Terpsichore. Once the processes are enumerated, we sort them and use the OpenProcess function within a For loop to get handles to each process, with which we can call
Page 175
Figure 11-3. A process memory map
such functions as GetModuleNameEx. Note that we use the EnumProcessModules function to get the first module in each process, since this is always the EXE for the process. (You will find the RaiseAPIError function defined at the end of Chapter 3, API Declarations.)
Example 11-4. Enumerating Processes
Public Const PROCESS_VM_READ = &H10 Public Const PROCESS_QUERY_INFORMATION = &H400
Dim i As Integer, j As Integer, l As Long Dim cbNeeded As Long Dim hEXE As Long Dim hProcess As Long Dim lPriority As Long
' Initial guess cProcesses = 25
Do ' Size array ReDim lProcessIDs(1 To cProcesses)
Page 176
Example 11-4. Enumerating Processes (continued)
' Enumerate lret = EnumProcesses(lProcessIDs(1), cProcesses * 4, cbNeeded) If lret = 0 Then RaiseApiError Err.LastDllError Exit Sub 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
' Sort by processID For i = 1 To cProcesses For j = i + 1 To cProcesses If lProcessIDs(i) > lProcessIDs(j) Then ' Swap l = lProcessIDs(i) lProcessIDs(i) = lProcessIDs(j) lProcessIDs(j) = 1 End If Next Next
ReDim Preserve lProcessIDs(1 To cProcesses) ReDim sEXENames(1 To cProcesses) ReDim sFQEXENames(1 To cProcesses) ReDim sPriorityClass(1 To cProcesses)
' Now we have the process IDs ' Use OpenProcess to get a handle to each process For i = 1 To cProcesses
' 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
Page 177
Example 11-4. Enumerating Processes (continued)
Case 28 sEXENames(i) = csrss.exe (Win32) sFQEXENames(i) = csrss.exe (Win32) End Select
' If error skip this process If hProcess <> 0 Then
' 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
' 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))
' Get priority lPriority = GetPriorityClass(hProcess) Select Case lPriority Case IDLE_PRIORITY_CLASS sPriorityClass(i) = idle Case NORMAL_PRIORITY_CLASS sPriorityClass(i) = normal Case HIGH_PRIORITY_CLASS sPriorityClass(i) = high Case REALTIME_PRIORITY_CLASS sPriorityClass(i) = real Case Else sPriorityClass(i) = ??? End Select
End If ' EXE <> 0
End If ' hProcess <> 0
' Close handle lret = CloseHandle(hProcess)
Next
End Sub
Page 178
When the user selects a process, the utility goes through a very similar procedure to enumerate the modules for the selected process, using EnumProcessModules, GetModuleBaseName, and GetModuleFileNameEx. In addition, it calls GetModuleInformation, which fills the MODULEINFO structure:
Type MODULEINFO
lpBaseOfDll As Long
' pointer to starting point of module
SizeOfImage As Long
' size in bytes of module
EntryPoint As Long
' pointer to entry point of module
End Type
From this structure, we retrieve the size of the module and its base address.
Also, we can get some memory information by calling GetProcessMemoryInfo. This fills a structure with all sorts of interesting stuff, but we single out the working set size, the page file usage, and the page fault count. We will discuss memory in more detail in Chapter 13, but, briefly, here is what you need to know:
The working set size is the amount of actual physical RAM that is currently used by the process.
The page file usage is the amount of the Windows page file (swap file) that is being used by the selected process.
The page fault count is the number of times that the process tried to access a memory address that was not currently mapped to actual physical memory. This necessitated a swapping of a memory page out of RAM to make room for the requested page.
Device drivers are also executable files, but they are system-wide, rather than belonging to a specific process. The functions EnumDeviceDrivers, GetDeviceDriverBaseName, and GetDeviceDriverFileName are similar to the corresponding functions for processes and modules.
Once we have module and device driver information, we can create a memory map (at the bottom of the screen). Clicking on a module or driver will change its memory map entry to white for easier identification. Note how the memory map changes as you scroll through the list of processes. We will discuss memory maps in Chapter 13.
Enumerating Processes Under Windows 9x
The Windows 9x version of the previous utility is also on the CD. The main window is shown in Figure 11-4.
The code uses the Toolhelp DLL mentioned in the section ''Getting Module Names and Handles" earlier in this chapter. While the Win95 version does not enumerate drivers, it does take advantage of the ability of user mode code to examine the upper region of a process's address space.
Page 179
Figure 11-4. Enumerating processes under Windows 9x
Note that, by comparing the memory maps for a process under Windows NT and Windows 95/98, it is possible to see that Windows 95/98 puts the Win32 (and other) system DLLs in a different location than Windows NT. For example, under Windows NT, KERNEL32.DLL is just under the 2GB mark, which is in the area of memory reserved for applications. However, under Windows 95/98, this DLL is at the 3GB mark, in the area of memory reserved for the operating system.
In fact, Figure 11-5 shows the upper portion of the memory map of a Win95 process. Note the location of KERNEL32.DLL and the other system DLLs. Under Windows NT, this memory area is protected. We will discuss memory maps in more detail in Chapter 13.
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.
Page 180
Figure 11-5. The upper portion of a Win95 process space
Using FindWindow
The first method is the simplest, but works 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
Page 181
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 its final value:
Private Sub Form_Activate() Me.Caption = "rpiClipViewer" End Sub
Now, during the Form_Load event, 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, but 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 ourselves, so to speak.
The rpiAccessProcess DLL that we will discuss in Chapter 20, 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 Chapter 20. In any
Page 182
case, you should be able to use this function whenever you need SetForegroundWindow under Windows 98/2000. (By the way, I do believe that this feature should be used only in very special situations.)
Using a Usage Count
Conceptually, the simplest approach to this problem is just to have our VB application maintain a small text file that contains a single number serving as a usage count for the application and that is located in some fixed directory, such as the Windows system directory. 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.
Page 183
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. The entire VC++ source code is shown in Example 11-5.
Example 11-5. VC++ Source Code for the rpiUsageCount DLL
// 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);
BOOL WINAPI DllMain (HANDLE hInst, ULONG ul_reason_for_call, LPVOID lpReserved) { // Keep the instance handle for later use hDLLInst = hInst;
Page 184
Example 11-5. VC++ Source Code for the rpiUsageCount DLL (continued)
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 giUsageCount. The DLL exports four functions for use with this variable. (This is more than is needed, but I got carried away.)
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 code shown in Example 11-6 to the Load and Unload events of the main VB form.
Page 185
Example 11-6. Calling the rpiGetUsageCount Function
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 and 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 filename (and perhaps even complete path). Unfortunately, as we have seen, this requires different code under Windows NT and Windows 9x. 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 filename or a fully qualified EXE name (path and filename). 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. Examples 11-7 and 11-8 show the code (both versions), including the necessary declarations.
Example 11-7. Walking the Windows NT Process List
Option Explicit '************************* ' NOTE: Windows NT 4.0 only ' *************************
Page 186
Example 11-7. Walking the Windows NT Process List (continued)
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)
Page 187
Example 11-7. Walking the Windows NT Process List (continued)
' 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 ' -------------
Page 188
Example 11-7. Walking the Windows NT Process List (continued)
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 ' ----------------
Page 189
Example 11-7. Walking the Windows NT Process List (continued)
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 9x version uses Toolhelp. The corresponding function (and required declarations) is shown in Example 11-8.
Example 11-8. Walking the Windows 9x Process List
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
Public Declare Function CreateToolhelp32Snapshot Lib kernel32 ( _ ByVal dwFlags As Long, _ ByVal th32ProcessID As Long _ ) As Long
Public Declare Function Process32First Lib kernel32 ( _
Page 190
Example 11-8. Walking the Windows 9x Process List (continued)
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)
Page 191
Example 11-8. Walking the Windows 9x Process List (continued)
' 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 ' ----------
Page 192
Example 11-8. Walking the Windows 9x Process List (continued)
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