Search code examples
wixwindows-installerwix3.11

Implement dependency of ExePackage on other MSI so uninstall completes if required MSI was already removed


Background:
I have a large project which consists of a server component, client component and microservice hosting component. Each is packed in its own MSI.
The server component and the microservice hosting component each install a Windows service. The server components' Windows service depends on the microservice hosting components' Windows service.

Everything is chained together and distributed by a setup bundle *.exe installer. In the installer, a customer can choose if he wants to install only the client, only the server or both (client and server).
At the end of the installation the customer is informed that everything went well (all files copied, all services running) (= success message) or not (= failure message with the possibility to send the log files to our help desk).

Updates are implemented as MajorUpgrades since the setup consists of hundreds of files. Our customers expect that if an update fails, the setup "rolls back" to the previously installed version of the product (= the whole bundle).

What lead to the problem?
Since Wix v3 does not handle rollbacks of bundles the way I expect it (see here) -- only the MSI that triggered the failure is rollbacked, previously installed MSIs in the chain before are only removed -- we had to implement a few workarounds:

  1. install the component, which most likely causes errors on a customer machine, first -- then install the other MSIs.
  2. deal with the Windows service dependencies of the components later (= restart the server component service)

Point 2 is achieved by using ExePackages that start and stop the servers' Windows service. This is done by using the technique described here. The service component MSI does not start the Windows service, only stops it on uninstall.
For architectural reasons, I do not want to configure the service components' Windows service in the microservice hosting service MSI.

This lead to the following Wix source code:
Bundle.wxs

       <Chain>
            <PackageGroupRef Id="SqlExpress"/>
            <PackageGroupRef Id="vcredist"/>
            <!-- ... -->
            <PackageGroupRef Id="ServiceHandlingStop" />
            <PackageGroupRef Id="Server"/>
            <PackageGroupRef Id="MicroServiceHost"/>
            <PackageGroupRef Id="ServiceHandlingStart" />
            <PackageGroupRef Id="Client"/>
        </Chain>

Fragment of the "service handling":
The decision of the user what to install is stored inside "InternalInstallType"

    <Fragment>
        <PackageGroup Id="ServiceHandlingStop">
            <ExePackage Id="ServiceHandlingStopCall"
                        Compressed="yes"
                        DisplayName="Service Management"
                        SourceFile="redist\Runner.bat"
                        InstallCommand="net stop WCFHostService"
UninstallCommand="net start WCFHostService"
                        DetectCondition="WixBundleInstalled = 1 AND InternalInstallType &lt;&gt; &quot;Client&quot;"
                        InstallCondition="InternalInstallType &lt;&gt; &quot;Client&quot;"
                        Permanent="no"
                        Vital="no"
                        PerMachine="yes">               
                <ExitCode Behavior="success" />
            </ExePackage>
        </PackageGroup>
        
        <PackageGroup Id="ServiceHandlingStart">
            <ExePackage Id="ServiceHandlingStartCall"
                        Compressed="yes"
                        DisplayName="Service Management"
                        SourceFile="redist\Runner.bat"
                        InstallCommand="net start WCFHostService"
                        UninstallCommand="net stop WCFHostService"
                        DetectCondition="WixBundleInstalled = 1 AND InternalInstallType &lt;&gt; &quot;Client&quot;"
                        InstallCondition="InternalInstallType &lt;&gt; &quot;Client&quot;"
                        Permanent="no"
                        Vital="yes"
                        PerMachine="yes">
                <ExitCode Value="0" Behavior="success" />
                <ExitCode Behavior="error" />
            </ExePackage>
        </PackageGroup>
    </Fragment>

What should be achieved by this bunch of code?
"Only if the server component is installed, start the server components' Windows service after the microservice hosting component is installed. On a failed upgrade, start the server components' Windows service after the previous version was installed again. If the user chooses to uninstall, don't care about the server components' Windows service."

The problem
If the setup bundle on uninstallation by a user is killed / closed unexpectedly, it can occur that some of the MSIs mentioned in the chain above are already installed.
Now if the user launches the setup again to uninstall once more, Burn runs the ExePackage ServiceHandlingStartCall again and since is marked as Vital=yes (to inform the user if the service startup succeeded on install, upgrade and rollback and abort if neccessary), the setup fails and the user is unable to remove the product.

Question:
How to implement the dependency between the ExePackage ServiceHandlingStartCall and the service component MSI so uninstallation is possible even though the other MSIs are already gone?

I tried

  • "InstallCondition" / "DetectCondition"
  • Dependency I don't find any examples / documentation

Or if someone has an idea of how to handle the dependency of server component and microservice hosting component, my ears are wide open.

Thank you!


Solution

  • Are you able to check whether the individual compoments (MSIs) are installed?

    It seems you're only checking if the entire bundle is installed or not (WixBundleInstalled = 1). Even though the InternalInstallType variable holds the "decision of the user what to install", that might not reflect the actual state of the system if the installer was previously interrupted, as you're describing.

    If you could make every component (MSI) mark if it is installed or not (in the registry, for example) you could then do a RegistrySearch in Wix and only try to uninstall the component if it was detected.

    I've implemented the same thing recently. This code check if the WebView2 Runtime is already installed, before attempting to install it:

    Bundle.wxs (Bootstrapper)

    <?xml version="1.0" encoding="UTF-8"?>
    
    <Wix xmlns="http://schemas.microsoft.com/wix/2006/wi"
         xmlns:bal="http://schemas.microsoft.com/wix/BalExtension"
         xmlns:util="http://schemas.microsoft.com/wix/UtilExtension">
      
        <Bundle ...>
    
            ... code removed for brevity
    
            <!-- Check if WebView2 Runtime already installed -->
            <util:RegistrySearch
                Root="HKLM"
                Key="SOFTWARE\WOW6432Node\Microsoft\EdgeUpdate\Clients\{F3017226-FE2A-4295-8BDF-00C3A9A7E4C5}"
                Value="pv"
                Variable="WV2RuntimeMachineVersion" />
            <util:RegistrySearch
                Root="HKCU"
                Key="SOFTWARE\Microsoft\EdgeUpdate\Clients\{F3017226-FE2A-4295-8BDF-00C3A9A7E4C5}"
                Value="pv"
                Variable="WV2RuntimeUserVersion" />
    
            <Chain>
            
                <!--This will only be installed when INSTALLING or REPAIRING and the WebView2 Runtime is not already installed-->
                <ExePackage Id="TheWebView2Runtime"
                    DisplayName="WebView2 Runtime"
                    Vital="no"
                    Cache="no"
                    Permanent="yes"
                    SourceFile="..\..\data\bundle\MicrosoftEdgeWebView2RuntimeInstallerX64.exe"
                    InstallCommand="/silent /install"
                    DetectCondition="NOT (WixBundleAction = 5 OR WixBundleAction = 7) OR (WV2RuntimeMachineVersion >= v102.0.1245.22 OR WV2RuntimeUserVersion >= v102.0.1245.22)" />
            
            </Chain>
        </Bundle>
    </Wix>
    

    Note that in my case, I don't want to uninstall the dependency (WebView2 Runtime) ever, but that could easily be changed by removing the NOT (WixBundleAction = 5 OR WixBundleAction = 7) of the DetectCondition.

    Do you think this approach might work for you?