//+---------------------------------------------------------------------------- // // File: cmstp.cpp // // Module: CMSTP.EXE // // Synopsis: This file is the main function for the CM profile installer. This // file basically processes command line switches for the installer and // then launches the appropriate function. // // Copyright (c) 1998-1999 Microsoft Corporation // // Author: quintinb Created 07/13/98 // //+---------------------------------------------------------------------------- #include "cmmaster.h" #include "installerfuncs.h" #include "cmstpex.h" // // Text Constants // static const TCHAR CMSTPMUTEXNAME[] = TEXT("Connection Manager Profile Installer Mutex"); // // Global Dynamic Library Classes to hold the ras dll's and shell32. See // the EnsureRasDllsLoaded and the EnsureShell32Loaded in common.cpp/common.h // CDynamicLibrary* g_pRasApi32 = NULL; CDynamicLibrary* g_pRnaph = NULL; CDynamicLibrary* g_pShell32 = NULL; CDynamicLibrary* g_pNetShell = NULL; // // Function Headers // BOOL PromptUserToUninstallProfile(HINSTANCE hInstance, LPCTSTR pszInfFile); // from uninstall.cpp BOOL PromptUserToUninstallCm(HINSTANCE hInstance); // from uninstallcm.cpp // // Enum for the LastManOut function which follows. // typedef enum _UNINSTALLTYPE { PROFILEUNINSTALL, // a profile is being uninstalled CMUNINSTALL // the cm bits themselves are being uninstalled. } UNINSTALLTYPE; //+---------------------------------------------------------------------------- // // Function: LastManOut // // Synopsis: This function determines if the current uninstall action is the // last uninstall action which should then delete cmstp.exe. If the // uninstall action is a profile uninstall we need to check that // cm has already been uninstalled and that there is only one profile // installed currently (the one we are about to delete). If the // uninstall action is uninstalling CM then we need to make sure there // are no other profiles on the machine. Notice that this function // never returns TRUE on Native CM platforms. If it did, then cmstp.exe // would be deleted inadvertently even though UninstallCm wouldn't // actually delete the rest of CM. // // Arguments: UNINSTALLTYPE UninstallType - an enum value which tells if this is // a profile uninstall or a CM uninstall. // // Returns: BOOL - TRUE if this install is the last one out and cmstp.exe should // be deleted. // // History: quintinb Created 6/28/99 // //+---------------------------------------------------------------------------- BOOL LastManOut(UNINSTALLTYPE UninstallType, LPCTSTR pszInfFile) { BOOL bReturn = FALSE; // // First check to make sure that remcmstp.inf doesn't exist in the system // directory. If it does, then we know that Cmstp.exe has already determined // that it is the last man and should delete itself. Thus it wrote the cmstp.exe // command into remcmstp.inf and the inf engine will delete cmstp.exe when it is done. // Thus we need to check for this file and if it exists return FALSE. // TCHAR szSystemDir[MAX_PATH+1]; TCHAR szTemp[MAX_PATH+1]; if (0 == GetSystemDirectory(szSystemDir, CELEMS(szSystemDir))) { CMASSERTMSG(FALSE, TEXT("LastManOut -- Unable to obtain a path to the System Directory")); return FALSE; } wsprintf(szTemp, TEXT("%s\\remcmstp.inf"), szSystemDir); if (FileExists(szTemp)) { CMTRACE1(TEXT("\tDetected remcmstp.inf, not setting last man out -- Process ID is 0x%x "), GetCurrentProcessId()); Sleep(2000); // we sleep here to put a little delay in the processing to let any other copies // of cmstp.exe clean themselves up. I found that on a system with several copies of // cmstp.exe all deleting profiles and then a cmstp to delete CM, not all of the cmstps // would clean up in time and thus cmstp.exe wouldn't get deleted. A sleep is hokey, but // two seconds in the last man out situation only fixes it and it no down level user should // ever have 8 profiles (which was home many I tested it with) let alone delete them // all at once. It works fine for deleting two profiles and CM simultaneously either way. return FALSE; } // // Make sure that we aren't trying to Remove cmstp.exe on a platform where CM is Native. // If CM is Native, then always return FALSE because the CM uninstall function won't // uninstall CM and we don't want to accidently delete cmstp.exe. // if (!CmIsNative()) { if (PROFILEUNINSTALL == UninstallType) { // // We are uninstalling a profile. We need to check to see if CM has been deleted and // if there are any other profiles on the machine besides the one we are going to delete. // wsprintf(szTemp, TEXT("%s\\cmdial32.dll"), szSystemDir); if (!FileExists(szTemp)) { // // Then we know that CM is already gone. We need to check and see if any other // profiles exist besides the one we are about to delete. // HKEY hKey; DWORD dwNumValues; TCHAR szServiceName[MAX_PATH+1]; if (ERROR_SUCCESS == RegOpenKeyEx(HKEY_LOCAL_MACHINE, c_pszRegCmMappings, 0, KEY_READ, &hKey)) { if ((ERROR_SUCCESS == RegQueryInfoKey(hKey, NULL, NULL, NULL, NULL, NULL, NULL, &dwNumValues, NULL, NULL, NULL, NULL)) && (dwNumValues == 1)) { // // Then we have only the one profile mappings key, is it the correct one? // if (0 != GetPrivateProfileString(c_pszInfSectionStrings, c_pszCmEntryServiceName, TEXT(""), szServiceName, MAX_PATH, pszInfFile)) { DWORD dwSize = MAX_PATH; LONG lResult = RegQueryValueEx(hKey, szServiceName, NULL, NULL, (LPBYTE)szTemp, &dwSize); if ((ERROR_SUCCESS == lResult) && (TEXT('\0') != szTemp[0])) { CMTRACE1(TEXT("\tDetected Last Man Out -- Process ID is 0x%x "), GetCurrentProcessId()); bReturn = TRUE; } } } RegCloseKey(hKey); } } } else if (CMUNINSTALL == UninstallType) { // // We are uninstalling CM. We want to make sure that we don't have any profiles // still installed. If not, then we are the last man out. // if (!AllUserProfilesInstalled()) { CMTRACE1(TEXT("\tDetected Last Man Out -- Process ID is 0x%x "), GetCurrentProcessId()); bReturn = TRUE; } } else { CMASSERTMSG(FALSE, TEXT("LastManOut -- Unknown Uninstall Type")); } } return bReturn; } //+---------------------------------------------------------------------------- // // Function: ExtractInfAndRelaunchCmstp // // Synopsis: This function is used to cleanup Cmstp.exe in the last man out // scenario. In order to not leave cmstp.exe on a users machine, // we must extract remcmstp.inf and write the uninstall command to it. // That way, the inf will monitor the cmstp.exe process and when it is // finished it can then delete cmstp.exe. // // Arguments: HINSTANCE hInstance - Instance handle to load resources // DWORD dwFlags - Command line param flags // LPCTSTR szInfPath - path to the inf file. // // Returns: BOOL -- TRUE if Successful // // History: quintinb Created 6/28/99 // //+---------------------------------------------------------------------------- BOOL ExtractInfAndRelaunchCmstp(HINSTANCE hInstance, DWORD dwFlags, LPCTSTR pszInfPath) { // // Check Parameters // if (0 == dwFlags || NULL == pszInfPath || TEXT('\0') == pszInfPath[0]) { CMASSERTMSG(FALSE, TEXT("Invalid Paramater passed to ExtractInfAndRelaunchCmstp.")); return FALSE; } // // Get the Path to the System Directory // TCHAR szSystemDir[MAX_PATH+1]; if (0 == GetSystemDirectory(szSystemDir, CELEMS(szSystemDir))) { CMASSERTMSG(FALSE, TEXT("ExtractInfAndRelaunchCmstp -- Unable to obtain a path to the System Directory")); return FALSE; } // // Extract remcmstp.inf // HGLOBAL hRemCmstp = NULL; LPTSTR pszRemCmstpInf = NULL; HRSRC hResource = FindResource(hInstance, MAKEINTRESOURCE(IDT_REMCMSTP_INF), TEXT("REGINST")); if (hResource) { hRemCmstp = LoadResource(hInstance, hResource); if (hRemCmstp) { // // Note that we don't need to call FreeResource, which is obsolete, this // will be cleaned up when cmstp.exe exits. // pszRemCmstpInf = (LPTSTR)LockResource(hRemCmstp); } } // // Now that we have the remcmstp.inf file that is stored in the cmstp.exe resource // loaded into memory and have a pointer to it, lets create the file that we are // going to write it out to. // if (pszRemCmstpInf) { TCHAR szRemCmstpPath[MAX_PATH+1]; wsprintf(szRemCmstpPath, TEXT("%s\\remcmstp.inf"), szSystemDir); HANDLE hFile = CreateFile(szRemCmstpPath, GENERIC_WRITE, FILE_SHARE_READ, NULL, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL); if (INVALID_HANDLE_VALUE != hFile) { // // Then we have the file, lets write the data to it. // DWORD cbWritten; if (WriteFile(hFile, pszRemCmstpInf, lstrlen(pszRemCmstpInf)*sizeof(TCHAR), &cbWritten, NULL)) { // // We launch the inf to delete cmstp right now. The inf has a PreSetupCommand that // launches the cmstp.exe uninstall command with a /s switch (which we write in the // inf after extracting it). The inf then launches the new cmstp, which forces the newly // launched cmstp.exe to wait on the mutex of the current cmstp.exe until it is finished. // Since profile installs will error on the mutex instead of waiting for it, we // shouldn't get any installs until after the uninstall and the cleanup inf have run. // Note that the inf will wait for the PreSetupCommands to finish before processing the inf. // This is important because we could be waiting on User input (the OK dialog from // deleting CM for instance). // CloseHandle(hFile); // // Now lets write the cmstp.exe command into remcmstp.inf // LPTSTR pszUninstallFlag = NULL; if (dwFlags & c_dwUninstallCm) { pszUninstallFlag = c_pszUninstallCm; } else if (dwFlags & c_dwUninstall) { pszUninstallFlag = c_pszUninstall; } else { CMASSERTMSG(FALSE, TEXT("ExtractInfAndRelaunchCmstp -- Unknown Uninstall Type, exiting")); return FALSE; } TCHAR szShortInfPath[MAX_PATH+1] = {0}; TCHAR szParams[2*MAX_PATH+1] = {0}; DWORD dwRet = GetShortPathName(pszInfPath, szShortInfPath, MAX_PATH); if (0 == dwRet || MAX_PATH < dwRet) { CMASSERTMSG(FALSE, TEXT("ExtractInfAndRelaunchCmstp -- Unable to get the short path to the Inf, exiting")); return FALSE; } wsprintf(szParams, TEXT("%s\\cmstp.exe %s %s %s"), szSystemDir, pszUninstallFlag, c_pszSilent, szShortInfPath); WritePrivateProfileSection(TEXT("PreSetupCommandsSection"), szParams, szRemCmstpPath); // // Finally lets launch the inf uninstall with the new cmstp command in it. // wsprintf(szParams, TEXT("advpack.dll,LaunchINFSection %s\\remcmstp.inf, Uninstall"), szSystemDir); SHELLEXECUTEINFO sei = {0}; sei.cbSize = sizeof(sei); sei.fMask = SEE_MASK_FLAG_NO_UI; sei.nShow = SW_SHOWNORMAL; sei.lpFile = TEXT("Rundll32.exe"); sei.lpParameters = szParams; sei.lpDirectory = szSystemDir; if (!ShellExecuteEx(&sei)) { CMTRACE1(TEXT("ExtractInfAndRelaunchCmstp -- ShellExecute Returned an error, GLE %d"), GetLastError()); } else { return TRUE; } } else { CloseHandle(hFile); CMASSERTMSG(FALSE, TEXT("ExtractInfAndRelaunchCmstp -- Unable to write the file data to remcmstp.inf")); } } else { CMASSERTMSG(FALSE, TEXT("ExtractInfAndRelaunchCmstp -- Unable to Create remcmstp.inf in the system directory.")); } } else { CMASSERTMSG(FALSE, TEXT("ExtractInfAndRelaunchCmstp -- Unable to load the remcmstp.inf custom resource.")); } return FALSE; } //+---------------------------------------------------------------------------- // // Function: IsInstall // // Synopsis: Wrapper function to check and see if this is an install or not. // // Arguments: DWORD dwFlags - the action flags parameter returned from the // command line parsing class. // // Returns: BOOL - TRUE if this is an Install command // // History: quintinb Created Header 6/28/99 // //+---------------------------------------------------------------------------- BOOL IsInstall(DWORD dwFlags) { return (0 == (dwFlags & 0xFF)); } //+---------------------------------------------------------------------------- // // Function: ProcessCmstpExtensionDll // // Synopsis: Processes the cmstp extension dll registry keys and calls out // to the extension proc as necessary to modify the action behavior. // Using the extension proc, we can modify the install, uninstall, // etc. behavior that cmstp exhibits. This is most useful on platforms // that have Native CM (or just a very new copy of CM) but an older // profile is being installed. Since the cmstp.exe that is in the package // does the actual installation, we can modify the installation parameters, // modify the inf path, or even stop the install. Since we get called // after the install as well, we can even take post-install or cleanup // actions. // // Arguments: LPDWORD pdwFlags - pointer to the flags parameter, note that it // can be modified by the extension proc // LPTSTR pszInfPath - Inf path, note that it can be modified // by the extension proc. // HRESULT hrRet - current return value, this is only used on // the post action proc call. // EXTENSIONDLLPROCTIMES PreOrPost - if this is a Pre action // call or a Post action call. // // Returns: BOOL - TRUE if cmstp.exe should continue, FALSE stops the action // (install, uninstall, migration, whatever) without further // action. // // History: quintinb Created Header 6/28/99 // //+---------------------------------------------------------------------------- BOOL ProcessCmstpExtensionDll (LPDWORD pdwFlags, LPTSTR pszInfPath, HRESULT hrRet, EXTENSIONDLLPROCTIMES PreOrPost) { // // Check for the CmstpExtensionDll reg key in Cm App Paths // const TCHAR* const c_pszRegCmstpExtensionDll = TEXT("CmstpExtensionDll"); const char* const c_pszCmstpExtensionProc = "CmstpExtensionProc"; // GetProcAddress takes ANSI strings -- quintinb pfnCmstpExtensionProcSpec pfnCmstpExtensionProc = NULL; HKEY hKey; TCHAR szCmstpExtensionDllPath[MAX_PATH+1]; ZeroMemory(szCmstpExtensionDllPath, CELEMS(szCmstpExtensionDllPath)); if (ERROR_SUCCESS == RegOpenKeyEx(HKEY_LOCAL_MACHINE, c_pszRegCmAppPaths, 0, KEY_READ, &hKey)) { DWORD dwSize = CELEMS(szCmstpExtensionDllPath); DWORD dwType = REG_SZ; if (ERROR_SUCCESS == RegQueryValueEx(hKey, c_pszRegCmstpExtensionDll, NULL, &dwType, (LPBYTE)szCmstpExtensionDllPath, &dwSize)) { CDynamicLibrary CmstpExtensionDll (szCmstpExtensionDllPath); pfnCmstpExtensionProc = (pfnCmstpExtensionProcSpec)CmstpExtensionDll.GetProcAddress(c_pszCmstpExtensionProc); if (NULL == pfnCmstpExtensionProc) { return TRUE; } else { return (pfnCmstpExtensionProc)(pdwFlags, pszInfPath, hrRet, PreOrPost); } } RegCloseKey(hKey); } return TRUE; } //_____________________________________________________________________________ // // Function: WinMain // // Synopsis: Processes command line switches -- see common\inc\cmstpex.h for full list // // // Arguments: HINSTANCE hInstance - // HINSTANCE hPrevInstance - // PSTR szCmdLine - pass in the inf file name here // int iCmdShow - // // Returns: int WINAPI - // // History: Re-created quintinb 7-13-98 // //_____________________________________________________________________________ int WINAPI WinMain (HINSTANCE, //hInstance HINSTANCE, //hPrevInstance PSTR, //szCmdLine int //iCmdShow ) { CMTRACE(TEXT("=====================================================")); CMTRACE1(TEXT(" CMSTP.EXE - LOADING - Process ID is 0x%x "), GetCurrentProcessId()); CMTRACE(TEXT("=====================================================")); BOOL bUsageError = FALSE; BOOL bAnotherInstanceRunning = FALSE; HRESULT hrReturn = S_OK; TCHAR szMsg[MAX_PATH+1]; TCHAR szTitle[MAX_PATH+1]; TCHAR szInfPath[MAX_PATH+1]; DWORD dwFlags = 0; CPlatform plat; CNamedMutex CmstpMutex; // keep this here so it doesn't get destructed until main ends. // this gives us better control of when it is unlocked. HINSTANCE hInstance = GetModuleHandleA(NULL); LPTSTR szCmdLine = GetCommandLine(); // // Check to make sure that we aren't an x86 version of cmstp running on an Alpha // #ifdef CMX86BUILD if (plat.IsAlpha()) { MYVERIFY(0 != LoadString(hInstance, IDS_CMSTP_TITLE, szTitle, MAX_PATH)); MYVERIFY(0 != LoadString(hInstance, IDS_BINARY_NOT_ALPHA, szMsg, MAX_PATH)); MessageBox(NULL, szMsg, szTitle, MB_OK); return FALSE; } #endif // // Setup the Command Line Arguments // ZeroMemory(szInfPath, sizeof(szInfPath)); { // Make sure ArgProcessor gets destructed properly and we don't leak mem CProcessCmdLn ArgProcessor(c_NumArgs, (ArgStruct*)&Args, TRUE, FALSE); //bSkipFirstToken == TRUE, bBlankCmdLnOkay == FALSE if (ArgProcessor.GetCmdLineArgs(szCmdLine, &dwFlags, szInfPath, MAX_PATH)) { // // We want to wait indefinitely, unless this is an install. If it is an // install then we want to return immediately and throw an error if we couldn't // get the lock (NTRAID 261248). We also want to be able to launch two profiles // simulaneously on NT5 (cmstp.exe takes the place of explorer.exe) thus we will // pass the pointer to the CNamedMutex object to the install function so that // it can release the mutex once the install is finished except for launching the // profile (NTRAID 310478). // BOOL bWait = !IsInstall(dwFlags); if (CmstpMutex.Lock(CMSTPMUTEXNAME, bWait, INFINITE)) { // // We got the mutex lock, so go ahead and process the command line // arguments. First, however, check for a cmstp Dll listed in the // app paths key of CM. If a dll is listed here, then we want to load // the dll and pass it the inf path and the install flags. If the dll // proc returns FALSE, then we want to exit. Otherwise continue with // the install as normal. // Of the install flags we first check for /x, ,/m, or /mp // (these switches must be by themselves, we don't allow any // modifier switches with these), the non-install commands. We now allow the uninstall // command to take the Silent switch to silence our uninstall prompt. // if (ProcessCmstpExtensionDll(&dwFlags, szInfPath, S_OK, PRE)) { CMTRACE2(TEXT("CMSTP.EXE -- Entering Flag Processing Loop, dwFlags = %u and szInfPath = %s"), dwFlags, szInfPath); if (c_dwHelp & dwFlags) { bUsageError = TRUE; } else if (c_dwUninstall & dwFlags) { if (((c_dwUninstall == dwFlags) || ((c_dwUninstall | c_dwSilent) == dwFlags)) && (TEXT('\0') != szInfPath[0])) { BOOL bSilent = (dwFlags & c_dwSilent); if (bSilent || PromptUserToUninstallProfile(hInstance, szInfPath)) { // // Okay, the user wants to uninstall. Now check to see if we are the last // man out. If we are then we also need to delete cmstp. // if (LastManOut(PROFILEUNINSTALL, szInfPath)) { ExtractInfAndRelaunchCmstp(hInstance, dwFlags, szInfPath); } else { hrReturn = UninstallProfile(hInstance, szInfPath, TRUE); // bCleanUpCreds == TRUE MYVERIFY(SUCCEEDED(hrReturn)); } } } else { bUsageError = TRUE; } } else if (c_dwOsMigration & dwFlags) { if ((c_dwOsMigration == dwFlags) && (TEXT('\0') == szInfPath[0])) { hrReturn = MigrateCmProfilesForWin2kUpgrade(hInstance); MYVERIFY(SUCCEEDED(hrReturn)); } else { bUsageError = TRUE; } } else if (c_dwProfileMigration & dwFlags) { if ((c_dwProfileMigration == dwFlags) && (TEXT('\0') == szInfPath[0])) { TCHAR szCurrentDir[MAX_PATH+1]; if (0 == GetCurrentDirectory(MAX_PATH, szCurrentDir)) { return FALSE; } lstrcat(szCurrentDir, TEXT("\\")); hrReturn = MigrateOldCmProfilesForProfileInstall(hInstance, szCurrentDir); MYVERIFY(SUCCEEDED(hrReturn)); } else { bUsageError = TRUE; } } else if (c_dwUninstallCm & dwFlags) { if (((c_dwUninstallCm == dwFlags) || ((c_dwUninstallCm | c_dwSilent) == dwFlags)) && (TEXT('\0') != szInfPath[0])) { BOOL bNoBeginPrompt = (dwFlags & c_dwSilent); if (bNoBeginPrompt || PromptUserToUninstallCm(hInstance)) { // // Okay, the user wants to uninstall. Now check to see if we are the last // man out. If we are then we also need to delete cmstp. // if (LastManOut(CMUNINSTALL, szInfPath)) { if (ExtractInfAndRelaunchCmstp(hInstance, dwFlags, szInfPath)) { // // We need to delete the Uninstall key so that we don't leave // it in Add/Remove Programs (the refresh is keyed off of this // executable ending not the relaunched cmstp.exe's ending). // NTRAID 336249 // HRESULT hrTemp = HrRegDeleteKeyTree(HKEY_LOCAL_MACHINE, TEXT("SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\Connection Manager")); MYDBGASSERT(SUCCEEDED(hrTemp)); } } else { hrReturn = UninstallCm(hInstance, szInfPath); MYVERIFY(SUCCEEDED(hrReturn)); } } } else { bUsageError = TRUE; } } else { // // Install, note that on NT5 we will release the CmstpMutex once // we are finished installing and just want to launch the profile. // hrReturn = InstallInf(hInstance, szInfPath, (dwFlags & c_dwNoSupportFiles), (dwFlags & c_dwNoLegacyIcon), (dwFlags & c_dwNoNT5Shortcut), (dwFlags & c_dwSilent), (dwFlags & c_dwSingleUser), (dwFlags & c_dwSetDefaultCon), &CmstpMutex); if (FAILED(hrReturn)) { CMTRACE2("Cmstp.exe -- InstallInf failed with error %d (0x%lx)", hrReturn, hrReturn); } } // // Again call the Cmstp Extension Dll if one exists. We want to give it // a chance to take post install actions if necessary. ProcessCmstpExtensionDll(&dwFlags, szInfPath, hrReturn, POST); } } else { bAnotherInstanceRunning = TRUE; } } else { bUsageError = TRUE; } } // // Clean up our Dll's // if (g_pRasApi32) { g_pRasApi32->Unload(); CmFree(g_pRasApi32); } if (g_pRnaph) { g_pRnaph->Unload(); CmFree(g_pRnaph); } if (g_pShell32) { g_pShell32->Unload(); CmFree(g_pShell32); } if (g_pNetShell) { g_pNetShell->Unload(); CmFree(g_pNetShell); } // // UnLock the cmstp mutex, note that it may never have been locked or // it could have been unlocked on Windows 2000 upon launching a profile, // the named mutex class will handle this. // CmstpMutex.Unlock(); // // Display any error messages after unlocking the mutex so that don't hold // it in the Usage message case. Another instance running should only // happen when an install tries to acquire the mutex while another cmstp // is running, thus the mutex was never acquired but put the message code // here to keep it in one place. // if (bUsageError) { CMTRACE("Cmstp.exe -- Usage Error!"); if (0 == (dwFlags & c_dwSilent)) { const int c_MsgLen = 1024; TCHAR* pszMsg = (TCHAR*)CmMalloc(sizeof(TCHAR)*(c_MsgLen+1)); if (pszMsg) { MYVERIFY(0 != LoadString(hInstance, IDS_CMSTP_TITLE, szTitle, MAX_PATH)); MYVERIFY(0 != LoadString(hInstance, IDS_USAGE_MSG, pszMsg, c_MsgLen)); MessageBox(NULL, pszMsg, szTitle, MB_OK | MB_ICONINFORMATION); CmFree(pszMsg); } } } else if (bAnotherInstanceRunning) { MYVERIFY(0 != LoadString(hInstance, IDS_CMSTP_TITLE, szTitle, MAX_PATH)); MYVERIFY(0 != LoadString(hInstance, IDS_INUSE_MSG, szMsg, MAX_PATH)); MessageBox(NULL, szMsg, szTitle, MB_OK); } // // Check for memory leaks // EndDebugMemory(); // // get return value // BOOL bRet = SUCCEEDED(hrReturn) && !bUsageError && !bAnotherInstanceRunning; // // Since we don't link to libc, we need to do this ourselves. // CMTRACE(TEXT("=====================================================")); CMTRACE1(TEXT(" CMSTP.EXE - UNLOADING - Process ID is 0x%x "), GetCurrentProcessId()); CMTRACE(TEXT("=====================================================")); ExitProcess((UINT)bRet); return bRet; }