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"
.
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:
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?
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.