Figure 2   ModuleList.CPP


//==================================================
// ModuleList - Matt Pietrek 1998
// Microsoft Systems Journal, September 1998
// FILE: ModuleList.CPP
//==================================================
#include <windows.h>
#include <COMMCTRL.H>
#pragma hdrstop
#include "ModuleList.h"
#include "ModuleListClasses.h"
#include "ModuleListOSCode.h"

// Helper function prototypes
void    Handle_WM_INITDIALOG(HWND hDlg);
void    Handle_WM_COMMAND(HWND hWndDlg, WPARAM wParam, LPARAM lParam );
void    Handle_WM_CLOSE( HWND hDlg );
void    Handle_WM_SIZE(HWND hWndDlg, WPARAM wParam, LPARAM lParam );
BOOL    CALLBACK ModuleListDlgProc(HWND,UINT,WPARAM,LPARAM);
void    GetSetPositionInfoFromRegistry( BOOL fSave, POINT *lppt );
void    PopulateTree( HWND hWndTree );

HTREEITEM AddTreeviewSubItem(   HWND hWndTree,
                                HTREEITEM hTreeItem,
                                LPTSTR pszItemText,
                                BOOL fItemData = FALSE,
                                LPARAM itemData = 0 );

BOOL GetFileDescription( PSTR pszFileName, PSTR pszDesc, unsigned cbDesc );
                                
// ======================= String literals ===============================
char gszRegistryKey[] = "Software\\WheatyProductions\\ModuleList";

char gszMBTitle[] =  "ModuleList, by Matt Pietrek - MSJ September 1998" ;

char gszAboutText[] =   "ModuleList shows all of the loaded DLLs in the "
                        "system.  Each DLL node contains the filename, the "
                        "load address, the directory where the DLL was loaded "
                        "from, and the number of processes using the DLL.";
                        
    // =========================== Global Variables =============================
HWND g_hWndTree = 0;    // HWND of the TreeView control
HWND g_hDlg = 0;        // HWND of the dialog

ModuleList          g_ModuleList;           // List of all loaded modules
ProcessIdToNameMap  g_ProcessIdToNameMap;   // List of all process names & IDs

// ============================== Start of code ===============================

int WINAPI WinMain( HINSTANCE hInstance, HINSTANCE hPrevInstance,
                    LPSTR lpszCmdLine, int nCmdShow )
{
    InitCommonControls();   // Needed for the TreeView control
    
    // Bring up the user interface
    DialogBox(  hInstance, MAKEINTRESOURCE(IDD_MODULELIST),
                0, (DLGPROC)ModuleListDlgProc );
    return 0;
}

BOOL
CALLBACK ModuleListDlgProc( HWND hDlg,UINT msg,WPARAM wParam,LPARAM lParam )
{
    //
    // The dialog procedure for the main window
    //
    switch ( msg )
    {
        case WM_INITDIALOG:
            Handle_WM_INITDIALOG( hDlg ); return TRUE;
        case WM_CLOSE:
            Handle_WM_CLOSE( hDlg ); break;
        case WM_SIZE:
            Handle_WM_SIZE( hDlg, wParam, lParam ); break;
        case WM_COMMAND:
            Handle_WM_COMMAND( hDlg, wParam, lParam ); break;
        // let everything else fall through
    }
    return FALSE;
}

//============================================================================
// Walk through the list of objects, adding each object name to the root of
// the treeview
//============================================================================

void PopulateTree( HWND hWndTree )
{
    // Empty the TreeView
    TreeView_DeleteAllItems( hWndTree );

    // Empty out the data structures (if necessary)
    g_ModuleList.Clear();
    g_ProcessIdToNameMap.Clear();

    // Populate the data structures.  Try using the Toolhelp32 APIs first
    BOOL fModListOK;
    
    fModListOK = PopulateModuleList_ToolHelp32(
                                    g_ModuleList, g_ProcessIdToNameMap );
    if ( !fModListOK )
    {
        // ToolHelp32 didn't work (probably wasn't present).  Try PSAPI.DLL 
        fModListOK = PopulateModuleList_PSAPI(
                            g_ModuleList, g_ProcessIdToNameMap );

        if ( !fModListOK )  // PSAPI.DLL probably wasn't found
            return;
    }

    // Enumerate the module list, adding the info for each DLL to the TreeView

    PModuleInstance pModInst = 0;   // 0 begins the enumeration
    
    while ( pModInst = g_ModuleList.Enumerate(pModInst) )
    {
        // Make a copy of the full DLL path that we can slice and dice
        char szFullModuleName[MAX_PATH+128];
        lstrcpy( szFullModuleName, pModInst->m_pszName );
        
        PSTR pszBaseName = strrchr( szFullModuleName, '\\' );
        *pszBaseName = 0;   // Separate base name from path
        pszBaseName++;      // Advanced past the null separator we just added
        strupr( pszBaseName );

        // Create the top level string for the DLL, then add it to the TreeView
        char szModuleDescription[MAX_PATH+128];     
        wsprintf( szModuleDescription, "%s  (%s)",
                  pszBaseName, szFullModuleName );

        HTREEITEM hTreeItem = AddTreeviewSubItem(   g_hWndTree,
                                                    NULL,
                                                    szModuleDescription );

        // Create and add a subitem string describing the load address and
        // DLL reference count
        char szOtherModuleInfo[MAX_PATH+128];       
        wsprintf( szOtherModuleInfo, "* Load address:%08X  Reference count:%u",
                  pModInst->m_hModule,pModInst->GetNumberOfProcessReferences() );

        AddTreeviewSubItem( g_hWndTree, hTreeItem, szOtherModuleInfo );

        //
        // Try to retrieve the file description from the file's version
        // resource.  If found, add it as another subitem
        //
        char szFileDesc[1024];  
        if ( GetFileDescription(pModInst->m_pszName,
                                szFileDesc, sizeof(szFileDesc)) )
        {
            char szBuffer[ sizeof(szFileDesc) + 64 ];
            wsprintf( szBuffer, "* Description: %s", szFileDesc );
            AddTreeviewSubItem( g_hWndTree, hTreeItem, szBuffer );
        }

        // Iterate through each process that references the DLL, and add it
        // as a subitem             
        int enumHandle = 0;     // Passing 0 begins the enumeration
        DWORD pid;              // -1 means end of pid list
        
        while ( -1 != (pid = pModInst->EnumerateProcessReferences(enumHandle)))
        {
            char szProcessInfo[MAX_PATH+128];
            
            wsprintf( szProcessInfo, "%s", g_ProcessIdToNameMap.Lookup(pid) );

            AddTreeviewSubItem( g_hWndTree, hTreeItem, szProcessInfo );
        }
    }
}


void Handle_WM_INITDIALOG(HWND hDlg)
{
    // Get the window coordinates where the program was last running,
    // and move the window to that spot.
    POINT pt;
    GetSetPositionInfoFromRegistry( FALSE, &pt );

    SetWindowPos(hDlg, 0, pt.x, pt.y, 0, 0,
                 SWP_NOSIZE | SWP_NOREDRAW | SWP_NOZORDER | SWP_NOACTIVATE);

    g_hDlg = hDlg;
    g_hWndTree = GetDlgItem(hDlg, IDC_TREE1);
    
    PopulateTree( g_hWndTree );    
}

void Handle_WM_COMMAND(HWND hWndDlg, WPARAM wParam, LPARAM lParam )
{
    WORD wNotifyCode = HIWORD(wParam); // notification code 
    WORD wID = LOWORD(wParam);         // item, control, or accelerator id
    HWND hwndCtl = (HWND) lParam;      // handle of control
    
    if ( IDC_BUTTON_REFRESH == wID )
    {
        if ( BN_CLICKED == wNotifyCode )
            PopulateTree( g_hWndTree );         
    }
    else if ( IDC_BUTTON_ABOUT == wID )
    {
        if ( BN_CLICKED == wNotifyCode )
            MessageBox( hWndDlg, gszAboutText, gszMBTitle, MB_OK );
    }
}

void Handle_WM_CLOSE( HWND hDlg )
{
    // Save off the window's X,Y coordinates for next time
    RECT rect;
    if ( GetWindowRect( hDlg, &rect ) )
        GetSetPositionInfoFromRegistry( TRUE, (LPPOINT)&rect );
  
    EndDialog(hDlg, 0);
}

void Handle_WM_SIZE(HWND hWndDlg, WPARAM wParam, LPARAM lParam )
{
    RECT tvRect, dlgRect;
    POINT pt;
    
    WORD nClientWidth = LOWORD(lParam);
    WORD nClientHeight= HIWORD(lParam);
    
    GetClientRect( hWndDlg, &dlgRect );     // Get size of dialog
    GetWindowRect( g_hWndTree, &tvRect );   // Get screen position of child
    
    pt.x = tvRect.left;             // Get the X,Y coordinates for the top left
    pt.y = tvRect.top;              // and reuse the M in the resized client 
    ScreenToClient( hWndDlg, &pt ); // Calculate screen X,Y of child window
        
    WORD tvWidth = nClientWidth - ( pt.x * 2);  // Equal spacing on all borders
    WORD tvHeight= nClientHeight - (WORD)(pt.y + pt.x);
    MoveWindow( g_hWndTree, pt.x, pt.y, tvWidth, tvHeight, TRUE );  
}

void GetSetPositionInfoFromRegistry( BOOL fSave, POINT *lppt )
{
    //
    // Function that saves or restores the coordinates of a dialog box
    // in the system registry.  Handles the case where there's nothing there.
    //
    HKEY hKey;
    DWORD dataSize, err, disposition;
    char szKeyName[] = "DlgCoordinates";
    
    if ( !fSave )               // In case the key's not there yet, we'll
        lppt->x = lppt->y = 0;  // return 0,0 for the coordinates

    // Open the registry key (or create it if the first time being used)
    err = RegCreateKeyEx( HKEY_CURRENT_USER, gszRegistryKey, 0, 0,
                          REG_OPTION_NON_VOLATILE, KEY_ALL_ACCESS,
                          0, &hKey, &disposition );
    if ( ERROR_SUCCESS != err )
        return;

    if ( fSave )            // Save out coordinates
    {
        RegSetValueEx(hKey,szKeyName, 0, REG_BINARY,(PBYTE)lppt,sizeof(*lppt));
    }
    else                    // read in coordinates
    {
        dataSize = sizeof(*lppt);
        RegQueryValueEx( hKey, szKeyName, 0, 0, (PBYTE)lppt, &dataSize );
    }

    RegCloseKey( hKey );
}

HTREEITEM AddTreeviewSubItem(   HWND hWndTree,
                                HTREEITEM hTreeItem,
                                LPTSTR pszItemText,
                                BOOL fItemData,
                                LPARAM itemData )
{
    TVINSERTSTRUCT tvi;
    
    tvi.hParent = hTreeItem;
    tvi.hInsertAfter = TVI_SORT;
    tvi.item.mask = TVIF_TEXT;
    tvi.item.pszText = pszItemText;
    tvi.item.cchTextMax = lstrlen( pszItemText );
    if ( fItemData )
    {
        tvi.item.mask |= TVIF_PARAM;
        tvi.item.lParam = itemData;
    }

    return TreeView_InsertItem( hWndTree, &tvi );
}

BOOL GetFileDescription( PSTR pszFileName, PSTR pszDesc, unsigned cbDesc )
{
    //
    // Give a filename, tries to extract the FileDescription version resource
    //
    BYTE verInfo[8192];
    DWORD cbVerInfo = sizeof(verInfo);
    
    if ( !GetFileVersionInfo(pszFileName, 0, cbVerInfo, verInfo) )
        return FALSE;

    PSTR pszVerRetVal;
    UINT cbReturn;
    BOOL fFound;

    // Try first with the 1252 codepage.  To do so, we need to format a
    // string with the 1252 codepage (Windows Multilingual)
    char szQueryStr[0x100];
    wsprintf( szQueryStr, "\\StringFileInfo\\%04X%04X\\FileDescription",
              GetUserDefaultLangID(), 1252 );

    fFound = VerQueryValue( verInfo, szQueryStr,
                            (LPVOID *)&pszVerRetVal, &cbReturn );
    if ( !fFound )
    {
        // Hmm... 1252 wasn't found.  Try the 1200 codepage
        wsprintf( szQueryStr, "\\StringFileInfo\\%04X%04X\\FileDescription",
                  GetUserDefaultLangID(), 1200 );
        fFound = VerQueryValue( verInfo, szQueryStr,
                                (LPVOID *)&pszVerRetVal, &cbReturn );
    }

    if ( fFound )   // If we found the string, copy it to the return buffer
    {
        lstrcpyn( pszDesc, pszVerRetVal, min(cbReturn+1, cbDesc) );
        return TRUE;
    }
        
    return FALSE;       
}

ModuleListClasses.H


class ModuleList;
//=============================================================================
// ModuleInstance class:
//      Represents exactly one loaded module (DLL).  DLLs with the same
//      name, but in different directories are distinct.  Likewise, if the
//      the DLL is loaded at a different address is multiple processes, each
//      distinct load address is represented by a unique ModuleInstance.
//
//      Besides the HMODULE and name, the class also keeps a list of process
//      IDs that have this module loaded.
//=============================================================================
class ModuleInstance
{
    friend class ModuleList;
    
    ModuleInstance * m_pNext;

    DWORD   m_nProcessReferences;  // Number of referencing process IDs
    PDWORD  m_pProcessReferences;  // Array of process IDs (initially empty)
    
    public:

    ModuleInstance( HMODULE hModule, PSTR pszName );
    ~ModuleInstance(void);
    
    BOOL    IsEqual( HMODULE hModule, PSTR pszName );
    BOOL    AddProcessReference( DWORD pid );
    DWORD   EnumerateProcessReferences( int & enumHandle );
    DWORD   GetNumberOfProcessReferences( void ){return m_nProcessReferences;}    
            
    PSTR    m_pszName;
    HMODULE m_hModule;
};
typedef ModuleInstance * PModuleInstance;

//=============================================================================
// ModuleList class:
//      A simple linked list container for instances of the ModuleInstance
//      class.  Methods are provided to lookup, add, and enumerate the list.
//=============================================================================
class ModuleList
{
    PModuleInstance m_pModuleInstanceList;
    
    public:

    ModuleList( void ){ m_pModuleInstanceList = 0; }
    ~ModuleList( void ){ Clear(); }
    void Clear( void );     // Empty the list

    PModuleInstance Lookup( HMODULE hModule, PSTR pszName );
    PModuleInstance Add( HMODULE hModule, PSTR pszName );
    PModuleInstance Enumerate( PModuleInstance pModInst );
    
};

//=============================================================================
// ProcessIdToNameMap class:
//      A simple array based mapping between process IDs and the complete
//      pathname to the EXE for the process.
//=============================================================================
class ProcessIdToNameMap
{
    struct ProcessIdName    // A private class.  Each process has one instance
    {
        DWORD   m_pid;
        PSTR    m_pszName;
    };
    
    ProcessIdName * m_array;
    DWORD           m_nEntries;
    
    public:
    
    ProcessIdToNameMap( void ){ m_array = 0; m_nEntries = 0; }
    ~ProcessIdToNameMap( void ){ Clear(); }
    void Clear( void );
        
    BOOL Add( DWORD pid, PSTR pszName );
    PSTR Lookup( DWORD pid );   
};

ModuleListOSCode.CPP


//==================================================
// ModuleList - Matt Pietrek 1998
// Microsoft Systems Journal, September 1998
// FILE: ModuleListOSCode.CPP
//==================================================
#include <windows.h>
#pragma hdrstop
#include "tlhelp32.h"
#include "ModuleListClasses.h"
#include "ModuleListOSCode.h"

//==================== typedefs for ToolHelp32 functions =====================

typedef HANDLE (WINAPI * PFNCREATETOOLHELP32SNAPSHOT)(
    DWORD dwFlags, DWORD th32ProcessID);
    
typedef BOOL (WINAPI * PFNPROCESS32FIRST)(
    HANDLE hSnapshot, LPPROCESSENTRY32 lppe);
    
typedef BOOL (WINAPI * PFNPROCESS32NEXT)(
    HANDLE hSnapshot, LPPROCESSENTRY32 lppe);
    
typedef BOOL (WINAPI * PFNMODULE32FIRST)(
    HANDLE hSnapshot, LPMODULEENTRY32 lpme);
    
typedef BOOL (WINAPI * PFNMODULE32NEXT)(
    HANDLE hSnapshot, LPMODULEENTRY32 lpme);
    
//====================== Populate the module list using ToolHelp32 ============

BOOL
PopulateModuleList_ToolHelp32(
    ModuleList & modList,
    ProcessIdToNameMap &pidNameMap )
{
    static HMODULE hModKERNEL32 = 0;
    static PFNCREATETOOLHELP32SNAPSHOT pfnCreateToolhelp32Snapshot = 0;
    static PFNPROCESS32FIRST pfnProcess32First = 0;
    static PFNPROCESS32NEXT pfnProcess32Next = 0;
    static PFNMODULE32FIRST pfnModule32First = 0;
    static PFNMODULE32NEXT pfnModule32Next = 0;

    //
    // Hook up to the ToolHelp32 functions dynamically.  We can't just call
    // the functions implicitly, since that would make this program not run
    // under Windows NT 3.X and Windows NT 4
    //
    
    if ( !hModKERNEL32 )
        hModKERNEL32 = GetModuleHandle( "KERNEL32.DLL" );

    pfnCreateToolhelp32Snapshot = (PFNCREATETOOLHELP32SNAPSHOT)
        GetProcAddress( hModKERNEL32, "CreateToolhelp32Snapshot" );
        
    pfnProcess32First = (PFNPROCESS32FIRST)
        GetProcAddress( hModKERNEL32, "Process32First" );
        
    pfnProcess32Next = (PFNPROCESS32NEXT)
        GetProcAddress( hModKERNEL32, "Process32Next" );
        
    pfnModule32First = (PFNMODULE32FIRST)
        GetProcAddress( hModKERNEL32, "Module32First" );
        
    pfnModule32Next = (PFNMODULE32NEXT)
        GetProcAddress( hModKERNEL32, "Module32Next" );
        
    if (    !pfnCreateToolhelp32Snapshot
        ||  !pfnProcess32First || !pfnProcess32Next
        ||  !pfnModule32First || !pfnModule32Next )
        return FALSE;

    //
    // Create a ToolHelp32 snapshot containing the process list
    //  
    HANDLE hSnapshotProcess;
    hSnapshotProcess = pfnCreateToolhelp32Snapshot( TH32CS_SNAPPROCESS, 0 );
    if ( !hSnapshotProcess )
        return FALSE;

    // Iterate through each of the processes in the snapshot
    PROCESSENTRY32 procEntry = { sizeof(PROCESSENTRY32) };
    BOOL fProcessWalkContinue;
        
    for (fProcessWalkContinue = pfnProcess32First(hSnapshotProcess,&procEntry);
         fProcessWalkContinue;
         fProcessWalkContinue = pfnProcess32Next(hSnapshotProcess,&procEntry) )
    {
        // Add the process name and pid to the mapping
        pidNameMap.Add( procEntry.th32ProcessID, procEntry.szExeFile );

        //
        // Enumerate the module list for this process.  Start by taking
        // another ToolHelp32 snapshot, this time of the process's module list
        //
        HANDLE hSnapshotModule;
        hSnapshotModule = pfnCreateToolhelp32Snapshot( TH32CS_SNAPMODULE,
                                                       procEntry.th32ProcessID );
        if ( !hSnapshotModule )
            continue;

        // Iterate through each module in the snapshot                                                  
        MODULEENTRY32 modEntry = { sizeof(MODULEENTRY32) };
        BOOL fModWalkContinue;

        for (fModWalkContinue = pfnModule32First(hSnapshotModule,&modEntry);
             fModWalkContinue;
             fModWalkContinue = pfnModule32Next(hSnapshotModule,&modEntry) )
        {
            // Hack!  Cheezy way to figure out if this is EXE module itself
            // If so, we don't want to add it to the module list
            if ( 0 == stricmp( modEntry.szExePath, procEntry.szExeFile ) )
                continue;

            // Determine if this is a DLL we've already seen                
            PModuleInstance pModInst = modList.Lookup(modEntry.hModule,
                                                      modEntry.szExePath );

            // If we haven't see it, add it to the list
            if ( !pModInst )
                pModInst = modList.Add( modEntry.hModule, modEntry.szExePath );

            // Add this process to the list of processes using the DLL              
            pModInst->AddProcessReference( procEntry.th32ProcessID );       
        }
        
        CloseHandle( hSnapshotModule ); // Done with module list snapshot
    }
    
    CloseHandle( hSnapshotProcess );    // Done with process list snapshot
    
    return TRUE;
}

//====================== typedefs for PSAPI.DLL functions =====================

typedef BOOL (WINAPI * PFNENUMPROCESSES)(
    DWORD * lpidProcess,
    DWORD   cb,
    DWORD * cbNeeded
    );

typedef BOOL (WINAPI * PFNENUMPROCESSMODULES)(
    HANDLE hProcess,
    HMODULE *lphModule,
    DWORD cb,
    LPDWORD lpcbNeeded
    );

typedef DWORD (WINAPI * PFNGETMODULEFILENAMEEXA)(
    HANDLE hProcess,
    HMODULE hModule,
    LPSTR lpFilename,
    DWORD nSize
    );

//=========================== Populate the module list using PSAPI ============

BOOL
PopulateModuleList_PSAPI(
    ModuleList & modList,
    ProcessIdToNameMap &pidNameMap )
{
    static HMODULE hModPSAPI = 0;
    static PFNENUMPROCESSES pfnEnumProcesses = 0;
    static PFNENUMPROCESSMODULES pfnEnumProcessModules = 0;
    static PFNGETMODULEFILENAMEEXA pfnGetModuleFileNameExA = 0;

    //
    // Hook up to the 3 functions in PSAPI.DLL dynamically.  We can't
    // just call the functions implicitly, since that would make this program
    // require the presence of PSAPI.DLL
    //
    if ( !hModPSAPI )
        hModPSAPI = LoadLibrary( "PSAPI.DLL" );

    if ( !hModPSAPI )
        return FALSE;
        
    pfnEnumProcesses = (PFNENUMPROCESSES)
            GetProcAddress( hModPSAPI,"EnumProcesses" );

    pfnEnumProcessModules = (PFNENUMPROCESSMODULES)
            GetProcAddress( hModPSAPI, "EnumProcessModules" );

    pfnGetModuleFileNameExA = (PFNGETMODULEFILENAMEEXA)
            GetProcAddress( hModPSAPI, "GetModuleFileNameExA" );

    if (    !pfnEnumProcesses
        ||  !pfnEnumProcessModules
        ||  !pfnGetModuleFileNameExA )
        return FALSE;
    
    // If we get to this point, we've successfully hooked up to the PSAPI APIs

    DWORD pidArray[1024];
    DWORD cbNeeded;
    DWORD nProcesses;
            
    // EnumProcesses returns an array of process IDs
    if ( !pfnEnumProcesses(pidArray, sizeof(pidArray), &cbNeeded) )
        return FALSE;

    nProcesses = cbNeeded / sizeof(DWORD);  // Determine number of processes
    
    // Iterate through each process in the array
    for ( unsigned i = 0; i < nProcesses; i++ )
    {
        HMODULE hModuleArray[1024];
        HANDLE hProcess;
        DWORD pid = pidArray[i];
        DWORD nModules;
        
        // Using the process ID, open up a handle to the process
        hProcess = OpenProcess( PROCESS_QUERY_INFORMATION |
                                PROCESS_VM_READ,
                                FALSE, pid );
        if ( !hProcess )
            continue;
            
        // EnumProcessModules returns an array of HMODULEs for the process
        if ( !pfnEnumProcessModules(hProcess, hModuleArray,
                                    sizeof(hModuleArray), &cbNeeded ) )
        {
            CloseHandle( hProcess );
            continue;
        }

        // Calculate number of modules in the process                                   
        nModules = cbNeeded / sizeof(hModuleArray[0]);

        // Iterate through each of the process's modules
        for ( unsigned j=0; j < nModules; j++ )
        {
            HMODULE hModule = hModuleArray[j];
            char szModuleName[MAX_PATH];

            // GetModuleFileNameEx is like GetModuleFileName, but works
            // in other process address spaces
            pfnGetModuleFileNameExA(hProcess, hModule,
                                    szModuleName, sizeof(szModuleName) );

            if ( 0 == j )   // First module is the EXE.  Just add it to the map
            {
                pidNameMap.Add( pid, szModuleName );    
            }
            else    // Not the first module.  It's a DLL
            {
                // Determine if this is a DLL we've already seen                
                PModuleInstance pModInst = modList.Lookup(hModule,
                                                          szModuleName );
                // If we haven't see it, add it to the list
                if ( !pModInst )
                    pModInst = modList.Add( hModule, szModuleName );
                    
                // Add this process to the list of processes using the DLL              
                pModInst->AddProcessReference( pid );
            }
        }
    
        CloseHandle( hProcess );    // We're done with this process handle
    }
    
    return TRUE;
}