다음을 통해 공유


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

Comments

  • Anonymous
    April 22, 2009
    PingBack from http://microsoft-sharepoint.simplynetdev.com/programmatically-discriminate-between-upgrade-or-uninstall-of-a-cab-on-windows-mobile/

  • Anonymous
    April 22, 2009
    Very interesting post. I have had a similar problem and maybe it's interesting to know that OpenProcess (you use this API in GetProcessHandleByName) could fail and return NULL on Smartphone with two-tier Lock or Prompt security policy. I was calling it with SYNCHRONIZE instead of PROCESS_ALL_ACCESS, but it should not make any difference, since these flags are not supported in Windows CE 5.x. My solution was to cast the process id pe.th32ProcessID to a handle , since process ID and process handle are the same in Window CE (I'm not sure it's documented, but it is true, at least up to CE 5): *phProcessHandle = (HANDLE)pe.th32ProcessID; This seems to work fine with the most common Smartphone security policies. Giuseppe

  • Anonymous
    April 23, 2009
    Grazie Giuseppe! :-)

  • Anonymous
    May 05, 2009
    In the sample code, in the function GetProcessHandleByName(), shouldn't CloseHandle(hSnapshot); actually be CloseToolhelp32Snapshot(hSnapshot); -Vino

  • Anonymous
    May 05, 2009
    Thanks for commenting! And thanks for the hint... I was used to the DESKTOP-way, where CloseHandle is fine: see http://msdn.microsoft.com/en-us/library/ms686701(VS.85).aspx, for example, or even in the CreateToolhelp32Snapshot doc (http://msdn.microsoft.com/en-us/library/ms682489(VS.85).aspx) it's reported "[...] To destroy the snapshot, use the CloseHandle function.". But you're totally correct: on Windows CE the story is different... see the doc page for CreateToolhelp32Snapshot (http://msdn.microsoft.com/en-us/library/aa911386.aspx) "[...] To close a snapshot, call the CloseToolhelp32Snapshot function. Do not call the CloseHandle function to close the snapshot call. That generates a memory leak. " So thanks again and please continue validating my code!! :-) ~raffaele

  • Anonymous
    May 06, 2009
    Apart from the code, I was more interested in getting rid of the message "“The previous version of… Select Ok to continue or cancel to quit”". We have an app packaged as a cab file. This cab has been created using the /nouninstall flag. This means the app will not appear in the 'Remove Applications' section. Also, we have our own uninstall logic built into the app. Also, we do not write into the registry whatsoever. Further, if we uninstall and the re-install the app, everything works fine with no prompts from wceload. This has worked fine with WM 6.0 Come WM 6.1, using the same cab exhibits different behavior. After uninstalling the app, and then installing it again, we get the above message. I select OK. Moving on, another prompt comes up saying, ' The app cannot be removed. Proceed?'. Select Yes, and the cab extracts successfully. But at the end of it, yet another prompt which says, 'The app.cab was not installed successfully.' All this prompts and our customers are very unhappy. My question is there anyway I can get rid of the prompts? I searched the web and found http://social.msdn.microsoft.com/Forums/en-US/vssmartdevicesvbcs/thread/835784af-def1-4928-af6f-e7d8d326aa5c But it did not help at all. I guess the issue is still open. Any help/advice/suggestion is appreciated.

  • Anonymous
    May 07, 2009
    hmm... I would say that your query deserves some tests and researches. Why not opening a call towards Microsoft Technical Support? If you're located in EMEA, we may end up talking each other... :-) Remember: "get what you paid for"!! (http://blogs.msdn.com/raffael/archive/2008/05/14/get-what-you-paid-for.aspx)

  • Anonymous
    May 15, 2009
    The comment has been removed

  • Anonymous
    May 19, 2009
    Hi Victor, I would say you were using a unsupported way to remove entries under the "Remove Programs" list (removing registry keys), contrarily to using the UnInstall CSP, for example. Anyway, you may find interesting a freeware tool from SK Tools called "ssnap", which takes a snap-shot of the state of PPC that is useful for monitoring of changes on PPC, e.g. those made during installation of programs (http://www.s-k-tools.com/index.html?m_util.html). I've never used it but can be useful if you want to pursue with this approach... HTH, bye! ~raffaele

  • Anonymous
    May 31, 2009
    I used the MemMaid tool and I found out that after an installation, the application entry is made into the mxip_swmgmt.vol. Is there any way to get rid of this entry especially considering that the cab was created using the /nouninstall flag ? Here is the relevant post with what I have tried - http://forum.xda-developers.com/showthread.php?p=3769269

  • Anonymous
    June 28, 2009
    The comment has been removed

  • Anonymous
    August 17, 2009
    Thank you very much for providing a flow on how the CAB installation process works. I had found out before, but that was by trial and error, so this is a great post that I wished I had read 6 months ago. :)

  • Anonymous
    June 27, 2011
    hmm, am I missing something or API has been changed since, but... SETUP_API codeINSTALL_INIT Install_Init(    HWND        hwndParent,    BOOL        firstCall,     // is this the first time this function is being called?    BOOL        previouslyInstalled,    LPCTSTR     installDir ) ... has "BOOL previouslyInstalled" parameter

  • Anonymous
    September 03, 2013
    Hello, I found this post very helpful, as I need to do the same exact thing that is described here on a Windows CE 5 Device. The steps described during an update confirms what was scaring me on doing an update without bothering on what happens if something goes bad during the installation. In fact, what can happen is that the update starts, first uninstalls the old app and then starts the installation of the new one. If something goes bad at this point, what can happen is that the device will be without the app. So the idea was that in the setup.dll at Install Init, I start to backup all the files and reg settings that the new cab is going to modify, but to do so I want to discover the list of operations at runtime. I found that in the CAB there is a _setup.xml file that describes all the operations that the CAB itself is going to do during the installation. So I was thinking of creating a setup.dll that loads that _setup.xml file when the Installation starts and discovers at runtime which files and reg settings the cab will modify, stores those files in some persistent memory and eventually it restores those files if something bad happens during the installation. The point on doing this is that I can't find any idea on how to get the _setup.xml file itself and its content that is contained in the cab. Any info on that? Thanks Stefano