Search code examples
wixwindows-serviceswindows-installerwix3.10

Conditional stop/start of services


Wix 3.10.3, Installer is a custom bootstrapper wrapping an msi. I have implemented Install/Uninstall and Modify functionality.

The MSI installs a couple of services, at least one of which interfaces with the PCI drivers - when I add or remove the PCI components, I need to stop and restart the services which interface with said components, in order to prevent the dreaded "must reboot" scenario.

I found that I could add a ServiceControl element to the component which installed the driver, but, it would also start the services when Uninstalling the app.

I read some mentions of "shared component", albeit without much documentation about it, which suggested that would be the way to go in these kinda of cases.

<Fragment>
    <Component Id="PCIDriver_SvcCtl" Directory="INSTALLDIR" Guid="{5EAB2128-228D-44BE-950F-5F258B94BFED}" Win64="$(var.Win64)" >
        <CreateFolder/>
        <!-- Any action but uninstall. -->
        <Condition>NOT REMOVE~="ALL"</Condition>
        <ServiceControl Id="Service1_SvcCtl" Name="service1" Start="both" Stop="both" Wait="no" />
        <ServiceControl Id="Service2_SvcCtl" Name="service2" Start="both" Stop="both" Wait="no" />
    </Component>
</Fragment>

Then, in my Product.wxs, for the feature that requires this (which installs the PCI drivers), I added:

<ComponentRef Id="PCIDriver_SvcCtl" />

Probably has zero to do with my issue, but, related to restart manager, I have the following Restart Manager property set in my Product.wxs:

<Property Id="MSIRMSHUTDOWN" Value="1" />

So, my component runs, stopping then starting the services, when I add/remove the PCI drivers via Modify, but, it also runs when I Uninstall the entire app. As there are two services, when they are removed and this is called to start them, it adds two minutes to the uninstall (as the restart is attempted twice, with a 30s wait time for each).

What condition do I need to set to avoid calling this component on MSI uninstallation, yet allow it to run during Modify? Or, do I need to author it in a different fashion? Thanks!


Solution

  • I gave up trying to solve it the "wix way" and fell back to custom actions.

    Modifications to the Custom Action C# code to add two methods to Stop and Start a named service:

    using System.ServiceProcess;
    
    
    /**************************************************************************\
    *
    * Function:  StopService
    *
    * Purpose:  Stops the named service.
    *
    ***************************************************************************/
    [CustomAction]
    public static ActionResult StopService(Session session)
    {
        session.Log("Begin StopService Custom Action");
    
        string serviceName = session.CustomActionData["SERVICENAME"];
    
        try
        {
            if (!string.IsNullOrEmpty(serviceName))
            {
                ServiceController sc = new ServiceController(serviceName);
    
                // Start the service if the current status is any state other than running or start pending.
                if (!(sc.Status.Equals(ServiceControllerStatus.Stopped))
                 && !(sc.Status.Equals(ServiceControllerStatus.StopPending)))
                {
                    sc.Stop();
                }
            }
        }
        catch (Exception ex)
        {
            session.Log("Failed to stop service " + serviceName + " : " + ex.ToString());
            return ActionResult.Failure;
        }
        finally
        {
            session.Log("End StopService Custom Action");
        }
    
        return ActionResult.Success;
    }
    
    /**************************************************************************\
    *
    * Function:  StartService
    *
    * Purpose:  Starts the named service.
    *
    ***************************************************************************/
    [CustomAction]
    public static ActionResult StartService(Session session)
    {
        session.Log("Begin StartService Custom Action");
    
        string serviceName = session.CustomActionData["SERVICENAME"];
    
        try
        {
            if (!string.IsNullOrEmpty(serviceName))
            {
                ServiceController sc = new ServiceController(serviceName);
    
                // Start the service if the current status is any state other than running or start pending.
                if (!(sc.Status.Equals(ServiceControllerStatus.Running))
                 && !(sc.Status.Equals(ServiceControllerStatus.StartPending)))
                {
                    sc.Start();
                }
            }
        }
        catch (Exception ex)
        {
            session.Log("Failed to control service " + serviceName + " : " + ex.ToString());
            return ActionResult.Failure;
        }
        finally
        {
            session.Log("End StartService Custom Action");
        }
    
        return ActionResult.Success;
    }
    

    Next, in my Product.wxs, custom action definitions (two services, each to be stopped and started, plus one to set the service name to be acted upon):

    <!-- These next CA's relate to stopping and starting key services prior to Modifying the PCI feature state. -->
    <CustomAction Id="CAL_StopService_Service1.SetProperty" Property="CAL_StopService_Service1"
                  Value="SERVICENAME=$(var.Service1_Service_Name)" />
    <CustomAction Id="CAL_StopService_Service1" DllEntry="StopService" BinaryKey="CAL_dll" Execute="deferred" Return="ignore" />
    
    <CustomAction Id="CAL_StopService_Service2.SetProperty" Property="CAL_StopService_Service2"
                  Value="SERVICENAME=$(var.Service2_Service_Name)" />
    <CustomAction Id="CAL_StopService_Service2" DllEntry="StopService" BinaryKey="CAL_dll" Execute="deferred" Return="ignore" />
    
    <CustomAction Id="CAL_StartService_Service1.SetProperty" Property="CAL_StartService_Service1"
                  Value="SERVICENAME=$(var.Service1_Service_Name)" />
    <CustomAction Id="CAL_StartService_Service1" DllEntry="StartService" BinaryKey="CAL_dll" Execute="deferred" Return="ignore" />
    
    <CustomAction Id="CAL_StartService_Service2.SetProperty" Property="CAL_StartService_Service2"
                  Value="SERVICENAME=$(var.Service2_Service_Name)" />
    <CustomAction Id="CAL_StartService_Service2" DllEntry="StartService" BinaryKey="CAL_dll" Execute="deferred" Return="ignore" />
    

    Finally, the sequencing of the custom actions in InstallExecuteSequence. The conditions for Stopping are: state and action are changing for the PCI feature and the service component itself is installed. For starting, the same basic check as for stopping, plus, a check to ensure we're not uninstalling the app.

    <!-- Manage services affected by PCI feature. -->
    <Custom Action="CAL_StopService_Service1.SetProperty" Before="CAL_StopService_Service1" />
    <Custom Action="CAL_StopService_Service2.SetProperty" Before="CAL_StopService_Service2" />
    <Custom Action="CAL_StartService_Service1.SetProperty" Before="CAL_StartService_Service1" />
    <Custom Action="CAL_StartService_Service2.SetProperty" Before="CAL_StartService_Service2" />
    
    <Custom Action="CAL_StopService_Service1" Before="StopServices">
        <![CDATA[(&PCI <> !PCI) AND (?Service1_service = 3)]]>
    </Custom>
    
    <Custom Action="CAL_StopService_Service2" Before="StopServices">
        <![CDATA[(&PCI <> !PCI) AND (?Service2_service = 3)]]>
    </Custom>
    
    <Custom Action="CAL_StartService_Service1" After="StartServices">
        <![CDATA[((&PCI <> !PCI) AND (?Service1_service = 3)) AND NOT REMOVE~="ALL"]]>
    </Custom>
    
    <Custom Action="CAL_StartService_Service2" After="StartServices">
        <![CDATA[((&PCI <> !PCI) AND (?Service2_service = 3)) AND NOT REMOVE~="ALL"]]>
    </Custom>
    

    Solves the issue at hand. Critique? Suggestions to improve?