/****************************************************************************** Copyright (C) Microsoft Corporation 1985-1993. All rights reserved. Title: drvrrare.c - Installable driver code. Less common code Version: 1.00 Date: 10-Jun-1990 Author: DAVIDDS ROBWI ------------------------------------------------------------------------------ Change log: DATE REV DESCRIPTION ----------- ----- ----------------------------------------------------------- 28-FEB-1992 ROBINSP Port to NT 23-Apr-1992 StephenE Unicoded 22-Apr-1993 RobinSp Add NT multithread protection Multithread design : Uses 2 critical sections : DriverListCritSec : protects the list of drivers : hInstalledDriverList - handle of global driver list cInstalledDrivers - high water mark of installed drivers so that only 1 thread at a time has the list locked and can refer to or update it. DriverLoadFreeCritSec Makes sure that actual loads and frees of drivers don't overlap and that the actual loading of a driver via LoadLibrary coincides with its first message being DRV_LOAD. This can easily happen if the DRV_OPEN from another thread can get in before the DRV_LOAD has been sent. *****************************************************************************/ #include #include #include #define MMNOSOUND #define MMNOWAVE #define MMNOMIDI #define MMNOSEQ #define MMNOTIMER #define MMNOJOY #define MMNOMCI #define NOTIMERDEV #define NOJOYDEV #define NOMCIDEV #define NOSEQDEV #define NOWAVEDEV #define NOMIDIDEV #define NOTASKDEV #include #include "drvr.h" extern HANDLE hInstalledDriverList; // List of installed driver instances extern int cInstalledDrivers; // High water count of installed driver instances extern DWORD FAR PASCAL DriverProc(DWORD dwID, HDRVR hdrv, UINT msg, DWORD dw1, DWORD dw2); /* Support for using 3.1 APIs if available */ typedef HANDLE (FAR PASCAL *OPENDRIVER31)(LPCSTR, LPCSTR, LONG); typedef LONG (FAR PASCAL *CLOSEDRIVER31)(HANDLE, LONG, LONG); typedef HANDLE (FAR PASCAL *GETDRIVERMODULEHANDLE31)(HANDLE); typedef LONG (FAR PASCAL *SENDDRIVERMESSAGE31)(HANDLE, UINT, LONG, LONG); typedef LONG (FAR PASCAL *DEFDRIVERPROC31)(DWORD, HANDLE, UINT, LONG, LONG); OPENDRIVER31 lpOpenDriver; CLOSEDRIVER31 lpCloseDriver; GETDRIVERMODULEHANDLE31 lpGetDriverModuleHandle; SENDDRIVERMESSAGE31 lpSendDriverMessage; DEFDRIVERPROC31 lpDefDriverProc; #if 0 BOOL fUseWinAPI = 0; // NOTE: fUseWinAPI is not being used at present // as we only have a partial device loading story #endif /*************************************************************************** strings ****************************************************************************/ #if 0 extern char far szBoot[]; extern char far szUser[]; extern char far szOpenDriver[]; extern char far szCloseDriver[]; extern char far szDrvModuleHandle[]; extern char far szSendDriverMessage[]; extern char far szDefDriverProc[]; extern char far szDriverProc[]; #endif /*************************************************************************** * * @doc DDK * * @api LONG | DrvClose | This function closes an open driver * instance and decrements * the driver's open count. Once the driver's open count becomes zero, * the driver is unloaded. * * @parm HANDLE | hDriver | Specifies the handle of the installable * driver to close. * * @parm LONG | lParam1 | Specifies the first message parameter for * the DRV_CLOSE message. This data is passed directly to the driver. * * @parm LONG | lParam2 | Specifies the second message parameter * for DRV_CLOSE message. This data is passed directly to the driver. * * @rdesc Returns zero if the driver aborted the close; * otherwise, returns the return result from the driver. * @xref DrvOpen * ***************************************************************************/ LONG APIENTRY DrvClose(HANDLE hDriver, LONG lParam1, LONG lParam2) { /* The driver will receive the following message sequence: * * DRV_CLOSE * if DRV_CLOSE returns non-zero * if driver usage count = 1 * DRV_DISABLE * DRV_FREE */ if (fUseWinAPI) return ((*lpCloseDriver)(hDriver, lParam1, lParam2)); else return InternalCloseDriver((UINT)hDriver, lParam1, lParam2, TRUE); } /*************************************************************************** * * @doc DDK * * @api LONG | DrvOpen | This function opens an installable driver. * The first time a driver is opened it is loaded * and enabled. A driver must be opened before messages are sent * to it. * * @parm LPSTR | szDriverName | Specifies a far pointer to a * null-terminated character string * containing a driver filename or a keyname from a * section of the SYSTEM.INI file. * * @parm LPSTR | szSectionName | Specifies a far pointer to a * null-terminated character string containing the name of * the driver section to search. If

is * not null, the specified section of the SYSTEM.INI file is * searched instead of the [Drivers] section. If *

is null, the default [Drivers] section is used. * * @parm LONG | lParam | Specifies a message parameter to * pass to the driver procedure with the message. * * @rdesc Returns a handle to the driver. * * @comm Installable drivers must export a routine of * the form: * * @cb LONG FAR PASCAL | DriverProc | This entry point receives the * messages sent to an installable driver. This entry will always * handle the system messages as a minimum set of messages. * * @parm DWORD | dwDriverIdentifier | Specifies the device driver * identifier. * * @parm HANDLE | hDriver | Specifies the device driver handle. * * @parm UINT | wMessage | Specifies the message for the device * driver. * * @parm LONG | lParm1 | Specifies message dependent data. * * @parm LONG | lParm2 | Specifies message dependent data. * * @xref DrvClose * ****************************************************************************/ HANDLE APIENTRY DrvOpen( LPCWSTR szDriverName, LPCWSTR szSectionName, LONG lParam2) { /* The driver will receive the following message sequence: * * if driver not loaded and can be found * DRV_LOAD * if DRV_LOAD returns non-zero * DRV_ENABLE * if driver loaded correctly * DRV_OPEN */ HDRVR hdrv; if (fUseWinAPI) { /*------------------------------------------------------------*\ * UNICODE: convert szDriver and szSectionName to ascii * and then call WIN31 driver \*------------------------------------------------------------*/ LPSTR aszDriver; LPSTR aszSectionName; INT lenD; INT lenS; lenD = lstrlenW( szDriverName ) * sizeof( WCHAR ) + sizeof( WCHAR ); aszDriver = HeapAlloc( hHeap, 0, lenD ); if ( aszDriver == (LPSTR)NULL ) { return NULL; } lenS = lstrlenW( szSectionName ) * sizeof( WCHAR ) + sizeof( WCHAR ); aszSectionName = HeapAlloc( hHeap, 0, lenS ); if ( aszSectionName == (LPSTR)NULL ) { HeapFree( hHeap, 0, aszDriver ); return NULL; } // Unicode to Ascii UnicodeStrToAsciiStr( (PBYTE)aszDriver, (PBYTE)aszDriver + lenD, szDriverName ); UnicodeStrToAsciiStr( (PBYTE)aszSectionName, (PBYTE)aszSectionName + lenS, szSectionName ); hdrv = (HDRVR)((*lpOpenDriver)( aszDriver, aszSectionName, lParam2 )); HeapFree( hHeap, 0, aszDriver ); HeapFree( hHeap, 0, aszSectionName ); } else { dprintf2(("DrvOpen(%ls), Looking in Win.ini [%ls]", szDriverName, szSectionName ? szSectionName : L"NULL !!" )); hdrv = (HDRVR)InternalOpenDriver(szDriverName, szSectionName, lParam2, TRUE); } #if DBG if (hdrv) { WCHAR ach[255]; static SZCODE szFormat[] = "DrvOpen(): Opened %ls (%ls)\r\n"; GetModuleFileNameW( DrvGetModuleHandle( hdrv ), ach, sizeof(ach) / sizeof(WCHAR) ); dprintf2((szFormat, szDriverName, ach)); } #endif return (HANDLE)hdrv; } /*************************************************************************** * * @doc DDK * * @api HANDLE | DrvGetModuleHandle | This function returns the library * module handle of the specified installable driver. * * @parm HANDLE | hDriver | Specifies the handle of the installable driver. * * @rdesc Returns the module handle of the driver specified by the * driver handle

. * * @comm A module handle is not the same as an installable driver handle. * ***************************************************************************/ HMODULE APIENTRY DrvGetModuleHandle(HDRVR hDriver) { LPDRIVERTABLE lpdt; HMODULE h = 0; if (fUseWinAPI) return ((*lpGetDriverModuleHandle)(hDriver)); DrvEnter(); if (hDriver && ((int)hDriver <= cInstalledDrivers)) { lpdt = (LPDRIVERTABLE)GlobalLock(hInstalledDriverList); h = (HMODULE)lpdt[(UINT)hDriver-1].hModule; GlobalUnlock(hInstalledDriverList); } DrvLeave(); return(h); } LONG FAR PASCAL InternalCloseDriver(UINT hDriver, LONG lParam1, LONG lParam2, BOOL fSendDisable) { LONG result; // check handle in valid range. DrvEnter(); if ((int)hDriver > cInstalledDrivers) { DrvLeave(); return(FALSE); } DrvLeave(); result = DrvSendMessage((HANDLE)hDriver, DRV_CLOSE, lParam1, lParam2); if (result) { InternalFreeDriver(hDriver, fSendDisable); } return(result); } LONG FAR PASCAL InternalOpenDriver( LPCWSTR szDriverName, LPCWSTR szSectionName, LONG lParam2, BOOL fSendEnable) { int hDriver; LPDRIVERTABLE lpdt; LONG result; WCHAR sz[128]; if (0 != (hDriver = InternalLoadDriver( szDriverName, szSectionName, sz, sizeof(sz) / sizeof(WCHAR), fSendEnable ) ) ) { /* * Set the driver identifier to the DRV_OPEN call to the * driver handle. This will let people build helper functions * that the driver can call with a unique identifier if they * want to. */ DrvEnter(); lpdt = (LPDRIVERTABLE)GlobalLock(hInstalledDriverList); lpdt[hDriver-1].dwDriverIdentifier = hDriver; GlobalUnlock(hInstalledDriverList); DrvLeave(); result = DrvSendMessage( (HANDLE)hDriver, DRV_OPEN, (LONG)(LPSTR)sz, lParam2); if (!result) { dprintf1(("DrvSendMessage failed, result = %8x",result)); InternalFreeDriver(hDriver, fSendEnable); } else { DrvEnter(); lpdt = (LPDRIVERTABLE)GlobalLock(hInstalledDriverList); lpdt[hDriver-1].dwDriverIdentifier = result; GlobalUnlock(hInstalledDriverList); DrvLeave(); result = hDriver; } } else result = 0L; return result; } /*************************************************************************** * * @doc INTERNAL * * @api LONG | InternalLoadDriver | Loads an installable driver. If this is * the first time that the driver is opened, the driver will be loaded * and enabled. * * @parm LPSTR | szDriverName | A null-terminated character string * containing a driver filename or a keyname from the [Drivers] * section of system.ini. * * @parm LPSTR | szSectionName | A null-terminated character string * that specifies a driver section to search. If szSectionName is * not null, the specified section of system.ini is searched instead * of the [Drivers] section. If szSectionName is null, the * default [Drivers] section is used. * * @parm LPSTR | lpstrTail | caller supplied buffer to return the "tail" * of the system.ini line in. The tail is any characters that follow * the filename. * * @parm UINT | cbTail | size of supplied buffer as a character count. * * @parm BOOL | fSendEnable | TRUE if driver should be enabled * * @rdesc Returns a long whose loword is the handle to the driver and whose * high word is an error code or the module handle * * @xref InternalOpenDriver * ****************************************************************************/ LONG FAR PASCAL InternalLoadDriver(LPCWSTR szDriverName, LPCWSTR szSectionName, LPWSTR lpstrTail, UINT cbTail, BOOL fSendEnable) { int index; LPDRIVERTABLE lpdt; LONG result; HANDLE h; DRIVERPROC lpDriverEntryPoint; /* The driver will receive the following message sequence: * * if driver not loaded and can be found * DRV_LOAD * if DRV_LOAD returns non-zero and fSendEnable * DRV_ENABLE */ /* Allocate a table entry */ // This can be made more efficient by keeping a count of how many drivers // we have loaded and how many entries there are in the table. Then when // we should reuse an entry we would not reallocate - unlike at present. DrvEnter(); if (!hInstalledDriverList) { h = GlobalAlloc(GHND, (DWORD)((UINT)sizeof(DRIVERTABLE))); // Note: it is valid to assume that the memory has been ZERO'ed // ...might want to add a debug WinAssert to verify... } else { /* Alloc space for the next driver we will install. We may not really * install the driver in the last entry but rather in an intermediate * entry which was freed. */ h = GlobalReAlloc(hInstalledDriverList, (DWORD)((UINT)sizeof(DRIVERTABLE)*(cInstalledDrivers+1)), GHND); // Note: it is valid to assume that the new memory has been ZERO'ed // ...might want to add a debug WinAssert to verify... } if (!h) { dprintf1(("Failed to allocate space for Installed driver list")); DrvLeave(); return(0L); } cInstalledDrivers++; hInstalledDriverList = h; lpdt = (LPDRIVERTABLE)GlobalLock(hInstalledDriverList); /* find an unused entry in the table */ for (index=0;index cInstalledDrivers || !hDriver) { DrvLeave(); LeaveCriticalSection(&DriverLoadFreeCritSec); return(0); } lpdt = (LPDRIVERTABLE)GlobalLock(hInstalledDriverList); /* * If the driver usage count is 1, then send * free and disable messages. */ /* Clear dwDriverIdentifier so that the sendmessage for DRV_OPEN and DRV_ENABLE have dwDriverIdentifier = 0 if an entry gets reused and so that the DRV_DISABLE and DRV_FREE messages below also get dwDriverIdentifier = 0. */ lpdt[hDriver-1].dwDriverIdentifier = 0; hModule = (HMODULE)lpdt[hDriver-1].hModule; GlobalUnlock(hInstalledDriverList); DrvLeave(); w = GetDrvrUsage((HANDLE)hModule); if (w == 1) { if (fSendDisable) DrvSendMessage((HANDLE)hDriver, DRV_DISABLE, 0L, 0L); DrvSendMessage((HANDLE)hDriver, DRV_FREE, 0L, 0L); } FreeLibrary(hModule); DrvEnter(); lpdt = (LPDRIVERTABLE)GlobalLock(hInstalledDriverList); /* Only one entry for the driver in the driver list has the first * instance flag set. This is to make it easier to handle system * messages that only need to be sent to a driver once. * * To maintain the flag, we must set the flag in one of the other * entries if we remove the driver entry with the flag set. * * Note that InternalFreeDriver returns the new usage count of * the driver so if it is zero, we know that there are no other * entries for the driver in the list and so we don't have to * do this loop. */ if (lpdt[hDriver - 1].fFirstEntry) { for (index=0;index>8) | (w<<8); } #endif #if 0 void NEAR PASCAL DrvInit(void) { HANDLE hlibUser; LPDRIVERTABLE lpdt; /* If the window's driver interface is present then use it. */ DOUT(("DrvInit\r\n")); hlibUser = GetModuleHandle(szUser); if(lpOpenDriver = (OPENDRIVER31)GetProcAddress(hlibUser,szOpenDriver)) fUseWinAPI = TRUE; else { fUseWinAPI = FALSE; DOUT((" - No Windows Driver I/F detected. Using MMSYSTEM\r\n")); // // force MMSYSTEM into the driver table, without enabling it. // DrvEnter(); cInstalledDrivers = 0; hInstalledDriverList = GlobalAlloc(GHND|GMEM_SHARE, (DWORD)((UINT)sizeof(DRIVERTABLE))); #if DBG if (hInstalledDriverList == NULL) { DOUT(("no memory for driver table\r\n")); // FatalExit(-1); return; } #endif lpdt = (LPDRIVERTABLE)GlobalLock(hInstalledDriverList); // // NOTE! we are not setting fFirstEntry==TRUE // // because under windows 3.0 MMSOUND will enable/disable us // we *dont* wan't the driver interface doing it! // lpdt->lpDriverEntryPoint = (DRIVERPROC)DriverProc; lpdt->hModule = ghInst; lpdt->fFirstEntry = 0; GlobalUnlock(hInstalledDriverList); DrvLeave(); } if (fUseWinAPI) { DOUT((" - Windows Driver I/F detected\r\n")); if (GetWinVer() < 0x30A) DOUT(("MMSYSTEM: WARNING !!! WINDOWS DRIVER I/F BUT VERSION LESS THAN 3.1\r\n")); // link to the relevant user APIs. lpCloseDriver = (CLOSEDRIVER31)GetProcAddress(hlibUser, szCloseDriver); lpGetDriverModuleHandle = (GETDRIVERMODULEHANDLE31)GetProcAddress(hlibUser, szDrvModuleHandle); lpSendDriverMessage = (SENDDRIVERMESSAGE31)GetProcAddress(hlibUser, szSendDriverMessage); lpDefDriverProc = (DEFDRIVERPROC31)GetProcAddress(hlibUser, szDefDriverProc); } } #endif #if 0 /*************************************************************************** * * @doc INTERNAL * * @api void | InternalInstallDriverChain | This function loads the * drivers specified on the Drivers= line of the [Boot] section * of system.ini. The Drivers are loaded but not opened. * * @rdesc None * ***************************************************************************/ void FAR PASCAL InternalInstallDriverChain(void) { char szBuffer[150]; BOOL bFinished; int iStart; int iEnd; if (!fUseWinAPI) { /* Load DLL's from DRIVERS section in system.ini */ szBuffer[0] = TEXT('\0'); winmmGetPrivateProfileString(szBoot, /* [Boot] section */ szDrivers, /* Drivers= */ szNull, /* Default if no match */ szBuffer, /* Return buffer */ sizeof(szBuffer), szSystemIni); if (!*szBuffer) { return; } bFinished = FALSE; iStart = 0; while (!bFinished) { iEnd = iStart; while (szBuffer[iEnd] && (szBuffer[iEnd] != ' ') && (szBuffer[iEnd] != ',')) iEnd++; if (szBuffer[iEnd] == NULL) bFinished = TRUE; else szBuffer[iEnd] = NULL; /* Load and enable the driver. */ InternalLoadDriver(&(szBuffer[iStart]), NULL, NULL, 0, TRUE); iStart = iEnd+1; } } } #endif /*************************************************************************** * * @doc INTERNAL * * @api void | InternalDriverEnable | This function enables all the * currently loaded installable drivers. If the user driver i/f * has been detected, this function will do nothing. * * @rdesc None * ***************************************************************************/ void FAR PASCAL InternalDriverEnable(void) { if (!fUseWinAPI) InternalBroadcastDriverMessage(1, DRV_ENABLE, 0L, 0L, IBDM_ONEINSTANCEONLY); } /*************************************************************************** * * @doc INTERNAL * * @api void | InternalDriverDisable | This function disables all the * currently loaded installable drivers. If the user driver I/F * has been detected, this function will do nothing. * * * @rdesc None * ***************************************************************************/ void FAR PASCAL InternalDriverDisable(void) { if (!fUseWinAPI) InternalBroadcastDriverMessage(0, DRV_DISABLE, 0L, 0L, IBDM_ONEINSTANCEONLY | IBDM_REVERSE); } /*************************************************************************** * * @doc INTERNAL * * @api BOOL | TestExeFormat | This function tests if the executable * supplied is loadable as a 32-bit executable * * @parm LPWSTR | szExe | The file to test * * @rdesc BOOL | TRUE if format was OK, FALSE otherwise * ***************************************************************************/ BOOL TestExeFormat(LPWSTR szExe) { HANDLE SectionHandle; HANDLE FileHandle; PVOID BaseAddress; ULONG ViewSize; WCHAR ExpandedName[MAX_PATH]; LPWSTR FilePart; // // See if it's already loaded // if (GetModuleHandleW(szExe)) { return TRUE; } // // Search for our DLL // if (!SearchPathW(NULL, szExe, NULL, MAX_PATH, ExpandedName, &FilePart)) { return FALSE; } // // Get a handle for it // FileHandle = CreateFileW(ExpandedName, GENERIC_READ, FILE_SHARE_READ, NULL, OPEN_EXISTING, 0, NULL); if (FileHandle == INVALID_HANDLE_VALUE) { return FALSE; } // // We create and map a section for this file as an IMAGE // to make sure it's recognized as such // if (!NT_SUCCESS(NtCreateSection( &SectionHandle, SECTION_ALL_ACCESS, NULL, NULL, PAGE_READONLY, SEC_IMAGE, FileHandle))) { CloseHandle(FileHandle); return FALSE; } // // Map it whereever it will go // ViewSize = 0; BaseAddress = NULL; // // See if the loader is happy with the format // if (!NT_SUCCESS(NtMapViewOfSection(SectionHandle, NtCurrentProcess(), &BaseAddress, 0L, 0L, NULL, &ViewSize, ViewShare, 0L, PAGE_READONLY))) { NtClose(SectionHandle); CloseHandle(FileHandle); return FALSE; } NtUnmapViewOfSection(NtCurrentProcess(), BaseAddress); NtClose(SectionHandle); CloseHandle(FileHandle); return TRUE; } /*************************************************************************** * * @doc INTERNAL * * @api HANDLE | LoadAliasedLibrary | This function loads the library module * contained in the specified file and returns its module handle * unless the specified name matches a keyname in the * specified section section of the specified ini file in which case * the library module in the file specified on the ini line is loaded. * * @parm LPSTR | szLibFileName | points to a null-terminated character * string containing the filename or system.ini keyname. * * @parm LPSTR | szSection | points to a null-terminated character * string containing the section name. * * @parm LPSTR | szIniFile | points to a null-terminated character * string containing the ini filename. * * @parm LPSTR | lpstrTail | caller supplied buffer to return the "tail" * of the system.ini line in. The tail is any characters that follow * the filename. * * @parm UINT | cbTail | size of supplied buffer. * * @rdesc Returns the library's module handle. * * @xref LoadLibrary * ***************************************************************************/ HANDLE LoadAliasedLibrary( LPCWSTR szLibFileName, LPCWSTR szSection, LPWSTR szIniFile, LPWSTR lpstrTail, UINT cbTail) { #define SZ_SIZE 128 #define SZ_SIZE_BYTES (SZ_SIZE * sizeof( WCHAR )) WCHAR sz[SZ_SIZE]; LPWSTR pch; HANDLE hReturn; DWORD OldErrorMode; // OFSTRUCT of; if (!szLibFileName || !*szLibFileName) return(NULL); // File not found // read the filename and additional info. into sz sz[0] = L'\0'; winmmGetPrivateProfileString(szSection, // ini section szLibFileName, // key name szLibFileName, // default if no match sz, // return buffer SZ_SIZE, // sizeof of return buffer szIniFile); // ini. file sz[SZ_SIZE - 1] = 0; // // strip off the additional info. // pch = (LPWSTR)sz; // // at exit from loop pch pts to ch after first space or null ch // while (*pch) { if ( *pch == ' ' ) { *pch++ = '\0'; break; } pch++; } // // These lines are removed for unicode because: // there is not a unicode version of OpenFile. // LoadLibrary performs the same test as the one below anyway // // if (!GetModuleHandle( sz ) && // OpenFile(sz, &of, OF_EXIST|OF_READ|OF_SHARE_DENY_NONE) == -1) { // // return(NULL); // } // // copy additional info. to lpstrTail // if (lpstrTail && cbTail) { while (cbTail-- && (0 != (*lpstrTail++ = *pch++))) ; *(lpstrTail-1) = 0; } // // If we're running in the server check if it's a good image. // The server bug checks if it tries to load bad images (LoadLibrary // inconsistency). // // To do this we simulate the load process far enough to make // the check that it's a valid image // if (WinmmRunningInServer && !TestExeFormat(sz)) { return NULL; } // // Disable hard error popups // OldErrorMode = SetErrorMode(SEM_FAILCRITICALERRORS); // // Try to load it // hReturn = LoadLibraryW( sz ); SetErrorMode(OldErrorMode); return hReturn; #undef SZ_SIZE_BYTES #undef SZ_SIZE } /*************************************************************************** * * @doc INTERNAL * * @api int | GetDrvrUsage | Runs through the driver list and figures * out how many instances of this driver module handle we have. * We use this instead of GetModuleUsage so that we can have drivers * loaded as normal DLLs and as installable drivers. * * @parm HANDLE | h | Driver's module handle * * @rdesc Returns the library's driver usage count. * ***************************************************************************/ int FAR PASCAL GetDrvrUsage(HANDLE h) { LPDRIVERTABLE lpdt; int index; int count; DrvEnter(); if (!hInstalledDriverList || !cInstalledDrivers) { DrvLeave(); return(0); } count = 0; lpdt = (LPDRIVERTABLE)GlobalLock(hInstalledDriverList); for (index=0;indexhModule==(UINT)h) { count++; } lpdt++; } GlobalUnlock(hInstalledDriverList); DrvLeave(); return(count); }