I have a number of batch files to run that depend on an environment variable. Up to now the setting of the environment variable has been done manually. I wish to automate this by running a VC++ program to compute the setting and update the environment variable.
Problems:
When the VC++ program runs it only gets a copy of the environment. I can successfully set variables but they are lost when the program exits.
I then used the Regxxx functions to write the setting in "HKEY_CURRENT_USER\Environment" followed by
rStat = SendMessageTimeout(HWND_BROADCAST, WM_SETTINGCHANGE, 0,(LPARAM)"Environment",SMTO_ABORTIFHUNG, 5000, &dwReturnVal);
This reports success and the change can be inspected using regedit. However if I launch a new cmd.exe shell the environment has not been updated.
If I inspect using SystemPropertiesAdvanced tool the new values are seen and after exit a new cmd.exe shell sees them.
Microsoft docs say the broadcast message should cause the changes to be recognised but it does not work.
Any suggestion gratefully received.
TCHAR newName[] = _T("cock of the walk");
DWORD newNameSize = sizeof(newName);
HKEY hKey;
LSTATUS stat;
stat = RegCreateKey(HKEY_CURRENT_USER, _T("Environment"), &hKey);
if (stat == ERROR_SUCCESS)
{
TCHAR val[200];
DWORD bufsiz = 100;
stat = RegGetValue(hKey, NULL, _T("donkeykong"), RRF_RT_REG_SZ, NULL, val, &bufsiz);
if ((stat == ERROR_SUCCESS) || (stat = ERROR_FILE_NOT_FOUND))
{
stat = RegSetKeyValue(hKey, NULL, _T("donkeykong"), REG_SZ, newName, newNameSize);
DWORD_PTR dwReturnVal;
LRESULT rStat;
rStat = SendMessageTimeout(HWND_BROADCAST, WM_SETTINGCHANGE, 0, (LPARAM)"Environment",
SMTO_ABORTIFHUNG, 5000, &dwReturnVal);
std::cout << "Bye" << std::endl;
}
stat = RegCloseKey(hKey);
Related
I want to spawn a batch file from my FMX app (on Win32) with elevated privileges. From Remy's answer at the bottom of this thread on ShellExecute I found how to launch the batch file. Now, i can't figure out how to launch it with elevated privilege. Below is my code:
String Prog = "c:\\Users\\rwp\\Desktop\\test.bat";
int nErrorCode = (int) ShellExecute(NULL, L"runas", Prog.c_str(), NULL, NULL, SW_SHOWNORMAL);
if (nErrorCode <= 32) {
ShowMessage("an error occured");
}
I added "runas" for the second argument after reading this to no avail. Running the batch file manually (right-click and run as admin) works. Here is content of the batch file fyi (just kicks of a system imaging):
c:\Windows\system32\wbAdmin.exe start backup -backupTarget:D: -include:C: -allCritical -quiet
How can i ShellExecute this batch file as admin?
UPDATE 1: I'm attempting to use CreateProcess per Remy suggestion. Here is my code (based on this example):
//Code is inside a __fastcall button click
PROCESS_INFORMATION piProcInfo;
STARTUPINFO siStartInfo;
siStartInfo.cb = sizeof(STARTUPINFO);
siStartInfo.lpReserved = NULL;
siStartInfo.lpReserved2 = NULL;
siStartInfo.cbReserved2 = 0;
siStartInfo.lpDesktop = NULL;
siStartInfo.dwFlags = 0;
// String strCmdLine = "C:\\Users\\rwpatter\\Desktop\\test.bat";
String strCmdLine = "C:\\Windows\\System32\\wbAdmin.exe start backup -backupTarget:T: -include:C: -allCritical -quiet";
// Create the child process.
int rtrn = CreateProcess(
NULL,
strCmdLine.c_str(),
NULL, // process security attributes
NULL, // primary thread security attributes
0, // handles are inherited
0, // creation flags
0, // use parent's environment
0, // use parent's current directory
&siStartInfo, // STARTUPINFO pointer
&piProcInfo); // receives PROCESS_INFORMATION
// Wait for the processs to finish
DWORD rc = WaitForSingleObject(
piProcInfo.hProcess, // process handle
INFINITE);
ShowMessage(IntToStr(rtrn));
If I run it as shown (right-click on exe and run as admin) it returns 0 which means it failed. If I run it by putting the wbAdmin command line in the test.bat file (see commented line right above String strCmdLine in the code) then CreateProcess returns a 1 (success) but wbAdmin is still not running. It flashed a DOS window and i captured it as shown in the picture below. It shows oriental characters in the title bar and says not recognized as internal or external command. But, if i run that test.bat directly (elevated) it runs wbAdmin no problem.
Any ideas on what is wrong? Besides me obviously being ignorant. (p.s. i'll get to testing Golvind's answer on the ShellExecute after this...)
Running the batch file manually (right-click and run as admin) works.
Because you are running the 64-bit version of cmd when you start it manually.
It shows oriental characters in the title bar and says not recognized
as internal or external command.
Because your application is 32-bit. A 32-bit application does not see the same System32 folder as 64-bit applications. You can access the 64-bit System32 folder in 32-bit applications with the virtual sysnative folder.
#include <shellapi.h>
...
String strCmdLine = "wbAdmin.exe start backup -backupTarget:T: -include:C: -allCritical -quiet";
int rtrn = CreateProcess(
NULL,
strCmdLine.c_str(),
NULL, // process security attributes
NULL, // primary thread security attributes
0, // handles are inherited
0, // creation flags
0, // use parent's environment
0, // use parent's current directory
&siStartInfo, // STARTUPINFO pointer
&piProcInfo); // receives PROCESS_INFORMATION
if (!rtrn)
{
String newCmdLine = "c:\\windows\\sysnative\\wbAdmin.exe start backup -backupTarget:T: -include:C: -allCritical -quiet";
rtrn = CreateProcess(
NULL,
newCmdLine.c_str(),
NULL, // process security attributes
NULL, // primary thread security attributes
0, // handles are inherited
0, // creation flags
0, // use parent's environment
0, // use parent's current directory
&siStartInfo, // STARTUPINFO pointer
&piProcInfo); // receives PROCESS_INFORMATION
}
Or compile your application to 64-bit.
You need to launch CMD.exe as Administrator with "runas", and specify the batch file as a "run-me-then-exit" (i.e. /c) argument to command prompt, as so:
WCHAR wszCmdPath[MAX_PATH];
GetEnvironmentVariableW(L"ComSpec", wszCmdPath, MAX_PATH);
ShellExecuteW(NULL, L"runas", wszCmdPath, L"/c \"C:\\Path\\BatchFile.bat\"", L"", SW_SHOW);
Both functions called here can fail, and robust code would test for success before proceeding.
I need to launch a process and then break it immediately so that it can be debugged using Visual Studio.
I wrote this code after searching bits and pieces on the internet. The code is not working. The process gets launched, but it does not break and the calling code keeps waiting infinitely. If I don't launch the process in suspended mode, it runs immediately and exits.
I cannot modify the code of .exe that I am launching. I just have the .exe and symbols file.
Code:
#include<iostream>
#include<Windows.h>
using namespace std;
int main(int argc, char **argv)
{
STARTUPINFO si;
PROCESS_INFORMATION pi;
LPWSTR commandLine;
int commandLength;
ZeroMemory(&si, sizeof(si));
si.cb = sizeof(si);
ZeroMemory(&pi, sizeof(pi));
if (argc != 2)
{
cout << "Usage: Launcher <commandline>" << endl;
return 1;
}
commandLength = MultiByteToWideChar(CP_UTF8, 0, argv[1], -1, NULL, 0);
commandLine = new WCHAR[commandLength];
MultiByteToWideChar(CP_UTF8, 0, argv[1], -1, commandLine, commandLength);
if (!CreateProcess(NULL, commandLine, NULL, NULL, FALSE, CREATE_SUSPENDED, NULL, NULL, &si, &pi))
{
cout << "CreateProcess failed (" << GetLastError() << ")." << endl;
delete[] commandLine;
return 1;
}
cout << pi.dwProcessId << " " << pi.dwThreadId << endl;
delete[] commandLine;
DebugBreakProcess(pi.hProcess);
WaitForSingleObject(pi.hProcess, INFINITE);
CloseHandle(pi.hProcess);
CloseHandle(pi.hThread);
return 0;
}
What am I doing wrong here?
EDIT: This is the code after the suggestion by tyson.
#include<iostream>
#include<Windows.h>
using namespace std;
int main(int argc, char **argv)
{
STARTUPINFO si;
PROCESS_INFORMATION pi;
LPWSTR commandLine;
int commandLength;
HANDLE processHandle;
ZeroMemory(&si, sizeof(si));
si.cb = sizeof(si);
ZeroMemory(&pi, sizeof(pi));
if (argc != 2)
{
cout << "Usage: Launcher <commandline>" << endl;
return 1;
}
commandLength = MultiByteToWideChar(CP_UTF8, 0, argv[1], -1, NULL, 0);
commandLine = new WCHAR[commandLength];
MultiByteToWideChar(CP_UTF8, 0, argv[1], -1, commandLine, commandLength);
if (!CreateProcess(NULL, commandLine, NULL, NULL, FALSE, CREATE_SUSPENDED, NULL, NULL, &si, &pi))
{
cout << "CreateProcess failed (" << GetLastError() << ")." << endl;
delete[] commandLine;
return 1;
}
delete[] commandLine;
processHandle = OpenProcess(PROCESS_ALL_ACCESS, FALSE, pi.dwProcessId);
if (processHandle == NULL)
{
cout << "Could not obtain handle (" << GetLastError() << ")." << endl;
CloseHandle(pi.hProcess);
CloseHandle(pi.hThread);
return 2;
}
DebugActiveProcess(pi.dwProcessId);
//ResumeThread(pi.hThread);
WaitForSingleObject(pi.hProcess, INFINITE);
CloseHandle(processHandle);
CloseHandle(pi.hProcess);
CloseHandle(pi.hThread);
return 0;
}
EDIT: revised instructions based on updated code.
I see three things that will help. First, you have to obtain a handle to the process with correct access. The handle returned by CreateProcess does not have correct access. Close that process handle (pi.hProcess and pi.hThread) and call OpenProcess requesting PROCESS_ALL_ACCESS. Keep in mind this may fail if the user account running the program does not have the required permissions.
Next, make sure you first initiate debugging by calling DebugActiveProcess with the process handle obtained above. (This may or may not be required.)
Finally, after your edit, I noticed that you are waiting for the process handle using WaitForSingleObject. This has the effect of waiting for the process to terminate. Instead, use WaitForDebugEvent.
You are trying too hard. The system is already well prepared to launch a debugger for any arbitrary process. You only have to set the Image File Execution Options appropriately to launch the debugger automatically.
For reference, here are the instructions quoted from the MSDN:
To setup an application to launch the debugger automatically
Start the Registry Editor (regedit).
In the Registry Editor, open the HKEY_LOCAL_MACHINE folder.
Navigate to HKEY_LOCAL_MACHINE\Software\Microsoft\Windows NT\currentversion\image file execution options.
In the Image File Execution Options folder, locate the name of the application you want to debug, such as myapp.exe. If you cannot find the application you want to debug:
a. Right-click the Image File Execution Options folder, and on the shortcut menu, click New Key.
b. Right-click the new key, and on the shortcut menu, click Rename.
c. Edit the key name to the name of your application; myapp.exe, in this example.
Right-click the myapp.exe folder, and on the shortcut menu, click New String Value.
Right-click the new string value, and on the shortcut menu, click Rename.
Change the name to debugger.
Right-click the new string value, and on the shortcut menu, click Modify.
The Edit String dialog box appears.
In the Value data box, type vsjitdebugger.exe.
Click OK.
From the Registry menu, click Exit.
The directory containing vsjitdebugger.exe must be in your system path. To add it to the system path, follow these steps:
a. Open the Control Panel in Classic view, and double-click System.
b. Click Advanced System Settings.
c. In System Properties, click the Advanced tab.
d. On the Advanced tab, click Environment Variables.
e. In the Environment Variables dialog box, under System variables, select Path, then click the Edit button.
f. In the Edit System Variable dialog box, add the directory to the Variable value box. Use a semicolon to separate it from other entries in the list.
g. Click OK to close the Edit System Variable dialog box.
h. Click OK to close the Environment Variables dialog box.
i. Click OK to close the System Properties dialog box.
Now, use any method to start your application. Visual Studio will start and load the application.
I writing small application in pure C++. But now I encourage strange problem. I wanted to add my application to autostart but it not working. I use this code to access to Registry:
BOOL SetKeyData(HKEY hRootKey, WCHAR *subKey, DWORD dwType, WCHAR *value, LPBYTE data, DWORD cbData)
{
HKEY hKey;
if(RegCreateKeyW(hRootKey, subKey, &hKey) != ERROR_SUCCESS)
return FALSE;
LSTATUS status = RegSetValueExW(hKey, value, 0, dwType, data, cbData);
if(status != ERROR_SUCCESS)
{
RegCloseKey(hKey);
return FALSE;
}
RegCloseKey(hKey);
return TRUE;
}
At first I thought that problem is in data that I serve, so i converted WCHAR with path to LPBYTE like this and execute this function in this way:
size_t i;
char *pMBBuffer = (char *)malloc( MAX_PATH );
wcstombs_s(&i, pMBBuffer, MAX_PATH, my_program, MAX_PATH-1 );
SetKeyData(HKEY_LOCAL_MACHINE, L"Software\\Microsoft\\Windows\\CurrentVersion\\Run", REG_SZ, L"zwApplication", (LPBYTE)pMBBuffer, i))
I get status code ERROR_ACCESS_DENIED. Maybe problem is policy in Windows 7, but I thought that I have full access to everything in HKEY_LOCAL_MACHINE. How to solve this problem?
Writing to HKEY_LOCAL_MACHINE requires that your app runs with elevated privileges. Which means your app would require to set this in its manifest file.
Without this, you can only write to HKEY_CURRENT_USER, or read from HKEY_LOCAL_MACHINE - but for your autostart requirement, that would work just as fine.
EDIT 3
OK, so it seems like this might not be an Installer issue after all. When I make a simple batch file:
exit /b 12
and call it as
cmd /c test.cmd
echo %ERRORLEVEL%
I get "12" on Windows Server 2003 R2, but "0" on XP. I thought I had tested this simple test case many times before but apparently not.
So, I've changed the tags and title but I'm leaving the other information here as there's actually a lot of useful stuff here that is not directly related to this issue.
Thoughts?
Original below
I have a custom action written in VBScript that in turn is calling a Windows batch file (the custom action is essentially allowing the user to execute something at install time they can also run later by running the batch file - it's a convenience). The function is below:
Function MainFunction
strCustomActionData = Session.Property("CustomActionData")
strSplit = Split(strCustomActionData, ";")
strInstallDir = strSplit(0)
strPostCopyAction = strSplit(1)
strScriptLocation = strInstallDir & "\MigrationMasterProcess.cmd"
strFullCommand = """" & strScriptLocation & """ " & strPostCopyAction
Set objShell = CreateObject("WScript.Shell")
Dim objExec
Set objExec = objShell.Exec(strFullCommand)
intReturnCode = objExec.ExitCode
Set objExec = Nothing
Set objShell = Nothing
WriteMessage "Return value: " & intReturnCode
' cf. http://msdn.microsoft.com/en-us/library/windows/desktop/aa371254(v=vs.85).aspx
If (intReturnCode = 0) Then
MainFunction = 1
Else
MainFunction = 3
End If
End Function
When I run the same kind of code outside of a custom action, and the batch file returns an error code (via EXIT /B), the return value is correctly captured in intReturnCode. However, from the custom action, the exit code seems to be "lost" - I always get a 0 back (I can see this in the installer log from the WriteMessage call). It doesn't matter if I use Exec or Run on the shell, I still get back a 0. The script writes its own return code out before returning it (I can see this in the stdout stream from Exec) so I know it's not actually 0. I need that return code to properly report an error back to the installer.
Ideas?
For the record this is Windows Installer 3.0 on Windows XP SP3. The installer is in Wise so I don't have a WiX snippet or I would include it, but this is the function being called.
Also this is somewhat stripped - I've left out comments and other calls to WriteMessage as well as that function. And yes psuedo-Hungarian is evil blah blah blah.
Edit: Here is the C version of the code. It's giving the same exact issue:
#include <Windows.h>
#include <msi.h>
#include <msiquery.h>
#include <stdio.h>
#include <stdlib.h>
#include "LaunchChildProcess.h"
BOOL APIENTRY DllMain( HANDLE hModule, DWORD ul_reason_for_call, LPVOID lpReserved ) {
return TRUE;
}
UINT __stdcall RunMigrationAction(MSIHANDLE hModule) {
UINT uiStat;
DWORD dwPropertySize = MAX_PATH * 2;
TCHAR szValueBuf[MAX_PATH * 2]; // arbitrary but we know the strings won't be near that long
TCHAR *szInstallDir, *szPostCopyAction;
TCHAR *szNextToken;
TCHAR szScriptLocation[MAX_PATH * 2];
TCHAR szParameters[MAX_PATH * 2];
INT iReturnValue;
LogTaggedString(hModule, TEXT("Action Status"), TEXT("Starting"));
uiStat = MsiGetProperty(hModule, TEXT("CustomActionData"), szValueBuf, &dwPropertySize);
if (ERROR_SUCCESS != uiStat) {
LogTaggedString(hModule, TEXT("Startup"), TEXT("Failed to get custom action data"));
return ERROR_INSTALL_FAILURE;
}
LogTaggedString(hModule, TEXT("Properties given"), szValueBuf);
LogTaggedInteger(hModule, TEXT("Property length"), dwPropertySize);
if (0 == dwPropertySize) {
return ERROR_INSTALL_FAILURE;
}
LogTaggedString(hModule, TEXT("Properties given"), szValueBuf);
szInstallDir = wcstok_s(szValueBuf, TEXT(";"), &szNextToken);
szPostCopyAction = wcstok_s(NULL, TEXT(";"), &szNextToken);
LogTaggedString(hModule, TEXT("Install dir"), szInstallDir);
LogTaggedString(hModule, TEXT("Post-copy action"), szPostCopyAction);
wcscpy_s(szScriptLocation, MAX_PATH * 2, szInstallDir);
wcscat_s(szScriptLocation, MAX_PATH * 2, TEXT("\\MigrationMasterProcess.cmd"));
LogTaggedString(hModule, TEXT("Script location"), szScriptLocation);
wcscpy_s(szParameters, MAX_PATH * 2, TEXT(" /C "));
wcscat_s(szParameters, MAX_PATH * 2, szScriptLocation);
wcscat_s(szParameters, MAX_PATH * 2, TEXT(" "));
wcscat_s(szParameters, MAX_PATH * 2, szPostCopyAction);
LogTaggedString(hModule, TEXT("Parameters to cmd.exe"), szParameters);
iReturnValue = ExecuteProcess(TEXT("cmd.exe"), szParameters);
LogTaggedInteger(hModule, TEXT("Return value from command"), iReturnValue);
LogTaggedString(hModule, TEXT("Action Status"), TEXT("Finished"));
return (0 == iReturnValue) ? ERROR_SUCCESS : ERROR_INSTALL_FAILURE;
}
void LogTaggedInteger(MSIHANDLE hInstall, TCHAR* szTag, INT iValue) {
TCHAR szValue[15];
_itow_s(iValue, szValue, 15, 10);
LogTaggedString(hInstall, szTag, szValue);
}
void LogTaggedString(MSIHANDLE hInstall, TCHAR* szTag, TCHAR* szMessage) {
MSIHANDLE hRecord;
UINT uiStat;
//TCHAR szFullMessage[4096];
//wcscpy_s(szFullMessage, 4096, TEXT("--------------- "));
//wcscat_s(szFullMessage, 4096, szTag);
//wcscat_s(szFullMessage, 4096, TEXT(": "));
//wcscat_s(szFullMessage, 4096, szMessage);
hRecord = MsiCreateRecord(3);
uiStat = MsiRecordSetString(hRecord, 0, TEXT("--------- [1]: [2]"));
uiStat = MsiRecordSetString(hRecord, 1, szTag);
uiStat = MsiRecordSetString(hRecord, 2, szMessage);
uiStat = MsiProcessMessage(hInstall, INSTALLMESSAGE(INSTALLMESSAGE_INFO), hRecord);
MsiCloseHandle(hRecord);
return;
}
int MsiMessageBox(MSIHANDLE hInstall, TCHAR* szString, DWORD dwDlgFlags) {
PMSIHANDLE newHandle = ::MsiCreateRecord(2);
MsiRecordSetString(newHandle, 0, szString);
return (MsiProcessMessage(hInstall, INSTALLMESSAGE(INSTALLMESSAGE_USER + dwDlgFlags), newHandle));
}
DWORD ExecuteProcess(TCHAR *szProcess, TCHAR *szParams) {
INT iMyCounter = 0, iPos = 0;
DWORD dwReturnVal = 0;
TCHAR *sTempStr = L"";
/* CreateProcessW can modify Parameters thus we allocate needed memory */
wchar_t * pwszParam = new wchar_t[wcslen(szParams) + 1];
if (NULL == pwszParam) {
return 1;
}
wcscpy_s(pwszParam, wcslen(szParams) + 1, szParams);
/* CreateProcess API initialization */
STARTUPINFOW siStartupInfo;
PROCESS_INFORMATION piProcessInfo;
memset(&siStartupInfo, 0, sizeof(siStartupInfo));
memset(&piProcessInfo, 0, sizeof(piProcessInfo));
siStartupInfo.cb = sizeof(siStartupInfo);
if (CreateProcessW(const_cast<LPCWSTR>(szProcess),
pwszParam, 0, 0, false,
CREATE_DEFAULT_ERROR_MODE, 0, 0,
&siStartupInfo, &piProcessInfo) != false) {
/* Watch the process. */
WaitForSingleObject(piProcessInfo.hProcess, INFINITE);
if (!GetExitCodeProcess(piProcessInfo.hProcess, &dwReturnVal)) {
dwReturnVal = GetLastError();
}
} else {
/* CreateProcess failed */
dwReturnVal = GetLastError();
}
/* Free memory */
free(pwszParam);
pwszParam = NULL;
/* Release handles */
CloseHandle(piProcessInfo.hProcess);
CloseHandle(piProcessInfo.hThread);
return dwReturnVal;
}
When run on my Windows Server 2003 R2 Visual Studio 2008 box, I get the error code as expected:
--------- Return value from command: 5023
When run on my Windows XP test box, I get a 0, even though it should be an error:
--------- Return value from command: 0
Both machines have Windows Installer 3.1. XP is 3.01.4001.5512, 2003 R2 is 3.01.4000.3959.
So it's something acting different between the boxes although I have no idea what.
EDIT 2
The exact table row for the action, as generated by the Wise for Windows Installer tool, is:
"RunMigrationActionCA","1537","Calllaunchchildprocess","RunMigrationAction","0"
To test the immediate flag I added 0x800 to the type column and no change was seen in the end behavior.
To be clear - this works fine on the 2003 R2 machine. That machine is not joined to a domain, but the XP machine is. Is there anything in group policy that could cause this behavior? (Grasping at straws at this point.)
It seems to be a bug in the cmd.exe of WinXP.
The solution is to use exit 123 instead of exit /b 123 in the batch file.
If you don't wish to change existing batch files, just add a wrapper.bat:
#echo off
call %*
exit %errorlevel%
And invoke it:
system("wrapper.bat your.bat all your args")
WScript objects don't work inside custom actions Please reader more here. You could use a DLL custom action. Here is a step by step tutorial.
How do I determine the (local-) path for the "Program Files" directory on a remote computer? There does not appear to any version of SHGetFolderPath (or related function) that takes the name of a remote computer as a parameter.
I guess I could try to query HKLM\Software\Microsoft\Windows\CurrentVersion\ProgramFilesDir using remote-registry, but I was hoping there would be "documented" way of doing it.
Many of the standard paths require a user to be logged in, especially the SH* functions as those are provided by the "shell", that is, Explorer. I suspect the only way you're going to get the right path is through the registry like you already mentioned.
This is what I ended up doing: (pszComputer must be on the form "\\name". nPath is size of pszPath (in TCHARs))
DWORD GetProgramFilesDir(PCTSTR pszComputer, PTSTR pszPath, DWORD& nPath)
{
DWORD n;
HKEY hHKLM;
if ((n = RegConnectRegistry(pszComputer, HKEY_LOCAL_MACHINE, &hHKLM)) == ERROR_SUCCESS)
{
HKEY hWin;
if ((n = RegOpenKeyEx(hHKLM, _T("Software\\Microsoft\\Windows\\CurrentVersion"), 0, KEY_READ, &hWin)) == ERROR_SUCCESS)
{
DWORD nType, cbPath = nPath * sizeof(TCHAR);
n = RegQueryValueEx(hWin, _T("ProgramFilesDir"), NULL, &nType, reinterpret_cast<PBYTE>(pszPath), &cbPath);
nPath = cbPath / sizeof(TCHAR);
RegCloseKey(hWin);
}
RegCloseKey(hHKLM);
}
return n;
}