ManuHArt
ManuHArt

Reputation: 13

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?

Upvotes: 0

Views: 42

Answers (2)

ManuHArt
ManuHArt

Reputation: 13

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.

Upvotes: 0

Christopher Painter
Christopher Painter

Reputation: 55581

You can put a service control element in for a service you didn't install and if it's found MSI will handle that also. So if you author ServiceControl for service B in installer A it should give you what you are looking for.

Also, if service A is running elevated it could have code in it that spins up service B as needed. This would cover scenarios where B was down and it wasn't install time.

Upvotes: 0

Related Questions