wixwix4

How to restart dependent Windows service in case of a rollback?


I have a WiX 4 bundle that installs multiple MSIs (also authored using WiX 4), lets name them MsiA, MsiB and MsiC. All components are under my control. The updates in all MSIs are authored as MajorUpgrade with Schedule="afterInstallInitialize".

  1. MsiA installs a Windows service (MsiAService).
  2. MsiB also installs a Windows service (MsiBService) which depends on MsiAService.
  3. MsiC doesn't contain a service.

This order is also reflected in the Chain element of my bundle. All services are configured using ServiceInstall, ServiceDependency and ServiceControl elements.

Authoring of MsiA:

                   <ServiceInstall Id="S.MsiA.Service"
                                      Name="MsiAService"
                                      DisplayName="MSI A service"
                                      Description="MSI A service"
                                      Type="ownProcess"
                                      Start="auto"
                                      Account="LocalSystem"
                                      ErrorControl="normal"                                      
                                      Vital="yes">
                  </ServiceInstall>
                  <ServiceControl Id="S.MsiA.Service"
                                  Start="install"
                                  Stop="both"
                                  Remove="both"
                                  Name="MsiAService"
                                  Wait="yes" />
                  <util:ServiceConfig ServiceName="MsiAService"
                                      FirstFailureActionType="none"
                                      SecondFailureActionType="none"
                                      ThirdFailureActionType="none" />

Authoring of MsiB (which has the service dependency):

               <ServiceInstall Id="S.MsiB.Service" Name="MsiBService"
                                DisplayName="MSI B Service"
                                Description="MSI B Service"
                                Type="ownProcess" Start="auto" Account="LocalSystem"
                                ErrorControl="normal" Vital="yes">                    
                    <ServiceDependency Id="MsiAService"/>
                </ServiceInstall>               
                <ServiceControl Id="S.MsiB.Service"
                                Start="install"
                                Stop="both"
                                Remove="both"
                                Name="MsiBService"
                                Wait="yes" />               
                <util:ServiceConfig ServiceName="MsiBService"
                                    FirstFailureActionType="none"
                                    SecondFailureActionType="none"
                                    ThirdFailureActionType="none" />

Now, if I upgrade the bundle, and MsiB fails, the rollback takes place, which is desired.
But in this process, the following happens:

  1. MsiB is rolled back and starts it's MsiBService.
  2. The bundle of the previous version is started and re-installs MsiA: It stops it's MsiAService and since MsiBService dependends on it, it also gets shut down.
    After it's rollback, it starts its MsiAService.
  3. -> issue: the dependent service MsiBService stays stopped.

I looked up the service control elements in the WiX 4 documentation but couldn't find a solution to this problem.
Also in the Google results I couldn't find a solution.

I thought about using a CustomAction that restarts the service (via "net start") but I don't know how to formulate the condition for this use case and to which MSI I should add it.

Did anyone stumble across this problem and how did you solve this?
Or is there a built-in solution in WiX 4?


Solution

  • I ended up writing a custom action in MsiA that starts the service B.

    This got a bit more elaborated than "net start" since I wait for the service to be in a proper state before issuing a SCM command.

            /// <summary>
            /// Executes an SCM command on a Windows service.
            /// </summary>
            [CustomAction]
            public static ActionResult ExecuteServiceCommand(Session session)
            {
                if (!session.CustomActionData.TryGetValue("ServiceName", out var serviceName))
                {                
                    return ActionResult.Failure;
                }
    
                if (!session.CustomActionData.TryGetValue("Command", out var serviceCommand))
                {
                    return ActionResult.Failure;
                }
    
                ServiceControllerStatus controllerStatus;
                switch (serviceCommand.ToLower())
                {
                    case "start":
                        controllerStatus = ServiceControllerStatus.Running;
                        break;
                    case "stop":
                        controllerStatus = ServiceControllerStatus.Stopped;
                        break;
                    default:
                        return ActionResult.Failure;
                }
                
                using var serviceController = new ServiceController(serviceName);
    
                if (!IsServiceExisting(serviceController))
                {                
                    return ActionResult.Failure;
                }
    
                if (WaitForStatus(serviceController, session, nameof(ExecuteServiceCommand)) != ActionResult.Success)
                {
                    return ActionResult.Failure;
                }
    
                switch (controllerStatus)
                {
                    case ServiceControllerStatus.Running:
                        serviceController.Start();
                        break;
                    case ServiceControllerStatus.Stopped:
                        serviceController.Stop();
                        break;
                }
    
                serviceController.WaitForStatus(controllerStatus, maxTimeout);
    
                return ActionResult.Success;
            }
    
            /// <summary>
            /// Waits for service status.
            /// </summary>
            [CustomAction]
            public static ActionResult WaitForServiceStatus(Session session)
            {
                if (!session.CustomActionData.TryGetValue("ServiceName", out var serviceName))
                {                
                    return ActionResult.Failure;
                }
                
                using var serviceController = new ServiceController(serviceName);
    
                if (!IsServiceExisting(serviceController))
                {                
                    return ActionResult.Success;
                }
    
                return WaitForStatus(serviceController, session, nameof(WaitForServiceStatus));
            }
    
            private static Boolean IsServiceExisting(ServiceController controller)
            {
                try
                {
                    if (controller == null)
                    {
                        return false;
                    }
    
                    return !controller.ServiceHandle.IsInvalid;
                }
                catch (InvalidOperationException e) when (e.InnerException is Win32Exception)
                {
                    return false;
                }
            }
    
            private static ActionResult WaitForStatus(ServiceController controller, Session session, String loggingMember)
            {
                // the following implementation is taken from ServiceController.WaitForStatus
                var utcNow = DateTime.UtcNow;
    
                while (controller.Status != ServiceControllerStatus.Stopped && controller.Status != ServiceControllerStatus.Running)
                {
                    if (DateTime.UtcNow - utcNow > maxTimeout)
                    {                    
                        return ActionResult.Failure;
                    }
    
                    Thread.Sleep(250);
                    controller.Refresh();
                }
    
                return ActionResult.Success;
            }
    

    For the condition used, see the linked question.