Programmatically Discriminate between Upgrade or Uninstall of a CAB on Windows Mobile

Recently I've worked with a developer on an interesting issue I’ve not found any clue on the web about, and the solution is based on one of those details that you can empirically retrieve but that there are not documented anywhere, therefore on future releases may change without any warning. This was for example what happened to the ClassName of NETCF applications... see Daniel Moth's post about this: "#NETCF_AGL_". I’ve also discussed about this in a MSDN Forum post I found interesting, where the topic was something like “how to prevent the CLR to not allow a second instance of the same NETCF application to run on Windows Mobile”. As I probably wrote elsewhere, “undocumented” doesn’t mean “technically not achievable”: it means that Product Group may change it as it doesn’t have to be backward-compatible.

In this case we had an application that may have been updated at a later time: the ISV was wondering if there’s any way in the setup.dll of application’s CAB to specify, during uninstallation, if the uninstall is taking place during a version-upgrade or if it's a pure uninstallation. This is because, for example, the application's installation copies also some large files that user no longer needs after the uninstall and therefore are deleted: but it still needs them if the user is uninstalling a former version of the app in order to install a newer one. I hope I've been clear... smile_confused Things can get more complicated by the fact that the when you do an “upgrade” of the same ap

Well... we found out that there's no documented and standard way to achieve the goal, so we had to be creative - as usual... Nerd To understand how to operate, we needed to understand the actual flow when installing\uninstalling\upgrading (=installing the CAB of a newer version of the app while a older one is installed); moreover, we had to take care a particular condition, i.e. when upgrading the user is prompted with the message “The previous version of… Select Ok to continue or cancel to quit” -- and here it comes handy the “undocumented but empirically retrievable” info, that I'm going to show in a minute.

The regular flow when installing and uninstalling is:

  • Install:
    1. DLL_PROCESS_ATTACH – Setup.dll is loaded
    2. Install_Init
    3. Install_Exit
    4. DLL_PROCESS_DETACH – Setup.dll is unloaded
  • Uninstall:
    1. DLL_PROCESS_ATTACH – Setup.dll is loaded
    2. Uninstall_Init
    3. Uninstall_Exit
    4. DLL_PROCESS_DETACH – Setup.dll is unloaded

When upgrading, the flow is as follows:

  1. DLL_PROCESS_ATTACH – SetupDLL.dll is loaded
  2. Install_Init
  3. DLL_PROCESS_DETACH – SetupDLL.dll is unloaded
  4. Message prompt to the user to confirm uninstall of previous version
    • Select Ok:
      1. DLL_PROCESS_ATTACH – SetupDLL.dll is loaded, *BUT* the installer doesn’t know if we’re uninstalling because of a real uninstall or an upgrade
      2. Uninstall_Init
      3. Uninstall_Exit
      4. DLL_PROCESS_DETACH – SetupDLL.dll is unloaded
      5. DLL_PROCESS_ATTACH – SetupDLL.dll is loaded
      6. Install_Init
      7. Install_Exit
      8. running the exec
      9. DLL_PROCESS_DETACH – SetupDLL.dll is unloaded
    • Select Cancel:
      1. Nothing happens (setup.dll was already unloaded)

So the problem is how to let the installer know that it’s uninstalling or upgrading… the idea I had was to modify the flow this way, based on the fact that when “upgrading”, the flow involves firstly a Install_Init and secondly a Uninstall_Init; in contrast when “uninstalling” the flow doesn’t involve a first step through Install_Init:

a. Install

  1. DLL_PROCESS_ATTACH – SetupDLL.dll is loaded
  2. Install_Init (query if the app is already installed (through the Uninstall CSP) and set a registry key or whatever, e.g.[HKLM\UpgradeKey]Upgrade=0 if it was not installed and 1 viceversa) –> now: Upgrade=0
  3. Install_Exit
  4. DLL_PROCESS_DETACH – SetupDLL.dll is unloaded

b. Upgrade:

  1. DLL_PROCESS_ATTACH – SetupDLL.dll is loaded
  2. Install_Init (query if the app is already installed (through the Uninstall CSP) and set a registry key or whatever, e.g.[HKLM\UpgradeKey]Upgrade=0 if it was not installed and 1 viceversa) –> now: Upgrade=1
  3. DLL_PROCESS_DETACH – SetupDLL.dll is unloaded
  4. Message prompt to the user to confirm uninstall of previous version
    • Select Ok:
      1. DLL_PROCESS_ATTACH – SetupDLL.dll is loaded
      2. Uninstall_Init (QUERY [HKLM\UpgradeKey]Upgrade and act accordingly) –> now: Upgrade=1 (was just set by Install_Init at point 2. of the Upgrade flow, and then it can be set back to 0)
      3. Uninstall_Exit
      4. DLL_PROCESS_DETACH – SetupDLL.dll is unloaded
      5. DLL_PROCESS_ATTACH – SetupDLL.dll is loaded
      6. Install_Init
      7. Install_Exit
      8. running the exec
      9. DLL_PROCESS_DETACH – SetupDLL.dll is unloaded
    • Select Cancel:
      1. Nothing happens (setup.dll was already unloaded)

c. Uninstall

  1. DLL_PROCESS_ATTACH – SetupDLL.dll is loaded
  2. Uninstall_Init (query if we’re upgrading by looking at the registry key) –> now: Upgrade=0 (it wasn’t changed by anyone)
  3. Uninstall_Exit
  4. DLL_PROCESS_DETACH – SetupDLL.dll is unloaded

To conclude, the idea was to:

  • Install_Init creates the “Upgrade” registry key (or other info) and sets 0 if the application is NOT already installed and 1 viceversa. To check if an application is already installed I think I’ve already discussed once on the Uninstall Configuration Service Provider… yes, see this post.
  • Uninstall_Init checks the value of the key and act accordingly (just as an example, if that’s an “uninstall” then remove some files that are no longer used)

 

HOWEVER… smile_confused this approach had a problem… what happens if user answers “Cancel” to the prompt “The previous version of… Select Ok to continue or cancel to quit”? Nobody can restore [HKLM\UpgradeKey]Upgrade to 0 after that Install_Init set it to 1, and future possible “Uninstalls” are considered as “Upgrades”! So basically the problem is when user firstly doesn't accept to uninstall the previous version during upgrade and then secondly she uninstalls the previous version on her own: when doing this second action, the uninstall procedure would find that the Upgrade registry key is set to 1 and therefore would consider an upgrade even if in reality it's an uninstall.

So, next question was: is there any programmatic way to know if user selects “Cancel” when prompted about uninstalling previous version? The only way I could think at was to get ahold of the WCELOAD.EXE process and invoke GetExitCodeProcess() API to retrieve its return value: the assumption was that it was different when user hits “Cancel”… it turned out that this is true, but this approach involved an external application to be launched for example in setup.dll’s DLL_PROCESS_ATTACH, that can monitor WCELOAD.EXE and check its return value during Uninstall phase… Why an external process? Because the prompt comes up EVEN BEFORE the setup.dll can handle Install_Init.

The “undocumented but empirically retrievable” info I was mentioning at the beginning is precisely the return value of WCELOAD.EXE when user hits Cancel. As I said, not being documented it may change on future releases without any notice..

And now some code please!!

I’m talking about the following in setup.dll:

 #define DELETE_STR(s) \
 if (NULL != s) \
 delete [] s;
  
  
 HINSTANCE g_hinstModule;
  
 BOOL APIENTRY DllMain(
     HANDLE hModule, 
     DWORD  ul_reason_for_call, 
     LPVOID lpReserved
     )
 {
     //MessageBox(NULL, TEXT("Now attach the debugger"), TEXT("Test"), MB_OK);
  
     switch (ul_reason_for_call)
     {
         case DLL_THREAD_ATTACH:
         case DLL_THREAD_DETACH:
         case DLL_PROCESS_DETACH:
             g_hinstModule = (HINSTANCE)hModule;
             break;
  
         case DLL_PROCESS_ATTACH:
               g_hinstModule = (HINSTANCE)hModule;
  
               //check if UpgCheck.exe is already available on device (1st time it won't, but in any case we don't need it)
               LPCWSTR pszFileNameWithPath = new TCHAR[MAX_PATH];
               pszFileNameWithPath = TEXT("\\Windows\\UpgCheck.exe");
               WIN32_FIND_DATA wfdFindFileData;
               HANDLE hFile = FindFirstFile(pszFileNameWithPath, &wfdFindFileData);
               if(hFile == INVALID_HANDLE_VALUE)
               {
                             DELETE_STR(pszFileNameWithPath);
                             break;
               }
               FindClose(hFile);
  
               //Launch external process that will monitor wceload.exe
               BOOL bRet;
               SHELLEXECUTEINFO sei = {0};
  
               sei.cbSize = sizeof(sei);
               sei.nShow = SW_SHOWNORMAL; 
               sei.lpFile = pszFileNameWithPath;
               sei.lpParameters = TEXT(" ");
               bRet = ShellExecuteEx(&sei);
  
               //if (!bRet)
               //     MessageBox(NULL, TEXT("Could not launch UpgCheck"), TEXT("Test"), MB_OK);
  
               DELETE_STR(pszFileNameWithPath);
               break;
     }
  
 return TRUE;
 }
  

And I’m talking about something similar in the wceload-monitor:

 int _tmain(int argc, _TCHAR* argv[])
 {
        int const MAXBUF = 32;
        HRESULT hr = E_FAIL;
        HANDLE hProcess = NULL;
        BOOL bRes = FALSE;
        DWORD dwRes = 0;
  
        LPTSTR lpBuf = new TCHAR[MAXBUF];
        ZeroMemory(lpBuf, MAXBUF - 1);
  
        //retrieve process handle of wceload.exe, until it's found
        do{
               hr = GetProcessHandleByName(TEXT("wceload.exe"), &hProcess);
               CHR(hr);
               Sleep(1000);
        } while (INVALID_HANDLE_VALUE == hProcess);
  
        //hr = LogToFile(TEXT("\r\nwceload found!\r\n"), g_pszFilename);
        //CHR(hr);
  
        //retrieve wceload.exe exit code, until it exits
        do {
               Sleep(1000);
               bRes = GetExitCodeProcess(hProcess, &dwRes);
  
               if ( !bRes )
               {
                      goto Exit; //GetLastError
               }
        } while (STILL_ACTIVE == dwRes); 
        
        hr = StringCchPrintf(lpBuf, 
               LocalSize(lpBuf) / sizeof(TCHAR),
               TEXT("ExitCode %d"),
               dwRes); //2147754005 when user select Cancel (0x80042015)
        CHR(hr);
  
        hr = LogToFile(lpBuf, g_pszFilename);
        CHR(hr);      
  
        //success
        hr = S_OK;
  
 Exit:
        DELETE_STR(lpBuf);
  
        return 0;
 }

 

Where the helper functions are:

 // **************************************************************************
 // Function Name: GetProcessHandleByName
 HRESULT GetProcessHandleByName (LPCTSTR pszProcessName, LPHANDLE phProcessHandle)
 {
        HRESULT hr = E_FAIL;
  
        if (pszProcessName == NULL)
               goto Exit;
  
        HANDLE hSnapshot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
        if (hSnapshot == INVALID_HANDLE_VALUE)
               goto Exit;
  
        *phProcessHandle = NULL;
        PROCESSENTRY32 pe;
        pe.dwSize = sizeof(pe);
  
        if (Process32First(hSnapshot, &pe))
        {
               do {
                      //log Exe name
                      hr = LogToFile(pe.szExeFile, g_pszFilename);
                      CHR(hr);
                      hr = LogToFile(TEXT("\r\n"), g_pszFilename);
                      CHR(hr);
                      
                      //compare current Exe name with passed Process Name
                      if (lstrcmpi(pszProcessName, pe.szExeFile) == 0)
                      {
                            //get the handle of the Exe name in case we reached the Exe we were looking for
                            *phProcessHandle = OpenProcess(PROCESS_ALL_ACCESS, FALSE, pe.th32ProcessID);
  
                            CloseHandle(hSnapshot);
                            return TRUE;
                      }
               } while (Process32Next(hSnapshot, &pe));
        }
  
        //Success
        hr = S_OK;
  
 Exit:
        if (NULL != hSnapshot)
               //UPDATE: thanks Vino!
               //Contrarily to desktop Win32, don't invoke CloseHandle() to close the snapshot call.
               //Desktop (https://msdn.microsoft.com/en-us/library/ms682489(VS.85).aspx): 
               //       "[...] To destroy the snapshot, use the CloseHandle function.".
               //Windows CE\Mobile (https://msdn.microsoft.com/en-us/library/aa911386.aspx): 
               //       "[...] To close a snapshot, call the CloseToolhelp32Snapshot function."
               CloseToolhelp32Snapshot(hSnapshot);
  
        return hr;
 }
  
  
 // **************************************************************************
 // Function Name: LogToFile 
 HRESULT LogToFile(LPTSTR szLog, LPCTSTR pszFilename)
 {
        HRESULT hr = E_FAIL;
        
        //Open the handle to the file (and create it if it doesn't exist
        HANDLE hFile = CreateFile(pszFilename, GENERIC_WRITE, FILE_SHARE_READ, NULL, OPEN_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL);
        if (INVALID_HANDLE_VALUE == hFile)
               goto Exit;
  
        //Set the pointer at the end so that we can append szLog
        DWORD dwFilePointer = SetFilePointer(hFile, 0, NULL, FILE_END);
        if (0xFFFFFFFF == dwFilePointer)
               goto Exit;
  
        //Write to the file
        DWORD dwBytesWritten = 0;
        BOOL bWriteFileRet = WriteFile(hFile, szLog, wcslen(szLog) * 2, &dwBytesWritten, NULL);
        if (!bWriteFileRet)
               goto Exit;
  
        //Flush the buffer
        BOOL bFlushFileBuffersRet = FlushFileBuffers(hFile);
        if (!bFlushFileBuffersRet)
               goto Exit;
  
        //Success
        hr = S_OK;
  
 Exit:
        if (NULL != hFile)
               CloseHandle(hFile);
  
        return hr;
 }

 

Hope this can help someone that absolutely has to distinguish if the application needs to be uninstalled or upgraded… but maybe the code above can find other meaningful usage! smile_nerd

 

Cheers,

~raffaele