Search code examples
.net-corewixbootstrapperburnwix4

WiX v4 bootstrapper process remains alive after installation ends


I have a WiX v4 managed BA that has many redistributables as prerequisites. The installation completes and everything is installed successfully, but frequently the 3 bootstrapper processes never terminate even after the UI is closed and Engine.Quit(0) is called. The redistributable installers exit successfully and there are no issues in their logs, so I assume the issue is coming from my BA.

In my bundle I have the redistributables included as follows:

<ExePackage 
    bal:PrereqPackage="yes" 
    Id="NetFramework48" 
    PerMachine="yes" 
    DetectCondition="NETFRAMEWORK45 &gt;= 528040" 
    Vital="yes" 
    Permanent="yes" 
    Protocol="netfx4" 
    LogPathVariable="NetFxInstallLog" 
    Compressed="no" 
    SourceFile="$(var.SolutionDir)Resources\Redistributables\ndp48-x86-x64-allos-enu.exe" 
    Name="redist\ndp48-x86-x64-allos-enu.exe" 
    InstallArguments="/q /norestart /ChainingPackage &quot;[WixBundleName]&quot; /log &quot;[NetFxInstallLog].html&quot;" 
    RepairArguments="/q /norestart /repair /ChainingPackage &quot;[WixBundleName]&quot; /log &quot;[NetFxInstallLog].html&quot;" 
    UninstallArguments="/uninstall /q /norestart /ChainingPackage &quot;[WixBundleName]&quot; /log &quot;[NetFxInstallLog].html&quot;" />

<ExePackage 
    Id="DotNET6DesktopRuntimeX86" 
    PerMachine="yes" 
    DetectCondition="DotNET6DesktopRuntimex86Installed" 
    Vital="yes" 
    Permanent="yes" 
    Compressed="no" 
    SourceFile="$(var.SolutionDir)Resources\Redistributables\DotNET6\windowsdesktop-runtime-6.0.11-win-x86.exe" 
    Name="redist\windowsdesktop-runtime-6.0.11-win-x86.exe" 
    InstallArguments="/install /quiet /norestart" 
    RepairArguments="/repair /quiet /norestart" 
    UninstallArguments="/uninstall /quiet /norestart">
    <ExitCode Behavior="success" Value="0" />
    <!-- Error Code 0x666 or 1638 means a newer version was detected -->
    <ExitCode Behavior="success" Value="1638" />
</ExePackage>

<ExePackage 
    Id="DotNET6DesktopRuntimeX64" 
    PerMachine="yes" 
    DetectCondition="DotNET6DesktopRuntimex64Installed" 
    Vital="yes" Permanent="yes" Compressed="no" 
    SourceFile="$(var.SolutionDir)Resources\Redistributables\DotNET6\windowsdesktop-runtime-6.0.11-win-x64.exe" 
    Name="redist\windowsdesktop-runtime-6.0.11-win-x64.exe" 
    InstallArguments="/install /quiet /norestart" 
    RepairArguments="/repair /quiet /norestart" 
    UninstallArguments="/uninstall /quiet /norestart">
    <ExitCode Behavior="success" Value="0" />
    <!-- Error Code 0x666 or 1638 means a newer version was detected -->
    <ExitCode Behavior="success" Value="1638" />
</ExePackage>

<ExePackage 
    Id="DotNET6AspCoreRuntimeX86" 
    PerMachine="yes" 
    DetectCondition="DotNET6ASPCoreRuntimex86Installed" 
    Vital="yes" 
    Permanent="yes" 
    Compressed="no" 
    SourceFile="$(var.SolutionDir)Resources\Redistributables\DotNET6\aspnetcore-runtime-6.0.11-win-x86.exe" 
    Name="redist\aspnetcore-runtime-6.0.11-win-x86.exe" 
    InstallArguments="/install /quiet /norestart" 
    RepairArguments="/repair /quiet /norestart" 
    UninstallArguments="/uninstall /quiet /norestart" />

<ExePackage
    Id="DotNET8DesktopRuntimeX86"
    InstallArguments="/install /quiet /norestart"
    RepairArguments="/repair /quiet /norestart"
    UninstallArguments="/uninstall /quiet /norestart"
    PerMachine="yes"
    DetectCondition="DotNET8DesktopRuntimex86Installed"
    Vital="yes"
    Permanent="yes"
    Compressed="no"
    SourceFile="$(var.SolutionDir)Resources\Redistributables\DotNET8\windowsdesktop-runtime-8.0.7-win-x86.exe"
    Name="redist\windowsdesktop-runtime-8.0.7-win-x86.exe">
    <ExitCode Behavior="success" Value="0"/>
    <!-- Error Code 0x666 or 1638 means a newer version was detected -->
    <ExitCode Behavior="success" Value="1638"/>
</ExePackage>

<ExePackage
    Id="DotNET8DesktopRuntimeX64"
    InstallArguments="/install /quiet /norestart"
    RepairArguments="/repair /quiet /norestart"
    UninstallArguments="/uninstall /quiet /norestart"
    PerMachine="yes"
    DetectCondition="DotNET8DesktopRuntimex64Installed"
    Vital="yes"
    Permanent="yes"
    Compressed="no"
    SourceFile="$(var.SolutionDir)Resources\Redistributables\DotNET8\windowsdesktop-runtime-8.0.7-win-x64.exe"
    Name="redist\windowsdesktop-runtime-8.0.7-win-x64.exe">
    <ExitCode Behavior="success" Value="0"/>
    <!-- Error Code 0x666 or 1638 means a newer version was detected -->
    <ExitCode Behavior="success" Value="1638"/>
</ExePackage>

<ExePackage
    Id="DotNET8AspCoreRuntimeX86"
    InstallArguments="/install /quiet /norestart"
    RepairArguments="/repair /quiet /norestart"
    UninstallArguments="/uninstall /quiet /norestart"
    PerMachine="yes"
    DetectCondition="DotNET8ASPCoreRuntimex86Installed"
    Vital="yes"
    Permanent="yes"
    Compressed="no"
    SourceFile="$(var.SolutionDir)Resources\Redistributables\DotNET8\aspnetcore-runtime-8.0.7-win-x86.exe"
    Name="redist\aspnetcore-runtime-8.0.7-win-x86.exe"/>

<ExePackage 
    Id="VisualCPlusPlus14Runtime" 
    PerMachine="yes" 
    DetectCondition="VC14RuntimeInstalled &gt; v0.0.0.0" 
    Vital="yes" 
    Permanent="yes" 
    Compressed="no" 
    SourceFile="$(var.SolutionDir)Resources\Redistributables\Visual C++\VC++2015-2022_x86.exe" 
    Name="redist\VC++2015-2022_x86.exe" 
    InstallArguments="/install /quiet /norestart" 
    RepairArguments="/repair /quiet /norestart" 
    UninstallArguments="/uninstall /quiet /norestart" />

<ExePackage 
    Id="DirectX9C" 
    PerMachine="yes" 
    DetectCondition="DirectX9cInstalled" 
    Vital="yes" 
    Permanent="yes" 
    Compressed="no" 
    SourceFile="$(var.SolutionDir)Resources\Redistributables\Directx90c\DXSETUP.exe" 
    Name="redist\Directx90c\DXSETUP.exe" 
    InstallArguments="/silent">
    <PayloadGroupRef Id="DirectX9cPayloadGroup" />
</ExePackage>

<ExePackage 
    Id="MicrosoftAccess2010DatabaseEngine" 
    PerMachine="yes" 
    DetectCondition="MSAccess2010DbEngineInstalled" 
    Vital="yes" 
    Permanent="yes" 
    Compressed="no" 
    SourceFile="$(var.SolutionDir)Resources\Redistributables\AccessDatabaseEngine.exe" 
    Name="redist\AccessDatabaseEngine.exe" 
    InstallArguments="/quiet /norestart" />

<MsiPackage 
    Id="Powershell" 
    InstallCondition="NOT PowershellInstalled" 
    Vital="yes" 
    Permanent="yes" 
    Compressed="no" 
    SourceFile="$(var.SolutionDir)Resources\Redistributables\Powershell\PowerShell-7.3.11-win-x86.msi" 
    Name="redist\PowerShell-7.3.11-win-x86.msi" />

And my BA class is:

using System.Windows.Threading;
using WixToolset.Mba.Core;

namespace Bootstrapper
{
    /// <summary>
    /// The custom bootstrapper application.
    /// </summary>
    public class CustomBA : BootstrapperApplication, ICustomBA
    {
#nullable enable

        public CustomBA(IEngine engine, IBootstrapperCommand command) : base(engine)
        {
            Engine = engine;
            Command = command;
        }

        /// <inheritdoc/>
        public IEngine Engine { get; set; }

        /// <inheritdoc/>
        public IBootstrapperCommand Command { get; set; }

        /// <summary>
        /// The global dispatcher.
        /// </summary>
        static public Dispatcher? BootstrapperDispatcher { get; private set; }

        /// <summary>
        /// The entry point for the custom UI.
        /// </summary>
        protected override void Run()
        {

            this.Engine.Log(LogLevel.Standard, "Launching custom bootstrapper UX");
            BootstrapperDispatcher = Dispatcher.CurrentDispatcher;

            MainWindowViewModel viewModel = new MainWindowViewModel(this);

            MainWindow view = new MainWindow();
            view.DataContext = viewModel;
            view.Closed += (sender, e) => BootstrapperDispatcher.InvokeShutdown();

            if (!viewModel.silent)
            {
                view.Show();
            }

            Dispatcher.Run();

            this.Engine.Quit(0);

        }
    }
}

From the logs it almost looks like the BA is hanging before it is able to execute the cleanup after Engine.Quit.

The bundle log ends with:

[098C:18D8][2024-07-31T13:40:42]i399: Apply complete, result: 0x0, restart: None, ba requested restart:  No
[098C:0B88][2024-07-31T13:45:23]i000: Dispatch complete, quitting engine. 
[098C:18D8][2024-07-31T13:45:23]i500: Shutting down, exit code: 0x0

Whereas the expected lines afterwords about

Cleanup begin.
Cleanup not required due to running Apply.
Cleanup complete, result: 0x0
All the variable ending values blah blah
Exit code: 0x0, restarting: No

Never appear.

Often (not always) if I install the NET Desktop Runtime redistributable myself BEFORE running the BA, the process terminates as expected and the cleanup is executed.

This happens on a fresh, completely new install. There are no pending reboot or file moves I can see. The BA is not encountering any obvious exceptions or giving any event viewer information.

Killing the process myself after Engine.Quit(0) using something like Process.GetCurrentProcess().Kill(); works, but is skipping any potential cleanup, shutdown events, and just feels gross in general.

This was not a problem with the same logic in v3, so either something with the upgrade to v4 is incorrect or v4 itself had a behavior change (some new asynchronous stuff?) regarding this that I cannot track down.

I can provide any additional log or code information upon request.

Any help or even left-field suggestions would be appreciated :)


Solution

  • The issue was being caused by passing a dummy handle to Engine.Apply.

     private void OnPlanComplete(object sender, PlanCompleteEventArgs e)
     {
         Bootstrapper.Engine.Log(LogLevel.Standard, $"Plan complete with status: {e.Status}");
    
         if (e.Status >= 0)
         {
             // Pass in a dummy handle since as of WiX v4, Apply no longer supports a handle of zero.
             // This handle is used for parenting Burn child UIs to the BA window, not needed for us.
             Bootstrapper.Engine.Apply(new System.Windows.Forms.Form().Handle);
         }
     }
    

    Without a real handle, the engine (sometimes?????) seems to be unable to clean up after itself when quitting, even if the install completes fine.

    Getting the actual handle from the View using new WindowInteropHelper(this).EnsureHandle(); and passing that to Apply instead like so:

    private void OnPlanComplete(object sender, PlanCompleteEventArgs e)
    {
        Bootstrapper.Engine.Log(LogLevel.Standard, $"Plan complete with status: {e.Status}");
    
        if (e.Status >= 0)
        {
            // Use real window handle from the view.
            Bootstrapper.Engine.Log(LogLevel.Standard, $"Applying with window handle: {Bootstrapper.Handle}");
            Bootstrapper.Engine.Apply(Bootstrapper.Handle);
        }
    }
    

    Fixed the issue for me.