539 lines
15 KiB
C
539 lines
15 KiB
C
|
/*****************************************************************************
|
||
|
*
|
||
|
* DIApHack.c
|
||
|
*
|
||
|
* Copyright (c) 1999 Microsoft Corporation. All Rights Reserved.
|
||
|
*
|
||
|
* Abstract:
|
||
|
*
|
||
|
* Support routines for app hacks
|
||
|
*
|
||
|
* Contents:
|
||
|
*
|
||
|
*****************************************************************************/
|
||
|
|
||
|
#include "dinputpr.h"
|
||
|
|
||
|
/*****************************************************************************
|
||
|
*
|
||
|
* The sqiffle for this file.
|
||
|
*
|
||
|
*****************************************************************************/
|
||
|
//ISSUE-2001/03/29-timgill Need to sort out a prefixed version of of SquirtSqflPtszV
|
||
|
TCHAR c_tszPrefix[]=TEXT("DINPUT: ");
|
||
|
|
||
|
#define sqfl sqflCompat
|
||
|
|
||
|
typedef enum
|
||
|
{
|
||
|
DICOMPATID_REACQUIRE, // Perform auto reaquire if device lost
|
||
|
DICOMPATID_NOSUBCLASS, // Do not use subclassing
|
||
|
DICOMPATID_MAXDEVICENAMELENGTH, // Truncate device names
|
||
|
DICOMPATID_NATIVEAXISONLY, // Always report axis data in native mode
|
||
|
DICOMPATID_NOPOLLUNACQUIRE, // Don't unaquire the device if a poll fails
|
||
|
DICOMPATID_SUCCEEDACQUIRE // Always return a success code for calls to Acquire()
|
||
|
} DIAPPHACKID, *LPDIAPPHACKID;
|
||
|
|
||
|
typedef struct tagAPPHACKENTRY
|
||
|
{
|
||
|
LPCTSTR pszName;
|
||
|
DWORD cbData;
|
||
|
DWORD dwOSMask;
|
||
|
} APPHACKENTRY, *LPAPPHACKENTRY;
|
||
|
|
||
|
typedef struct tagAPPHACKTABLE
|
||
|
{
|
||
|
LPAPPHACKENTRY aEntries;
|
||
|
ULONG cEntries;
|
||
|
} APPHACKTABLE, *LPAPPHACKTABLE;
|
||
|
|
||
|
#define BEGIN_DECLARE_APPHACK_ENTRIES(name) \
|
||
|
APPHACKENTRY name[] = {
|
||
|
|
||
|
#define DECLARE_APPHACK_ENTRY(name, type, osmask) \
|
||
|
{ TEXT(#name), sizeof(type), osmask },
|
||
|
|
||
|
#define END_DECLARE_APPHACK_ENTRIES() \
|
||
|
};
|
||
|
|
||
|
#define BEGIN_DECLARE_APPHACK_TABLE(name) \
|
||
|
APPHACKTABLE name =
|
||
|
|
||
|
#define DECLARE_APPHACK_TABLE(entries) \
|
||
|
{ entries, cA(entries) }
|
||
|
|
||
|
#define END_DECLARE_APPHACK_TABLE() \
|
||
|
;
|
||
|
|
||
|
#define DIHACKOS_WIN2K (0x00000001L)
|
||
|
#define DIHACKOS_WIN9X (0x00000002L)
|
||
|
|
||
|
BEGIN_DECLARE_APPHACK_ENTRIES(g_aheAppHackEntries)
|
||
|
DECLARE_APPHACK_ENTRY(ReAcquire, BOOL, DIHACKOS_WIN2K )
|
||
|
DECLARE_APPHACK_ENTRY(NoSubClass, BOOL, DIHACKOS_WIN2K )
|
||
|
DECLARE_APPHACK_ENTRY(MaxDeviceNameLength, DWORD, DIHACKOS_WIN2K | DIHACKOS_WIN9X )
|
||
|
DECLARE_APPHACK_ENTRY(NativeAxisOnly, BOOL, DIHACKOS_WIN2K | DIHACKOS_WIN9X )
|
||
|
DECLARE_APPHACK_ENTRY(NoPollUnacquire, BOOL, DIHACKOS_WIN2K | DIHACKOS_WIN9X )
|
||
|
DECLARE_APPHACK_ENTRY(SucceedAcquire, BOOL, DIHACKOS_WIN2K )
|
||
|
END_DECLARE_APPHACK_ENTRIES()
|
||
|
|
||
|
BEGIN_DECLARE_APPHACK_TABLE(g_ahtAppHackTable)
|
||
|
DECLARE_APPHACK_TABLE(g_aheAppHackEntries)
|
||
|
END_DECLARE_APPHACK_TABLE()
|
||
|
|
||
|
|
||
|
/***************************************************************************
|
||
|
*
|
||
|
* AhGetOSMask
|
||
|
*
|
||
|
* Description:
|
||
|
* Gets the mask for the current OS
|
||
|
* This mask should be used when we get app hacks for more than just
|
||
|
* Win2k such that hacks can be applied selectively per OS.
|
||
|
* For now just #define a value as constant.
|
||
|
*
|
||
|
* Arguments:
|
||
|
* none
|
||
|
*
|
||
|
* Returns:
|
||
|
* DWORD: Mask of flags applicable for the current OS.
|
||
|
*
|
||
|
***************************************************************************/
|
||
|
|
||
|
#ifdef WINNT
|
||
|
#define AhGetOSMask() DIHACKOS_WIN2K
|
||
|
#else
|
||
|
#define AhGetOSMask() DIHACKOS_WIN9X
|
||
|
#endif
|
||
|
|
||
|
/***************************************************************************
|
||
|
*
|
||
|
* AhGetCurrentApplicationPath
|
||
|
*
|
||
|
* Description:
|
||
|
* Gets the full path to the current application's executable.
|
||
|
*
|
||
|
* Arguments:
|
||
|
* LPTSTR [out]: receives application id. This buffer is assumed to be
|
||
|
* at least MAX_PATH characters in size.
|
||
|
* LPTSTR * [out]: receives pointer to executable part of the path.
|
||
|
*
|
||
|
* Returns:
|
||
|
* BOOL: TRUE on success.
|
||
|
*
|
||
|
***************************************************************************/
|
||
|
|
||
|
BOOL AhGetCurrentApplicationPath
|
||
|
(
|
||
|
LPTSTR pszPath,
|
||
|
LPTSTR * ppszModule
|
||
|
)
|
||
|
{
|
||
|
BOOL fSuccess = TRUE;
|
||
|
TCHAR szOriginal[MAX_PATH];
|
||
|
|
||
|
EnterProcI(AhGetCurrentApplicationPath, (_ ""));
|
||
|
|
||
|
fSuccess = GetModuleFileName(GetModuleHandle(NULL), szOriginal, cA(szOriginal));
|
||
|
|
||
|
if(fSuccess)
|
||
|
{
|
||
|
fSuccess = ( GetFullPathName(szOriginal, MAX_PATH, pszPath, ppszModule) != 0 );
|
||
|
}
|
||
|
|
||
|
ExitProcF(fSuccess);
|
||
|
|
||
|
return fSuccess;
|
||
|
}
|
||
|
|
||
|
|
||
|
/***************************************************************************
|
||
|
*
|
||
|
* AhGetApplicationId
|
||
|
*
|
||
|
* Description:
|
||
|
* Gets the id used to identify the current application.
|
||
|
*
|
||
|
* Arguments:
|
||
|
* LPTSTR [out]: receives application id.
|
||
|
*
|
||
|
* Arguments:
|
||
|
* LPTSTR [out optional]: receives application name.
|
||
|
*
|
||
|
* Returns:
|
||
|
* BOOL: TRUE on success.
|
||
|
*
|
||
|
***************************************************************************/
|
||
|
|
||
|
BOOL AhGetApplicationId
|
||
|
(
|
||
|
LPTSTR pszAppId,
|
||
|
LPTSTR pszAppName
|
||
|
)
|
||
|
{
|
||
|
HANDLE hFile = NULL;
|
||
|
TCHAR szExecutable[MAX_PATH];
|
||
|
LPTSTR pszModule;
|
||
|
IMAGE_NT_HEADERS nth;
|
||
|
IMAGE_DOS_HEADER dh;
|
||
|
DWORD cbRead;
|
||
|
DWORD dwFileSize;
|
||
|
BOOL fSuccess;
|
||
|
|
||
|
EnterProcI(AhGetApplicationId, (_ ""));
|
||
|
|
||
|
AssertF( pszAppId );
|
||
|
|
||
|
// Get the application path
|
||
|
fSuccess = AhGetCurrentApplicationPath(szExecutable, &pszModule);
|
||
|
|
||
|
if(fSuccess)
|
||
|
{
|
||
|
SquirtSqflPtszV(sqfl | sqflVerbose, TEXT("%sApplication executable path: %s"), c_tszPrefix, szExecutable);
|
||
|
SquirtSqflPtszV(sqfl | sqflVerbose, TEXT("%sApplication module: %s"), c_tszPrefix, pszModule);
|
||
|
}
|
||
|
|
||
|
// Open the executable
|
||
|
if(fSuccess)
|
||
|
{
|
||
|
hFile = CreateFile(szExecutable, GENERIC_READ, FILE_SHARE_READ, NULL, OPEN_EXISTING, 0, NULL);
|
||
|
|
||
|
if(!(( hFile ) && ( hFile != INVALID_HANDLE_VALUE )))
|
||
|
{
|
||
|
SquirtSqflPtszV(sqfl | sqflError, TEXT("%sCreateFile failed to open %s with error %lu"), c_tszPrefix,
|
||
|
szExecutable, GetLastError());
|
||
|
fSuccess = FALSE;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// Read the executable's DOS header
|
||
|
if(fSuccess)
|
||
|
{
|
||
|
fSuccess = ReadFile(hFile, &dh, sizeof(dh), &cbRead, NULL);
|
||
|
|
||
|
if(!fSuccess || sizeof(dh) != cbRead)
|
||
|
{
|
||
|
SquirtSqflPtszV(sqfl | sqflError, TEXT("%sUnable to read DOS header"), c_tszPrefix);
|
||
|
fSuccess = FALSE;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
if(fSuccess && IMAGE_DOS_SIGNATURE != dh.e_magic)
|
||
|
{
|
||
|
SquirtSqflPtszV(sqfl | sqflError, TEXT("%sInvalid DOS signature"), c_tszPrefix);
|
||
|
fSuccess = FALSE;
|
||
|
}
|
||
|
|
||
|
// Read the executable's PE header
|
||
|
if(fSuccess)
|
||
|
{
|
||
|
cbRead = SetFilePointer(hFile, dh.e_lfanew, NULL, FILE_BEGIN);
|
||
|
|
||
|
if((LONG)cbRead != dh.e_lfanew)
|
||
|
{
|
||
|
SquirtSqflPtszV(sqfl | sqflError, TEXT("%sUnable to seek to PE header"), c_tszPrefix);
|
||
|
fSuccess = FALSE;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
if(fSuccess)
|
||
|
{
|
||
|
fSuccess = ReadFile(hFile, &nth, sizeof(nth), &cbRead, NULL);
|
||
|
|
||
|
if(!fSuccess || sizeof(nth) != cbRead)
|
||
|
{
|
||
|
SquirtSqflPtszV(sqfl | sqflError, TEXT("%sUnable to read PE header"), c_tszPrefix);
|
||
|
fSuccess = FALSE;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
if(fSuccess && IMAGE_NT_SIGNATURE != nth.Signature)
|
||
|
{
|
||
|
SquirtSqflPtszV(sqfl | sqflError, TEXT("%sInvalid PE signature"), c_tszPrefix);
|
||
|
fSuccess = FALSE;
|
||
|
}
|
||
|
|
||
|
// Get the executable's size
|
||
|
if(fSuccess)
|
||
|
{
|
||
|
// Assuming < 4 GB
|
||
|
dwFileSize = GetFileSize(hFile, NULL);
|
||
|
|
||
|
if((DWORD)(-1) == dwFileSize)
|
||
|
{
|
||
|
SquirtSqflPtszV(sqfl | sqflError, TEXT("%sUnable to get file size"), c_tszPrefix);
|
||
|
fSuccess = FALSE;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// Create the application id
|
||
|
if(fSuccess)
|
||
|
{
|
||
|
CharUpper(pszModule);
|
||
|
wsprintf(pszAppId, TEXT("%s%8.8lX%8.8lX"), pszModule, nth.FileHeader.TimeDateStamp, dwFileSize);
|
||
|
|
||
|
if( pszAppName )
|
||
|
{
|
||
|
lstrcpy(pszAppName, pszModule);
|
||
|
}
|
||
|
|
||
|
SquirtSqflPtszV(sqfl | sqflTrace, TEXT("%sApplication id: %s"), c_tszPrefix, pszAppId);
|
||
|
}
|
||
|
|
||
|
// Clean up
|
||
|
if( hFile != NULL )
|
||
|
{
|
||
|
CloseHandle( hFile );
|
||
|
}
|
||
|
|
||
|
ExitProcF(fSuccess);
|
||
|
|
||
|
return fSuccess;
|
||
|
}
|
||
|
|
||
|
|
||
|
/***************************************************************************
|
||
|
*
|
||
|
* AhOpenApplicationKey
|
||
|
*
|
||
|
* Description:
|
||
|
* Opens or creates the application's root key.
|
||
|
*
|
||
|
* Arguments:
|
||
|
* LPCTSTR [in]: application id.
|
||
|
*
|
||
|
* Returns:
|
||
|
* HKEY: registry key handle.
|
||
|
*
|
||
|
***************************************************************************/
|
||
|
|
||
|
HKEY AhOpenApplicationKey
|
||
|
(
|
||
|
LPCTSTR pszAppId
|
||
|
)
|
||
|
{
|
||
|
|
||
|
#ifdef DEBUG
|
||
|
|
||
|
TCHAR szName[0x100] = { 0 };
|
||
|
LONG cbName = sizeof(szName);
|
||
|
|
||
|
#endif // DEBUG
|
||
|
|
||
|
HKEY hkeyAll = NULL;
|
||
|
HKEY hkeyApp = NULL;
|
||
|
HRESULT hr;
|
||
|
|
||
|
EnterProcI(AhOpenApplicationKey, (_ ""));
|
||
|
|
||
|
// Open the parent key
|
||
|
hr = hresMumbleKeyEx( HKEY_LOCAL_MACHINE,
|
||
|
REGSTR_PATH_DINPUT TEXT("\\") REGSTR_KEY_APPHACK, KEY_READ, 0, &hkeyAll );
|
||
|
|
||
|
if(SUCCEEDED(hr))
|
||
|
{
|
||
|
hr = hresMumbleKeyEx( hkeyAll, pszAppId, KEY_READ, 0, &hkeyApp );
|
||
|
|
||
|
RegCloseKey( hkeyAll );
|
||
|
#ifdef DEBUG
|
||
|
|
||
|
// Query for the application description
|
||
|
if(SUCCEEDED(hr))
|
||
|
{
|
||
|
JoyReg_GetValue( hkeyApp, NULL, REG_SZ, szName, cbName );
|
||
|
SquirtSqflPtszV(sqfl | sqflTrace,
|
||
|
TEXT( "%sApplication description: %ls"), c_tszPrefix, szName );
|
||
|
}
|
||
|
|
||
|
#endif // DEBUG
|
||
|
}
|
||
|
|
||
|
ExitProc();
|
||
|
|
||
|
return hkeyApp;
|
||
|
}
|
||
|
|
||
|
|
||
|
/***************************************************************************
|
||
|
*
|
||
|
* AhGetHackValue
|
||
|
*
|
||
|
* Description:
|
||
|
* Queries an apphack value.
|
||
|
*
|
||
|
* Arguments:
|
||
|
* HKEY [in]: application registry key.
|
||
|
* DSAPPHACKID [in]: apphack id.
|
||
|
* LPVOID [out]: receives apphack data.
|
||
|
* DWORD [in]: size of above data buffer.
|
||
|
*
|
||
|
* Returns:
|
||
|
* BOOL: TRUE on success.
|
||
|
*
|
||
|
***************************************************************************/
|
||
|
|
||
|
BOOL AhGetHackValue
|
||
|
(
|
||
|
HKEY hkey,
|
||
|
DWORD dwOSMask,
|
||
|
DIAPPHACKID ahid,
|
||
|
LPVOID pvData,
|
||
|
DWORD cbData
|
||
|
)
|
||
|
{
|
||
|
HRESULT hr;
|
||
|
|
||
|
EnterProcI(AhGetHackValue, (_ ""));
|
||
|
|
||
|
AssertF(ahid < (DIAPPHACKID)g_ahtAppHackTable.cEntries);
|
||
|
AssertF(cbData == g_ahtAppHackTable.aEntries[ahid].cbData);
|
||
|
|
||
|
if( !( dwOSMask & g_ahtAppHackTable.aEntries[ahid].dwOSMask ) )
|
||
|
{
|
||
|
hr = DI_OK;
|
||
|
}
|
||
|
else
|
||
|
{
|
||
|
hr = JoyReg_GetValue( hkey, g_ahtAppHackTable.aEntries[ahid].pszName,
|
||
|
REG_BINARY, pvData, cbData );
|
||
|
if( !SUCCEEDED( hr ) )
|
||
|
{
|
||
|
SquirtSqflPtszV(sqfl | sqflBenign,
|
||
|
TEXT("%sfailed to read value \"%s\", code 0x%08x"),
|
||
|
c_tszPrefix, g_ahtAppHackTable.aEntries[ahid].pszName, hr);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
ExitProcF(DI_OK == hr);
|
||
|
|
||
|
return DI_OK == hr;
|
||
|
}
|
||
|
|
||
|
|
||
|
/***************************************************************************
|
||
|
*
|
||
|
* AhGetAppHacks
|
||
|
*
|
||
|
* Description:
|
||
|
* Gets all app-hacks for the current application.
|
||
|
*
|
||
|
* Arguments:
|
||
|
* LPDSAPPHACKS [out]: receives app-hack data.
|
||
|
*
|
||
|
* Returns:
|
||
|
* BOOL: TRUE if any apphacks exist for the current application.
|
||
|
*
|
||
|
***************************************************************************/
|
||
|
|
||
|
BOOL AhGetAppHacks
|
||
|
(
|
||
|
LPDIAPPHACKS pahAppHacks
|
||
|
)
|
||
|
{
|
||
|
static const DIAPPHACKS ahDefaults = { FALSE, FALSE, FALSE, FALSE, FALSE, MAX_PATH };
|
||
|
TCHAR szAppId[MAX_PATH + 8 + 8] = { 0 };
|
||
|
HKEY hkey = NULL;
|
||
|
BOOL fSuccess;
|
||
|
DWORD dwOSMask;
|
||
|
|
||
|
EnterProcI(AhGetAppHacks, (_ ""));
|
||
|
|
||
|
// Assume defaults
|
||
|
CopyMemory(pahAppHacks, &ahDefaults, sizeof(ahDefaults));
|
||
|
|
||
|
// Get the OS version mask
|
||
|
dwOSMask = AhGetOSMask();
|
||
|
|
||
|
// Get the application id
|
||
|
fSuccess = AhGetApplicationId(szAppId, NULL);
|
||
|
|
||
|
if(fSuccess)
|
||
|
{
|
||
|
SquirtSqflPtszV(sqfl | sqflTrace, TEXT("%sFinding apphacks for %s..."), c_tszPrefix, szAppId);
|
||
|
}
|
||
|
|
||
|
// Open the application key
|
||
|
if(fSuccess)
|
||
|
{
|
||
|
hkey = AhOpenApplicationKey(szAppId);
|
||
|
fSuccess = ( hkey && (hkey != INVALID_HANDLE_VALUE ) );
|
||
|
}
|
||
|
|
||
|
#define GET_APP_HACK( hackid, field ) \
|
||
|
if( !AhGetHackValue( hkey, dwOSMask, hackid, &pahAppHacks->##field, sizeof(pahAppHacks->##field) ) ) \
|
||
|
{ \
|
||
|
pahAppHacks->##field = ahDefaults.##field; \
|
||
|
}
|
||
|
|
||
|
// Query all apphack values
|
||
|
if(fSuccess)
|
||
|
{
|
||
|
GET_APP_HACK( DICOMPATID_REACQUIRE, fReacquire );
|
||
|
GET_APP_HACK( DICOMPATID_NOSUBCLASS, fNoSubClass );
|
||
|
GET_APP_HACK( DICOMPATID_MAXDEVICENAMELENGTH, nMaxDeviceNameLength );
|
||
|
GET_APP_HACK( DICOMPATID_NATIVEAXISONLY, fNativeAxisOnly );
|
||
|
GET_APP_HACK( DICOMPATID_NOPOLLUNACQUIRE, fNoPollUnacquire );
|
||
|
GET_APP_HACK( DICOMPATID_SUCCEEDACQUIRE, fSucceedAcquire );
|
||
|
}
|
||
|
|
||
|
#undef GET_APP_HACK
|
||
|
|
||
|
if(fSuccess)
|
||
|
{
|
||
|
SquirtSqflPtszV(sqfl | sqflTrace, TEXT("%sfReacquire: %d"), c_tszPrefix, pahAppHacks->fReacquire );
|
||
|
SquirtSqflPtszV(sqfl | sqflTrace, TEXT("%sfNoSubClass: %d"), c_tszPrefix, pahAppHacks->fNoSubClass );
|
||
|
SquirtSqflPtszV(sqfl | sqflTrace, TEXT("%snMaxDeviceNameLength: %d"), c_tszPrefix, pahAppHacks->nMaxDeviceNameLength );
|
||
|
SquirtSqflPtszV(sqfl | sqflTrace, TEXT("%sfNativeAxisOnly: %d"), c_tszPrefix, pahAppHacks->fNativeAxisOnly );
|
||
|
SquirtSqflPtszV(sqfl | sqflTrace, TEXT("%sfNoPollUnacquire: %d"), c_tszPrefix, pahAppHacks->fNoPollUnacquire );
|
||
|
SquirtSqflPtszV(sqfl | sqflTrace, TEXT("%sfSucceedAcquire: %d"), c_tszPrefix, pahAppHacks->fSucceedAcquire );
|
||
|
}
|
||
|
else
|
||
|
{
|
||
|
SquirtSqflPtszV(sqfl | sqflTrace, TEXT("%sNo apphacks exist"), c_tszPrefix);
|
||
|
}
|
||
|
|
||
|
// Clean up
|
||
|
if( hkey )
|
||
|
{
|
||
|
RegCloseKey(hkey);
|
||
|
}
|
||
|
|
||
|
ExitProc();
|
||
|
|
||
|
return fSuccess;
|
||
|
}
|
||
|
|
||
|
|
||
|
|
||
|
HRESULT EXTERNAL AhAppRegister(DWORD dwVer)
|
||
|
{
|
||
|
TCHAR szAppName[MAX_PATH];
|
||
|
TCHAR szAppId[MAX_PATH + 8 + 8] = { 0 };
|
||
|
|
||
|
BOOL fSuccess;
|
||
|
HRESULT hr = E_FAIL;
|
||
|
|
||
|
fSuccess = AhGetApplicationId(szAppId, szAppName);
|
||
|
|
||
|
if (fSuccess)
|
||
|
{
|
||
|
HKEY hKey;
|
||
|
|
||
|
hr = hresMumbleKeyEx( HKEY_CURRENT_USER,
|
||
|
REGSTR_PATH_LASTAPP, KEY_WRITE, 0, &hKey );
|
||
|
|
||
|
if( SUCCEEDED(hr) )
|
||
|
{
|
||
|
FILETIME ftSysTime;
|
||
|
GetSystemTimeAsFileTime( &ftSysTime );
|
||
|
RegSetValueEx(hKey, DIRECTINPUT_REGSTR_VAL_VERSION, 0x0, REG_BINARY, (PUCHAR) &dwVer, cbX(dwVer) );
|
||
|
RegSetValueEx(hKey, DIRECTINPUT_REGSTR_VAL_NAME, 0x0, REG_SZ, (PUCHAR) szAppName, cbCtch(lstrlen(szAppName)+1) );
|
||
|
RegSetValueEx(hKey, DIRECTINPUT_REGSTR_VAL_ID, 0x0, REG_SZ, (PUCHAR) szAppId, cbCtch(lstrlen(szAppId)+1) );
|
||
|
RegSetValueEx(hKey, DIRECTINPUT_REGSTR_VAL_LASTSTART, 0x0, REG_BINARY, (PUCHAR)&ftSysTime, cbX(ftSysTime));
|
||
|
RegCloseKey(hKey);
|
||
|
}
|
||
|
}
|
||
|
return hr;
|
||
|
}
|
||
|
|