Compare commits

...

2 Commits

Author SHA1 Message Date
vanzue
df3e3cec1b Dev 2025-07-10 17:57:48 +08:00
vanzue
ec655f7dfd try luck 2025-07-07 12:00:29 +08:00
53 changed files with 6460 additions and 125 deletions

View File

@@ -110,6 +110,8 @@
<PackageVersion Include="WixToolset.UI.wixext" Version="5.0.2" />
<PackageVersion Include="WixToolset.NetFx.wixext" Version="5.0.2" />
<PackageVersion Include="WixToolset.Bal.wixext" Version="5.0.2" />
<PackageVersion Include="WixToolset.BootstrapperApplicationApi" Version="5.0.2" />
<PackageVersion Include="WixToolset.Dtf.WindowsInstaller" Version="5.0.2" />
<PackageVersion Include="FireGiant.HeatWave.BuildTools.wixext" Version="5.0.2" />
</ItemGroup>
<ItemGroup Condition="'$(IsExperimentationLive)'!=''">

View File

@@ -0,0 +1,10 @@
using System.Windows;
[assembly: ThemeInfo(
ResourceDictionaryLocation.None, //where theme specific resource dictionaries are located
//(used if a resource is not found in the page,
// or application resource dictionaries)
ResourceDictionaryLocation.SourceAssembly //where the generic resource dictionary is located
//(used if a resource is not found in the page,
// app, or any theme specific resource dictionaries)
)]

Binary file not shown.

After

Width:  |  Height:  |  Size: 287 KiB

View File

@@ -0,0 +1,38 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>WinExe</OutputType>
<TargetFramework>net9.0-windows10.0.26100.0</TargetFramework>
<Platforms>x64</Platforms>
<ImplicitUsings>disable</ImplicitUsings>
<UseWPF>true</UseWPF>
<UseWindowsForms>true</UseWindowsForms>
<SelfContained>true</SelfContained>
<RuntimeIdentifier>win-x64</RuntimeIdentifier>
<ApplicationIcon>Assets\Icon.ico</ApplicationIcon>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|x64'">
<DebugType>none</DebugType>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">
<DebugType>embedded</DebugType>
</PropertyGroup>
<ItemGroup>
<Resource Include="Assets\Icon.ico" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="WixToolset.BootstrapperApplicationApi" />
</ItemGroup>
<ItemGroup>
<Page Update="Views\ShellView.xaml">
<SubType>Designer</SubType>
<Generator>MSBuild:Compile</Generator>
</Page>
</ItemGroup>
</Project>

View File

@@ -0,0 +1,62 @@
using Bootstrapper.Models;
using Bootstrapper.Models.Util;
using System;
using WixToolset.BootstrapperApplicationApi;
namespace Bootstrapper;
internal class BootstrapperApp : BootstrapperApplication
{
private Model _model;
public int ExitCode { get; private set; }
protected override void OnCreate(CreateEventArgs args)
{
base.OnCreate(args);
try
{
var factory = new WpfBaFactory();
_model = factory.Create(this, args.Engine, args.Command);
}
catch (Exception ex)
{
ExitCode = ErrorHelper.HResultToWin32(ex.HResult);
args.Engine.Log(LogLevel.Error, ex.ToString());
throw;
}
}
protected override void Run()
{
var hResult = 0;
try
{
_model.Log.Write("Running bootstrapper application.");
try
{
_model.UiFacade.Initialize(_model);
_model.Engine.Detect();
_model.UiFacade.RunMessageLoop();
}
finally
{
hResult = _model.State.PhaseResult;
}
}
catch (Exception ex)
{
hResult = ex.HResult;
_model.Log.Write(ex);
}
finally
{
// If the HRESULT is an error, convert it to a win32 error code
ExitCode = ErrorHelper.HResultToWin32(hResult);
_model.SaveEmbeddedLog(ExitCode);
_model.Engine.Quit(ExitCode);
}
}
}

View File

@@ -0,0 +1,215 @@
using Bootstrapper.Models.State;
using Bootstrapper.Models.Util;
using System;
using System.IO;
using System.Linq;
using System.Text;
using WixToolset.BootstrapperApplicationApi;
namespace Bootstrapper.Models;
internal class Log
{
private readonly IEngine _engine;
public Log(IEngine engine)
{
_engine = engine;
}
public void Write(string message, bool indent = false, LogLevel level = LogLevel.Verbose)
{
var txt = message;
if (indent)
txt = $"{new string(' ', 10)}{txt}";
_engine.Log(level, txt);
}
public void Write(Exception ex)
{
Write(ex.ToString(), false, LogLevel.Error);
}
public void RemoveEmbeddedLog()
{
try
{
// delete any previous embedded log
var fileName = EmbeddedLogFileName();
if (!string.IsNullOrEmpty(fileName) && File.Exists(fileName))
File.Delete(fileName);
}
catch
{
// ignore
}
}
public string EmbeddedLogFileName()
{
var folder = Path.GetDirectoryName(_engine.GetVariableString(Constants.BundleLogName));
if (string.IsNullOrEmpty(folder))
return string.Empty;
return Path.Combine(folder, $"{_engine.GetVariableString(Constants.BundleNameVariable)}_embedded.txt");
}
public void WriteLogFile(AppState state, string fileName)
{
try
{
if (string.IsNullOrEmpty(fileName))
return;
var log = Read(state);
if (string.IsNullOrEmpty(log))
throw new InvalidOperationException("Reading log produced an empty string");
var logSb = new StringBuilder();
if (fileName == EmbeddedLogFileName())
{
// indent embedded log
var logArr = log.Split('\n');
foreach (var line in logArr)
{
logSb.Append(new string(' ', 8));
logSb.AppendLine(line.Trim());
}
File.WriteAllText(fileName, logSb.ToString());
}
else
File.WriteAllText(fileName, log);
}
catch (Exception ex)
{
Write($"Unable to write to {fileName}");
Write(ex);
}
}
/// <summary>
/// Formats app state, bundle log and any package logs that have been written into a single log
/// </summary>
/// <exception cref="InstallerVariableNotFoundException">
/// Thrown when the log cannot be read because a variable is missing
/// </exception>
private string Read(AppState state)
{
if (!_engine.ContainsVariable(Constants.BundleLogName))
throw new InstallerVariableNotFoundException(Constants.BundleLogName);
var tocSb = new StringBuilder();
tocSb.AppendLine("Table of Contents");
// Caches log file text. Will be appended to TOC once all logs have been read.
var logSb = new StringBuilder();
var tocIndex = 1;
// Add state to TOC
tocSb.AppendLine($"{tocIndex}. State");
// Add state to log
logSb.AppendLine();
logSb.AppendLine();
logSb.AppendLine("1. State");
logSb.AppendLine(Constants.Line);
logSb.AppendLine(state.ToString());
// Add TOC entry for bundle
tocIndex++;
var bundleLogFile = Path.GetFullPath(_engine.GetVariableString(Constants.BundleLogName));
tocSb.AppendLine($"{tocIndex}. Bundle Log (original location = \"{bundleLogFile}\")");
// Add bundle log text
logSb.Append(ReadLogFile(bundleLogFile, "Bundle Log", tocIndex));
var packageIds = state.Bundle.Packages.Values.Select(i => i.Id).ToArray();
foreach (var packageId in packageIds)
{
try
{
var packageLogFile = PackageLogFile(packageId);
if (string.IsNullOrEmpty(packageLogFile))
continue;
// Add package to TOC
var logName = state.GetPackageName(packageId);
tocIndex++;
tocSb.AppendLine($"{tocIndex}. {logName} (original location = \"{packageLogFile}\")");
// Add package log
logSb.Append(ReadLogFile(packageLogFile, logName, tocIndex));
}
catch (Exception ex)
{
logSb.AppendLine();
logSb.AppendLine();
logSb.AppendLine(ex.ToString());
}
}
if (!string.IsNullOrEmpty(state.RelatedBundleId))
{
var fileName = EmbeddedLogFileName();
if (!string.IsNullOrEmpty(fileName) && File.Exists(fileName))
{
tocIndex++;
var productName = _engine.GetVariableString(Constants.BundleNameVariable);
tocSb.AppendLine($"{tocIndex}. {productName} bundle (original location = \"{fileName}\")");
logSb.Append(ReadLogFile(fileName, productName, tocIndex));
}
}
tocSb.Append(logSb);
return tocSb.ToString();
}
private string PackageLogFile(string packageId)
{
var logLocationVar = $"{Constants.BundleLogName}_{packageId}";
// Variable won't exist until package has been run
if (!_engine.ContainsVariable(logLocationVar))
return null;
var logLocation = _engine.GetVariableString(logLocationVar);
if (string.IsNullOrWhiteSpace(logLocation))
return null;
var logFile = Path.GetFullPath(logLocation);
if (string.IsNullOrEmpty(logFile) || !File.Exists(logFile))
return null;
return logFile;
}
private string ReadLogFile(string fileName, string logName, int tocIndex)
{
string logText;
var bakFile = Path.GetFullPath($"{fileName}.view.bak");
// Copy the log file to avoid file contention.
File.Copy(fileName, bakFile);
try
{
logText = File.ReadAllText(bakFile);
}
catch (Exception ex)
{
logText = $"Unable to read file {bakFile} ({ex.Message})";
}
finally
{
File.Delete(bakFile);
}
var sb = new StringBuilder();
sb.AppendLine();
sb.AppendLine();
sb.AppendLine($"{tocIndex}. {logName}");
sb.AppendLine(Constants.Line);
sb.AppendLine(logText);
return sb.ToString();
}
}

View File

@@ -0,0 +1,112 @@
using Bootstrapper.Models.State;
using Bootstrapper.Models.Util;
using System;
using System.Diagnostics;
using System.IO;
using System.Windows;
using WixToolset.BootstrapperApplicationApi;
namespace Bootstrapper.Models;
/// <summary>
/// A model that exposes all the functionality of the BA.
/// </summary>
internal class Model
{
public Model(IEngine engine, IBootstrapperCommand commandInfo, WpfFacade uiFacade)
{
Engine = engine;
CommandInfo = commandInfo;
UiFacade = uiFacade;
Log = new Log(Engine);
State = new AppState(Engine, CommandInfo);
}
/// <summary>
/// Contains shared state used by the BA. All members are thread safe unless noted otherwise.
/// </summary>
public AppState State { get; }
/// <summary>
/// Command line parameters and other command info passed from the engine.
/// </summary>
public IBootstrapperCommand CommandInfo { get; }
/// <summary>
/// Read from and write to the bundle's log.
/// </summary>
public Log Log { get; }
/// <summary>
/// A facade exposing the limited UI functionality needed by the BA.
/// </summary>
public WpfFacade UiFacade { get; }
/// <summary>
/// WiX engine
/// </summary>
public IEngine Engine { get; }
/// <summary>
/// Starts the plan and apply phases with the given action.
/// </summary>
/// <param name="action">Action to plan</param>
public void PlanAndApply(LaunchAction action)
{
State.PlannedAction = action;
State.BaStatus = BaStatus.Planning;
State.CancelRequested = false;
Engine.Plan(action);
}
/// <summary>
/// Reads the log file and displays it in the user's default text editor.
/// </summary>
public void ShowLog()
{
try
{
var fileName = Path.GetFullPath($"{Engine.GetVariableString(Constants.BundleLogName)}.view.txt");
Log.WriteLogFile(State, fileName);
if (File.Exists(fileName))
{
Process process = null;
try
{
process = new Process();
process.StartInfo.FileName = fileName;
process.StartInfo.UseShellExecute = true;
process.StartInfo.Verb = "open";
process.Start();
}
finally
{
process?.Dispose();
}
}
}
catch (Exception ex)
{
UiFacade.ShowMessageBox($"Unable to display log: {ex.Message}", MessageBoxButton.OK, MessageBoxImage.Error, MessageBoxResult.OK);
}
}
/// <summary>
/// </summary>
public void SaveEmbeddedLog(int exitCode)
{
try
{
if (State.Display == Display.Embedded)
{
Log.Write($"Exit code: 0x{exitCode:X}");
Log.WriteLogFile(State, Log.EmbeddedLogFileName());
}
}
catch
{
// ignore
}
}
}

View File

@@ -0,0 +1,210 @@
using Bootstrapper.Models.Util;
using System;
using System.Text;
using System.Threading;
using WixToolset.BootstrapperApplicationApi;
namespace Bootstrapper.Models.State;
/// <summary>
/// Provides thread safe access to all state shared by the BA
/// </summary>
internal class AppState
{
private readonly IBootstrapperCommand _commandInfo;
private readonly object _lock = new();
private long _baStatus;
private long _plannedAction;
private long _relatedBundleStatus;
private string _relatedBundleId;
private string _relatedBundleVersion;
private long _phaseResult;
private string _errorMessage;
private long _cancelRequested;
private string _relatedBundleName;
public AppState(IEngine engine, IBootstrapperCommand commandInfo)
{
_commandInfo = commandInfo;
Bundle = new BootstrapperApplicationData().Bundle;
BundleVersion = engine.GetVariableVersion(Constants.VersionVariable);
_baStatus = (long)BaStatus.Initializing;
_relatedBundleStatus = (long)BundleStatus.Unknown;
_plannedAction = (long)LaunchAction.Unknown;
}
/// <summary>
/// Information about the packages included in the bundle.
/// </summary>
public IBundleInfo Bundle { get; }
/// <summary>
/// Version of the bundle that this BA will deploy.
/// </summary>
public string BundleVersion { get; }
/// <summary>
/// The display level of the BA.
/// </summary>
public Display Display => _commandInfo.Display;
/// <summary>
/// Whether a version of this bundle was previously installed, and if so, whether this bundle is newer or older than the
/// installed version.
/// </summary>
public BundleStatus RelatedBundleStatus
{
get => (BundleStatus)Interlocked.Read(ref _relatedBundleStatus);
set => Interlocked.Exchange(ref _relatedBundleStatus, (long)value);
}
/// <summary>
/// Package ID of the bundle that was discovered during the detection phase.
/// Will be <see langword="null" /> if not installed.
/// </summary>
public string RelatedBundleId
{
get
{
lock (_lock)
return _relatedBundleId;
}
set
{
lock (_lock)
_relatedBundleId = value;
}
}
public string RelatedBundleName
{
get
{
lock (_lock)
return _relatedBundleName;
}
set
{
lock (_lock)
_relatedBundleName = value;
}
}
/// <summary>
/// Version of the bundle that was discovered during the detection phase.
/// Will be <see langword="null" /> if not installed.
/// </summary>
public string RelatedBundleVersion
{
get
{
lock (_lock)
return _relatedBundleVersion;
}
set
{
lock (_lock)
_relatedBundleVersion = value;
}
}
/// <summary>
/// Current status of the BA.
/// </summary>
public BaStatus BaStatus
{
get => (BaStatus)Interlocked.Read(ref _baStatus);
set => Interlocked.Exchange(ref _baStatus, (long)value);
}
/// <summary>
/// Final result of the last phase that ran.
/// </summary>
public int PhaseResult
{
get => (int)Interlocked.Read(ref _phaseResult);
set => Interlocked.Exchange(ref _phaseResult, value);
}
/// <summary>
/// Action selected when beginning the plan phase.
/// </summary>
public LaunchAction PlannedAction
{
get => (LaunchAction)Interlocked.Read(ref _plannedAction);
set => Interlocked.Exchange(ref _plannedAction, (long)value);
}
/// <summary>
/// Can be set to <see langword="true" /> to cancel the plan and apply phases.
/// </summary>
public bool CancelRequested
{
get => Interlocked.Read(ref _cancelRequested) == 1L;
set => Interlocked.Exchange(ref _cancelRequested, Convert.ToInt32(value));
}
/// <summary>
/// Stores any error encountered during the apply phase.
/// </summary>
public string ErrorMessage
{
get
{
lock (_lock)
return _errorMessage;
}
set
{
lock (_lock)
_errorMessage = value;
}
}
/// <summary>
/// Gets the display name for a package if possible.
/// </summary>
/// <param name="packageId">Identity of the package.</param>
/// <returns>Display name of the package if found or the package id if not.</returns>
public string GetPackageName(string packageId)
{
var packageName = string.Empty;
if (packageId == RelatedBundleId)
packageName = RelatedBundleName;
else if (Bundle.Packages.TryGetValue(packageId, out var package) && !string.IsNullOrWhiteSpace(package.DisplayName))
packageName = package.DisplayName;
return packageName;
}
public override string ToString()
{
var tocSb = new StringBuilder();
string error;
if (string.IsNullOrWhiteSpace(ErrorMessage))
error = "None";
else
error = ErrorMessage;
string installedVersion;
if (string.IsNullOrWhiteSpace(RelatedBundleVersion))
installedVersion = "Not installed";
else
installedVersion = RelatedBundleVersion;
tocSb.AppendLine($"Bootstrapper status: {BaStatus}");
tocSb.AppendLine($"Bundle status: {RelatedBundleStatus}");
tocSb.AppendLine($"Bundle version: {BundleVersion}");
tocSb.AppendLine($"Installed version: {installedVersion}");
tocSb.AppendLine($"Planned action: {PlannedAction}");
tocSb.AppendLine($"Result of last phase: {PhaseResult}");
tocSb.AppendLine($"Bundle error message: {error}");
tocSb.AppendLine($"Display: {_commandInfo.Display}");
tocSb.AppendLine($"Command line: {_commandInfo.CommandLine}");
tocSb.AppendLine($"Command line action: {_commandInfo.Action}");
tocSb.AppendLine($"Command line resume: {_commandInfo.Resume}");
return tocSb.ToString();
}
}

View File

@@ -0,0 +1,9 @@
namespace Bootstrapper.Models.State
{
internal class ProgressReport
{
public string Message { get; set; }
public string PackageName { get; set; }
public int Progress { get; set; }
}
}

View File

@@ -0,0 +1,48 @@
namespace Bootstrapper.Models.Util
{
/// <summary>
/// The states of installation.
/// </summary>
internal enum BaStatus
{
/// <summary>
/// The BA is starting up.
/// </summary>
Initializing,
/// <summary>
/// The BA is busy running the detect phase.
/// </summary>
Detecting,
/// <summary>
/// The BA has completed the detect phase and is idle, waiting for the user to start the plan phase.
/// </summary>
Waiting,
/// <summary>
/// The BA is busy running the plan phase.
/// </summary>
Planning,
/// <summary>
/// The BA is busy running the apply phase.
/// </summary>
Applying,
/// <summary>
/// The apply phase has successfully completed and the BA is idle, waiting for the user to exit the app.
/// </summary>
Applied,
/// <summary>
/// The user cancelled a phase and the BA is idle, waiting for user input.
/// </summary>
Cancelled,
/// <summary>
/// A phase failed and the BA is idle, waiting for user input.
/// </summary>
Failed
}
}

View File

@@ -0,0 +1,30 @@
namespace Bootstrapper.Models.Util
{
/// <summary>
/// The states of bundle detection.
/// </summary>
internal enum BundleStatus
{
Unknown = 0,
/// <summary>
/// There are no upgrade related bundles installed.
/// </summary>
NotInstalled = 1,
/// <summary>
/// All upgrade related bundles that are installed are earlier versions than this bundle.
/// </summary>
OlderInstalled = 2,
/// <summary>
/// All upgrade related bundles that are installed are the same version as this bundle.
/// </summary>
Current = 3,
/// <summary>
/// At least one upgrade related bundle is installed that is a newer version than this bundle.
/// </summary>
NewerInstalled = 4
}
}

View File

@@ -0,0 +1,11 @@
namespace Bootstrapper.Models.Util
{
internal static class Constants
{
public const string BundleLogName = "WixBundleLog";
public const string VersionVariable = "WixBundleVersion";
public const string BundleNameVariable = "WixBundleName";
public const string RebootMessage = "This machine has other pending updates that require a reboot before the app can be installed. Please restart, then try installing again.";
public const string Line = "========================================================================================";
}
}

View File

@@ -0,0 +1,19 @@
using System;
using WixToolset.BootstrapperApplicationApi;
namespace Bootstrapper.Models.Util;
internal class DetectPhaseCompleteEventArgs : EventArgs
{
public DetectPhaseCompleteEventArgs(LaunchAction followupAction)
{
FollowupAction = followupAction;
}
/// <summary>
/// Indicates which action is being planned and executed after the detect phase has
/// completed. If <see cref="LaunchAction.Unknown" />, then no action is planned, and
/// the UI should prompt the user for the action (install, uninstall, updated, repair, etc.).
/// </summary>
public LaunchAction FollowupAction { get; }
}

View File

@@ -0,0 +1,12 @@
namespace Bootstrapper.Models.Util
{
/// <summary>
/// The states of detection.
/// </summary>
internal enum DetectionState
{
Unknown,
Absent,
Present
}
}

View File

@@ -0,0 +1,56 @@
using System.ComponentModel;
namespace Bootstrapper.Models.Util
{
public static class ErrorHelper
{
/// <summary>
/// WiX return code which indicates cancellation.
/// </summary>
public const int CancelCode = 1223;
/// <summary>
/// ERROR_INSTALL_USEREXIT (0x80070642)
/// </summary>
public const int CancelHResult = -2147023294;
public static bool HResultIsFailure(int hResult)
{
return hResult < 0;
}
/// <summary>
/// Converts an HRESULT to a win32 error.
/// </summary>
/// <param name="hResult"></param>
/// <returns></returns>
public static int HResultToWin32(int hResult)
{
var win32 = hResult;
if ((win32 & 0xFFFF0000) == 0x80070000)
win32 &= 0xFFFF;
return win32;
}
/// <summary>
/// Converts an HRESULT to a message.
/// </summary>
/// <param name="hResult"></param>
/// <returns></returns>
public static string HResultToMessage(int hResult)
{
if (hResult == 0)
return "OK";
if (HResultIsFailure(hResult))
{
var message = new Win32Exception(hResult).Message;
return $"0x{hResult:X} {message}";
}
return $"Result {hResult} (0x{hResult:X})";
}
}
}

View File

@@ -0,0 +1,15 @@
using System;
namespace Bootstrapper.Models.Util
{
internal class InstallerVariableNotFoundException : Exception
{
public InstallerVariableNotFoundException(string variableName)
: base($"The installer variable \"{variableName}\" could not be found.")
{ }
public InstallerVariableNotFoundException(string variableName, Exception innerException)
: base($"The installer variable \"{variableName}\" could not be found.", innerException)
{ }
}
}

View File

@@ -0,0 +1,16 @@
using System;
namespace Bootstrapper.Models.Util
{
/// <summary>
/// Exception raised when executing a phase
/// </summary>
internal class PhaseException : Exception
{
public PhaseException(Exception innerException)
: base(innerException.Message, innerException)
{
HResult = innerException.HResult;
}
}
}

View File

@@ -0,0 +1,201 @@
using Bootstrapper.Models.State;
using Bootstrapper.Models.Util;
using Bootstrapper.ViewModels;
using Bootstrapper.Views;
using System;
using System.Windows;
using System.Windows.Input;
using System.Windows.Interop;
using System.Windows.Threading;
using WixToolset.BootstrapperApplicationApi;
namespace Bootstrapper.Models;
internal class WpfFacade
{
private readonly Log _log;
private ShellViewModel _shellVm;
private Window _shell;
private Dispatcher _dispatcher;
public WpfFacade(Log log, Display display)
{
_log = log ?? throw new ArgumentNullException(nameof(log));
IsUiShown = display == Display.Full || display == Display.Passive;
}
/// <summary>
/// Dispatches progress reports to the UI. Will be <see langword="null" /> when there is no UI to receive these reports.
/// </summary>
public IProgress<ProgressReport> ProgressReporter { get; private set; }
/// <summary>
/// A valid window handle required for the apply phase.
/// </summary>
public IntPtr ShellWindowHandle { get; private set; }
/// <summary>
/// Returns <see langword="false" /> when the installer is running silently.
/// </summary>
public bool IsUiShown { get; }
/// <summary>
/// Builds out needed UI elements and displays the shell window if not running silently.
/// </summary>
/// <param name="model"></param>
public void Initialize(Model model)
{
_dispatcher = Dispatcher.CurrentDispatcher;
_dispatcher.UnhandledException += Dispatcher_UnhandledException;
_shell = new ShellView();
ShellWindowHandle = new WindowInteropHelper(_shell).EnsureHandle();
// Stop message loop when the window is closed.
_shell.Closed += (sender, e) => _dispatcher.InvokeShutdown();
if (!IsUiShown)
return;
_shellVm = new ShellViewModel(model);
_shell.DataContext = _shellVm;
ProgressReporter = new Progress<ProgressReport>(r => _shellVm.ProgressVm.ProcessProgressReport(r));
_log.Write("Displaying UI.");
_shell.Show();
}
/// <summary>
/// Starts the message loop for the UI framework. This is a blocking call that is exited by calling.
/// <see cref="ShutDown" />.
/// </summary>
/// <exception cref="InvalidOperationException"></exception>
public void RunMessageLoop()
{
if (_shell == null)
throw new InvalidOperationException($"{nameof(Initialize)} must be called before the message loop can be started");
Dispatcher.Run();
}
/// <summary>
/// Executes the given action on "the UI thread".
/// </summary>
/// <param name="action">Action to execute</param>
/// <param name="blockUntilComplete">
/// If <see langword="true" />, then this call will block until the action completes.
/// If <see langword="false" />, this call immediately returns after queuing the action
/// for processing by the message loop.
/// </param>
public void Dispatch(Action action, bool blockUntilComplete = true)
{
if (blockUntilComplete)
{
// No need to dispatch if already running on the UI thread
if (_dispatcher.CheckAccess())
action();
else
_dispatcher.Invoke(DispatcherPriority.Normal, action);
}
else
_dispatcher.BeginInvoke(DispatcherPriority.Normal, action);
}
/// <summary>
/// Closes any displayed windows and stops the message loop.
/// </summary>
public void ShutDown()
{
Dispatch(CloseShell);
}
/// <summary>
/// Submits a request to the UI to refresh all commands.
/// </summary>
public void Refresh()
{
if (IsUiShown)
Dispatch(CommandManager.InvalidateRequerySuggested);
}
/// <summary>
/// Displays a message box with the given criteria
/// </summary>
/// <param name="message"></param>
/// <param name="buttons"></param>
/// <param name="image"></param>
/// <param name="defaultResult"></param>
/// <returns></returns>
public MessageBoxResult ShowMessageBox(string message, MessageBoxButton buttons, MessageBoxImage image, MessageBoxResult defaultResult)
{
var result = defaultResult;
if (IsUiShown)
{
Dispatch(
() => result = MessageBox.Show(
_shell,
message,
"Installation",
buttons,
image,
defaultResult));
}
else
{
Dispatch(
() => result = MessageBox.Show(
message,
"Installation",
buttons,
image,
defaultResult));
}
return result;
}
/// <summary>
/// Informs the UI that the detect phase has completed.
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
public void OnDetectPhaseComplete(object sender, DetectPhaseCompleteEventArgs e)
{
if (_shellVm != null)
{
_log.Write($"{nameof(WpfFacade)}: Notified that detect is complete. Dispatching UI refresh tasks.");
Dispatch(() => _shellVm.AfterDetect(e.FollowupAction), false);
}
}
/// <summary>
/// Informs the UI that the apply phase has completed
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
public void OnApplyPhaseComplete(object sender, EventArgs e)
{
if (_shellVm != null)
{
_log.Write($"{nameof(WpfFacade)}: Notified that installation has completed. Dispatching UI refresh tasks.");
Dispatch(_shellVm.AfterApply, false);
}
}
private void CloseShell()
{
if (IsUiShown)
_shell.Close();
_dispatcher.InvokeShutdown();
}
private void Dispatcher_UnhandledException(object sender, DispatcherUnhandledExceptionEventArgs e)
{
e.Handled = true;
_log.Write(e.Exception);
if (IsUiShown)
ShowMessageBox($"An error occurred:\r\n{e.Exception.Message}", MessageBoxButton.OK, MessageBoxImage.Error, MessageBoxResult.OK);
}
}

View File

@@ -0,0 +1,554 @@
using Bootstrapper.Models;
using Bootstrapper.Models.Util;
using System;
using System.Linq;
using System.Windows;
using WixToolset.BootstrapperApplicationApi;
namespace Bootstrapper.Phases;
internal class ApplyPhase
{
private readonly Model _model;
public ApplyPhase(Model model)
{
_model = model;
}
public event EventHandler<EventArgs> ApplyPhaseComplete;
/// <summary>
/// Fired when the engine has begun installing the bundle.
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
/// <exception cref="PhaseException"></exception>
public virtual void OnApplyBegin(object sender, ApplyBeginEventArgs e)
{
try
{
_model.State.PhaseResult = 0;
_model.State.ErrorMessage = string.Empty;
if (e.Cancel)
return;
_model.State.BaStatus = BaStatus.Applying;
}
catch (Exception ex)
{
_model.Log.Write(ex);
throw new PhaseException(ex);
}
}
/// <summary>
/// Fired when the engine has completed installing the bundle.
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
/// <exception cref="PhaseException"></exception>
public virtual void OnApplyComplete(object sender, ApplyCompleteEventArgs e)
{
try
{
_model.State.PhaseResult = e.Status;
// Set the state to applied or failed unless the user cancelled.
if (_model.State.BaStatus == BaStatus.Cancelled || e.Status == ErrorHelper.CancelHResult)
{
_model.State.BaStatus = BaStatus.Cancelled;
_model.Log.Write("User cancelled");
}
else if (ErrorHelper.HResultIsFailure(e.Status))
{
_model.State.BaStatus = BaStatus.Failed;
var msg = $"Apply failed - {ErrorHelper.HResultToMessage(e.Status)}";
if (string.IsNullOrEmpty(_model.State.ErrorMessage))
_model.State.ErrorMessage = msg;
_model.Log.Write(msg);
if (e.Restart == ApplyRestart.RestartRequired && _model.UiFacade.IsUiShown)
_model.UiFacade.ShowMessageBox(Constants.RebootMessage, MessageBoxButton.OK, MessageBoxImage.Stop, MessageBoxResult.OK);
}
else
_model.State.BaStatus = BaStatus.Applied;
}
catch (Exception ex)
{
_model.Log.Write(ex);
throw new PhaseException(ex);
}
finally
{
if (_model.State.Display == Display.Full)
ApplyPhaseComplete?.Invoke(this, EventArgs.Empty);
else
_model.UiFacade.ShutDown();
}
}
/// <summary>
/// Fired when the engine has encountered an error.
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
/// <exception cref="PhaseException"></exception>
public virtual void OnError(object sender, ErrorEventArgs e)
{
try
{
if (e.ErrorCode == ErrorHelper.CancelCode)
_model.State.BaStatus = BaStatus.Cancelled;
else
{
_model.State.ErrorMessage = e.ErrorMessage;
_model.Log.Write("Error encountered");
_model.Log.Write(e.ErrorMessage, true);
_model.Log.Write($"Type: {e.ErrorType}", true);
_model.Log.Write($"Code: {e.ErrorCode}", true);
if (!string.IsNullOrWhiteSpace(e.PackageId))
_model.Log.Write($"Package: {e.PackageId}", true);
var data = e.Data?.Where(d => !string.IsNullOrWhiteSpace(d)).ToArray() ?? Array.Empty<string>();
if (data.Length > 0)
{
_model.Log.Write("Data:", true);
foreach (var d in data)
_model.Log.Write($" {d}", true);
}
if (_model.UiFacade.IsUiShown)
{
// Show an error dialog.
var button = MessageBoxButton.OK;
var buttonHint = e.UIHint & 0xF;
if (buttonHint >= 0 && buttonHint <= 4)
button = (MessageBoxButton)buttonHint;
var response = _model.UiFacade.ShowMessageBox(e.ErrorMessage, button, MessageBoxImage.Error, MessageBoxResult.None);
if (buttonHint == (int)button)
{
// If WiX supplied a hint, return the result
e.Result = (Result)response;
_model.Log.Write($"User response: {response}");
}
}
}
}
catch (Exception ex)
{
_model.Log.Write(ex);
throw new PhaseException(ex);
}
}
/// <summary>
/// Fired when the plan determined that nothing should happen to prevent downgrading.
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
public virtual void OnApplyDowngrade(object sender, ApplyDowngradeEventArgs e)
{ }
/// <summary>
/// Fired when the engine has begun installing packages.
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
public virtual void OnExecuteBegin(object sender, ExecuteBeginEventArgs e)
{ }
/// <summary>
/// Fired when the engine has completed installing packages.
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
public virtual void OnExecuteComplete(object sender, ExecuteCompleteEventArgs e)
{ }
/// <summary>
/// Fired by the engine while executing a package.
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
public virtual void OnExecuteProgress(object sender, ExecuteProgressEventArgs e)
{ }
///// <summary>
///// Fired when the engine has begun to set up the update package.
///// </summary>
///// <param name="sender"></param>
///// <param name="e"></param>
//public virtual void OnSetUpdateBegin(object sender, SetUpdateBeginEventArgs e)
//{ }
///// <summary>
///// Fired when the engine has completed setting up the update package.
///// </summary>
///// <param name="sender"></param>
///// <param name="e"></param>
///// <exception cref="PhaseException"></exception>
//public virtual void OnSetUpdateComplete(object sender, SetUpdateCompleteEventArgs e)
//{ }
/// <summary>
/// Fired when the engine executes one or more patches targeting a product.
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
public virtual void OnExecutePatchTarget(object sender, ExecutePatchTargetEventArgs e)
{ }
/// <summary>
/// Fired when the engine is about to begin an MSI transaction.
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
public virtual void OnBeginMsiTransactionBegin(object sender, BeginMsiTransactionBeginEventArgs e)
{ }
/// <summary>
/// Fired when the engine has completed beginning an MSI transaction.
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
public virtual void OnBeginMsiTransactionComplete(object sender, BeginMsiTransactionCompleteEventArgs e)
{ }
/// <summary>
/// Fired when the engine is about to commit an MSI transaction.
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
public virtual void OnCommitMsiTransactionBegin(object sender, CommitMsiTransactionBeginEventArgs e)
{ }
/// <summary>
/// Fired when the engine has completed comitting an MSI transaction.
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
public virtual void OnCommitMsiTransactionComplete(object sender, CommitMsiTransactionCompleteEventArgs e)
{ }
/// <summary>
/// Fired when the engine is about to rollback an MSI transaction.
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
public virtual void OnRollbackMsiTransactionBegin(object sender, RollbackMsiTransactionBeginEventArgs e)
{ }
/// <summary>
/// Fired when the engine has completed rolling back an MSI transaction.
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
public virtual void OnRollbackMsiTransactionComplete(object sender, RollbackMsiTransactionCompleteEventArgs e)
{ }
/// <summary>
/// Fired when the engine has begun acquiring the payload or container.
/// The BA can change the source using
/// <see cref="M:WixToolset.Mba.Core.IEngine.SetLocalSource(System.String,System.String,System.String)" />
/// or
/// <see
/// cref="M:WixToolset.Mba.Core.IEngine.SetDownloadSource(System.String,System.String,System.String,System.String,System.String)" />
/// .
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
public virtual void OnCacheAcquireBegin(object sender, CacheAcquireBeginEventArgs e)
{ }
/// <summary>
/// Fired when the engine has completed the acquisition of the payload or container.
/// The BA can change the source using
/// <see cref="M:WixToolset.Mba.Core.IEngine.SetLocalSource(System.String,System.String,System.String)" />
/// or
/// <see
/// cref="M:WixToolset.Mba.Core.IEngine.SetDownloadSource(System.String,System.String,System.String,System.String,System.String)" />
/// .
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
public virtual void OnCacheAcquireComplete(object sender, CacheAcquireCompleteEventArgs e)
{ }
/// <summary>
/// Fired by the engine to allow the BA to override the acquisition action.
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
public virtual void OnCacheAcquireResolving(object sender, CacheAcquireResolvingEventArgs e)
{ }
/// <summary>
/// Fired when the engine has progress acquiring the payload or container.
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
public virtual void OnCacheAcquireProgress(object sender, CacheAcquireProgressEventArgs e)
{ }
/// <summary>
/// Fired when the engine has begun caching the installation sources.
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
public virtual void OnCacheBegin(object sender, CacheBeginEventArgs e)
{ }
/// <summary>
/// Fired after the engine has cached the installation sources.
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
public virtual void OnCacheComplete(object sender, CacheCompleteEventArgs e)
{ }
/// <summary>
/// Fired when the engine begins the verification of the payload or container that was already in the package cache.
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
public virtual void OnCacheContainerOrPayloadVerifyBegin(object sender, CacheContainerOrPayloadVerifyBeginEventArgs e)
{ }
/// <summary>
/// Fired when the engine has completed the verification of the payload or container that was already in the package
/// cache.
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
public virtual void OnCacheContainerOrPayloadVerifyComplete(object sender, CacheContainerOrPayloadVerifyCompleteEventArgs e)
{ }
/// <summary>
/// Fired when the engine has progress verifying the payload or container that was already in the package cache.
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
public virtual void OnCacheContainerOrPayloadVerifyProgress(object sender, CacheContainerOrPayloadVerifyProgressEventArgs e)
{ }
/// <summary>
/// Fired when the engine has begun caching a specific package.
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
public virtual void OnCachePackageBegin(object sender, CachePackageBeginEventArgs e)
{ }
/// <summary>
/// Fired when the engine has completed caching a specific package.
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
public virtual void OnCachePackageComplete(object sender, CachePackageCompleteEventArgs e)
{ }
/// <summary>
/// Fired when the engine failed validating a package in the package cache that is non-vital to execution.
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
public virtual void OnCachePackageNonVitalValidationFailure(object sender, CachePackageNonVitalValidationFailureEventArgs e)
{ }
/// <summary>
/// Fired when the engine begins the extraction of the payload from the container.
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
public virtual void OnCachePayloadExtractBegin(object sender, CachePayloadExtractBeginEventArgs e)
{ }
/// <summary>
/// Fired when the engine has completed the extraction of the payload from the container.
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
public virtual void OnCachePayloadExtractComplete(object sender, CachePayloadExtractCompleteEventArgs e)
{ }
/// <summary>
/// Fired when the engine has progress extracting the payload from the container.
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
public virtual void OnCachePayloadExtractProgress(object sender, CachePayloadExtractProgressEventArgs e)
{ }
/// <summary>
/// Fired when the engine begins the verification of the acquired payload or container.
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
public virtual void OnCacheVerifyBegin(object sender, CacheVerifyBeginEventArgs e)
{ }
/// <summary>
/// Fired when the engine has completed the verification of the acquired payload or container.
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
public virtual void OnCacheVerifyComplete(object sender, CacheVerifyCompleteEventArgs e)
{ }
/// <summary>
/// Fired when the engine has progress verifying the payload or container.
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
public virtual void OnCacheVerifyProgress(object sender, CacheVerifyProgressEventArgs e)
{ }
/// <summary>
/// Fired when the engine has begun installing a specific package.
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
public virtual void OnExecutePackageBegin(object sender, ExecutePackageBeginEventArgs e)
{ }
/// <summary>
/// Fired when the engine has completed installing a specific package.
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
public virtual void OnExecutePackageComplete(object sender, ExecutePackageCompleteEventArgs e)
{ }
/// <summary>
/// Fired when the engine is about to pause Windows automatic updates.
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
public virtual void OnPauseAutomaticUpdatesBegin(object sender, PauseAutomaticUpdatesBeginEventArgs e)
{ }
/// <summary>
/// Fired when the engine has completed pausing Windows automatic updates.
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
public virtual void OnPauseAutomaticUpdatesComplete(object sender, PauseAutomaticUpdatesCompleteEventArgs e)
{ }
/// <summary>
/// Fired when the engine is about to take a system restore point.
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
public virtual void OnSystemRestorePointBegin(object sender, SystemRestorePointBeginEventArgs e)
{ }
/// <summary>
/// Fired when the engine has completed taking a system restore point.
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
public virtual void OnSystemRestorePointComplete(object sender, SystemRestorePointCompleteEventArgs e)
{ }
/// <summary>
/// Fired when the engine is about to start the elevated process.
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
public virtual void OnElevateBegin(object sender, ElevateBeginEventArgs e)
{ }
/// <summary>
/// Fired when the engine has completed starting the elevated process.
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
public virtual void OnElevateComplete(object sender, ElevateCompleteEventArgs e)
{ }
/// <summary>
/// Fired when the engine is about to launch the preapproved executable.
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
public virtual void OnLaunchApprovedExeBegin(object sender, LaunchApprovedExeBeginEventArgs e)
{ }
/// <summary>
/// Fired when the engine has completed launching the preapproved executable.
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
public virtual void OnLaunchApprovedExeComplete(object sender, LaunchApprovedExeCompleteEventArgs e)
{ }
/// <summary>
/// Fired when the engine has begun registering the location and visibility of the bundle.
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
public virtual void OnRegisterBegin(object sender, RegisterBeginEventArgs e)
{ }
/// <summary>
/// Fired when the engine has completed registering the location and visibility of the bundle.
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
public virtual void OnRegisterComplete(object sender, RegisterCompleteEventArgs e)
{ }
/// <summary>
/// Fired when the engine unregisters the bundle.
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
public virtual void OnUnregisterBegin(object sender, UnregisterBeginEventArgs e)
{ }
/// <summary>
/// Fired when the engine unregistration is complete.
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
public virtual void OnUnregisterComplete(object sender, UnregisterCompleteEventArgs e)
{ }
/// <summary>
/// Fired when the engine has changed progress for the bundle installation.
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
public virtual void OnProgress(object sender, ProgressEventArgs e)
{ }
/// <summary>
/// Fired when Windows Installer sends an installation message.
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
public virtual void OnExecuteMsiMessage(object sender, ExecuteMsiMessageEventArgs e)
{ }
/// <summary>
/// Fired when a package that spawned a process is cancelled.
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
public virtual void OnExecuteProcessCancel(object sender, ExecuteProcessCancelEventArgs e)
{ }
/// <summary>
/// Fired when a package sends a files in use installation message.
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
public virtual void OnExecuteFilesInUse(object sender, ExecuteFilesInUseEventArgs e)
{ }
}

View File

@@ -0,0 +1,29 @@
using Bootstrapper.Models;
using WixToolset.BootstrapperApplicationApi;
namespace Bootstrapper.Phases;
/// <summary>
/// Manages cancellation
/// </summary>
internal class CancelHandler
{
private readonly Model _model;
public CancelHandler(Model model)
{
_model = model;
}
public void CheckForCancel(object sender, CancellableHResultEventArgs e)
{
if (!e.Cancel && _model.State.CancelRequested)
e.Cancel = true;
}
public void CheckResult(object sender, ResultEventArgs e)
{
if (e.Result != Result.Abort && e.Result != Result.Error && e.Result != Result.Cancel && _model.State.CancelRequested)
e.Result = Result.Cancel;
}
}

View File

@@ -0,0 +1,287 @@
using Bootstrapper.Models;
using Bootstrapper.Models.Util;
using System;
using WixToolset.BootstrapperApplicationApi;
namespace Bootstrapper.Phases;
internal class DetectPhase
{
private readonly Model _model;
private DetectionState _bundleDetectedState;
public DetectPhase(Model model)
{
_model = model;
_bundleDetectedState = DetectionState.Unknown;
}
public event EventHandler<DetectPhaseCompleteEventArgs> DetectPhaseComplete;
/// <summary>
/// Fired when the engine is starting up the bootstrapper application.
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
public virtual void OnStartup(object sender, StartupEventArgs e)
{ }
/// <summary>
/// Fired when the engine is shutting down the bootstrapper application.
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
public virtual void OnShutdown(object sender, ShutdownEventArgs e)
{ }
/// <summary>
/// Fired when the overall detection phase has begun.
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
/// <exception cref="PhaseException"></exception>
public virtual void OnDetectBegin(object sender, DetectBeginEventArgs e)
{
try
{
_model.State.BaStatus = BaStatus.Detecting;
if (e.RegistrationType == RegistrationType.Full)
_bundleDetectedState = DetectionState.Present;
else
_bundleDetectedState = DetectionState.Absent;
_model.Log.Write($"{nameof(_bundleDetectedState)} set to {_bundleDetectedState}");
}
catch (Exception ex)
{
_model.Log.Write(ex);
throw new PhaseException(ex);
}
}
/// <summary>
/// Fired when a related bundle has been detected for a bundle.
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
/// <remarks>
/// Helpful when the detected bundle has the same upgrade code that this one does.
/// </remarks>
public virtual void OnDetectRelatedBundle(object sender, DetectRelatedBundleEventArgs e)
{
try
{
// If the detected bundle's upgrade code matches this bundle, but product code and version are different...
if (e.RelationType == RelationType.Upgrade)
{
_bundleDetectedState = DetectionState.Present;
if (string.IsNullOrWhiteSpace(_model.State.RelatedBundleVersion))
_model.State.RelatedBundleVersion = e.Version;
_model.Log.Write($"Detected version = {e.Version} / Bundle version = {_model.State.BundleVersion}");
if (_model.Engine.CompareVersions(_model.State.BundleVersion, e.Version) > 0)
{
if (_model.State.RelatedBundleStatus <= BundleStatus.Current)
_model.State.RelatedBundleStatus = BundleStatus.OlderInstalled;
}
else if (_model.Engine.CompareVersions(_model.State.BundleVersion, e.Version) == 0)
{
if (_model.State.RelatedBundleStatus == BundleStatus.NotInstalled)
_model.State.RelatedBundleStatus = BundleStatus.Current;
}
else
_model.State.RelatedBundleStatus = BundleStatus.NewerInstalled;
_model.Log.Write($"{nameof(_model.State.RelatedBundleStatus)} set to {_model.State.RelatedBundleStatus}");
}
if (!_model.State.Bundle.Packages.ContainsKey(e.ProductCode))
{
var package = _model.State.Bundle.AddRelatedBundleAsPackage(e.ProductCode, e.RelationType, e.PerMachine, e.Version);
_model.State.RelatedBundleId = package.Id;
if (_model.Engine.ContainsVariable(Constants.BundleNameVariable))
{
var name = _model.Engine.GetVariableString(Constants.BundleNameVariable);
_model.State.RelatedBundleName = $"{name} v{_model.State.RelatedBundleVersion}";
}
else
_model.State.RelatedBundleName = $"v{_model.State.RelatedBundleVersion}";
}
}
catch (Exception ex)
{
_model.Log.Write(ex);
throw new PhaseException(ex);
}
}
/// <summary>
/// Fired when the detection phase has completed.
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
/// <exception cref="PhaseException"></exception>
public virtual void OnDetectComplete(object sender, DetectCompleteEventArgs e)
{
var followupAction = LaunchAction.Unknown;
try
{
_model.State.PhaseResult = e.Status;
if (ErrorHelper.HResultIsFailure(e.Status))
{
var msg = $"Detect failed - {ErrorHelper.HResultToMessage(e.Status)}";
_model.Log.Write(msg);
if (string.IsNullOrEmpty(_model.State.ErrorMessage))
_model.State.ErrorMessage = msg;
if (!_model.UiFacade.IsUiShown)
_model.UiFacade.ShutDown();
return;
}
if (_model.State.RelatedBundleStatus == BundleStatus.Unknown)
{
if (_bundleDetectedState == DetectionState.Present)
{
_model.State.RelatedBundleStatus = BundleStatus.Current;
if (string.IsNullOrWhiteSpace(_model.State.RelatedBundleVersion))
_model.State.RelatedBundleVersion = _model.State.BundleVersion;
}
else
_model.State.RelatedBundleStatus = BundleStatus.NotInstalled;
}
_model.State.BaStatus = BaStatus.Waiting;
if (_model.CommandInfo.Action == LaunchAction.Uninstall && _model.CommandInfo.Resume == ResumeType.Arp)
{
_model.Log.Write("Starting plan for automatic uninstall");
followupAction = _model.CommandInfo.Action;
}
else if (_model.State.Display != Display.Full)
{
_model.Log.Write("Starting plan for silent mode.");
followupAction = _model.CommandInfo.Action;
}
}
catch (Exception ex)
{
_model.Log.Write(ex);
throw new PhaseException(ex);
}
finally
{
// Can't start plan until UI is notified
NotifyDetectComplete(followupAction);
}
try
{
if (followupAction != LaunchAction.Unknown)
_model.PlanAndApply(followupAction);
}
catch (Exception ex)
{
_model.Log.Write(ex);
throw new PhaseException(ex);
}
}
/// <summary>
/// Fired when a related bundle has been detected for a bundle package.
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
public virtual void OnDetectRelatedBundlePackage(object sender, DetectRelatedBundlePackageEventArgs e)
{ }
/// <summary>
/// Fired when a related MSI package has been detected for a package.
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
public virtual void OnDetectRelatedMsiPackage(object sender, DetectRelatedMsiPackageEventArgs e)
{ }
/// <summary>
/// Fired when the update detection has found a potential update candidate.
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
public virtual void OnDetectUpdate(object sender, DetectUpdateEventArgs e)
{ }
/// <summary>
/// Fired when the update detection phase has begun.
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
public virtual void OnDetectUpdateBegin(object sender, DetectUpdateBeginEventArgs e)
{ }
/// <summary>
/// Fired when the update detection phase has completed.
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
public virtual void OnDetectUpdateComplete(object sender, DetectUpdateCompleteEventArgs e)
{ }
/// <summary>
/// Fired when a forward compatible bundle is detected.
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
public virtual void OnDetectForwardCompatibleBundle(object sender, DetectForwardCompatibleBundleEventArgs e)
{ }
/// <summary>
/// Fired when the detection for a specific package has begun.
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
public virtual void OnDetectPackageBegin(object sender, DetectPackageBeginEventArgs e)
{ }
/// <summary>
/// Fired when the detection for a specific package has completed.
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
public virtual void OnDetectPackageComplete(object sender, DetectPackageCompleteEventArgs e)
{ }
/// <summary>
/// Fired when the engine detects a target product for an MSP package.
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
public virtual void OnDetectPatchTarget(object sender, DetectPatchTargetEventArgs e)
{ }
/// <summary>
/// Fired when a package was not detected but a package using the same provider key was.
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
public virtual void OnDetectCompatibleMsiPackage(object sender, DetectCompatibleMsiPackageEventArgs e)
{ }
/// <summary>
/// Fired when a feature in an MSI package has been detected.
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
public virtual void OnDetectMsiFeature(object sender, DetectMsiFeatureEventArgs e)
{ }
private void NotifyDetectComplete(LaunchAction followupAction)
{
if (_model.UiFacade.IsUiShown)
DetectPhaseComplete?.Invoke(this, new DetectPhaseCompleteEventArgs(followupAction));
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,435 @@
using Bootstrapper.Models;
using Bootstrapper.Models.Util;
using System;
using WixToolset.BootstrapperApplicationApi;
namespace Bootstrapper.Phases;
internal class LoggingDetectPhase : DetectPhase
{
private readonly Log _logger;
public LoggingDetectPhase(Model model)
: base(model)
{
_logger = model.Log;
}
/// <inheritdoc />
public override void OnStartup(object sender, StartupEventArgs e)
{
try
{
_logger.Write($"{nameof(DetectPhase)}: {nameof(OnStartup)} -------v");
base.OnStartup(sender, e);
_logger.Write($"{nameof(e.HResult)} = {ErrorHelper.HResultToMessage(e.HResult)}", true);
_logger.Write($"{nameof(DetectPhase)}: {nameof(OnStartup)} -------^");
}
catch (PhaseException)
{
throw;
}
catch (Exception ex)
{
_logger.Write(ex);
throw new PhaseException(ex);
}
}
/// <inheritdoc />
public override void OnShutdown(object sender, ShutdownEventArgs e)
{
try
{
_logger.Write($"{nameof(DetectPhase)}: {nameof(OnShutdown)} -------v");
base.OnShutdown(sender, e);
_logger.Write($"{nameof(e.Action)} = {e.Action}", true);
_logger.Write($"{nameof(e.HResult)} = {ErrorHelper.HResultToMessage(e.HResult)}", true);
_logger.Write($"{nameof(DetectPhase)}: {nameof(OnShutdown)} -------^");
}
catch (PhaseException)
{
throw;
}
catch (Exception ex)
{
_logger.Write(ex);
throw new PhaseException(ex);
}
}
/// <inheritdoc />
public override void OnDetectBegin(object sender, DetectBeginEventArgs e)
{
try
{
_logger.Write($"{nameof(DetectPhase)}: {nameof(OnDetectBegin)} -------v");
_logger.Write($"{nameof(e.PackageCount)} = {e.PackageCount}", true);
_logger.Write($"{nameof(e.Cached)} = {e.Cached}", true);
_logger.Write($"{nameof(e.RegistrationType)} = {e.RegistrationType}", true);
base.OnDetectBegin(sender, e);
_logger.Write($"{nameof(e.HResult)} = {ErrorHelper.HResultToMessage(e.HResult)}", true);
_logger.Write($"{nameof(DetectPhase)}: {nameof(OnDetectBegin)} -------^");
}
catch (PhaseException)
{ }
catch (Exception ex)
{
_logger.Write(ex);
e.HResult = ex.HResult;
}
}
/// <inheritdoc />
public override void OnDetectComplete(object sender, DetectCompleteEventArgs e)
{
try
{
_logger.Write($"{nameof(DetectPhase)}: {nameof(OnDetectComplete)} -------v");
_logger.Write($"{nameof(e.Status)} = {ErrorHelper.HResultToMessage(e.Status)}", true);
_logger.Write($"{nameof(e.EligibleForCleanup)} = {e.EligibleForCleanup}", true);
base.OnDetectComplete(sender, e);
_logger.Write($"{nameof(e.HResult)} = {ErrorHelper.HResultToMessage(e.HResult)}", true);
_logger.Write($"{nameof(DetectPhase)}: {nameof(OnDetectComplete)} -------^");
}
catch (PhaseException)
{
throw;
}
catch (Exception ex)
{
_logger.Write(ex);
throw new PhaseException(ex);
}
}
/// <inheritdoc />
public override void OnDetectRelatedBundle(object sender, DetectRelatedBundleEventArgs e)
{
try
{
_logger.Write($"{nameof(DetectPhase)}: {nameof(OnDetectRelatedBundle)} -------v");
_logger.Write($"{nameof(e.ProductCode)} = {e.ProductCode}", true);
_logger.Write($"{nameof(e.Version)} = {e.Version}", true);
_logger.Write($"{nameof(e.BundleTag)} = {e.BundleTag}", true);
_logger.Write($"{nameof(e.PerMachine)} = {e.PerMachine}", true);
_logger.Write($"{nameof(e.RelationType)} = {e.RelationType}", true);
_logger.Write($"{nameof(e.MissingFromCache)} = {e.MissingFromCache}", true);
base.OnDetectRelatedBundle(sender, e);
_logger.Write($"{nameof(e.HResult)} = {ErrorHelper.HResultToMessage(e.HResult)}", true);
_logger.Write($"{nameof(DetectPhase)}: {nameof(OnDetectRelatedBundle)} -------^");
}
catch (PhaseException)
{
throw;
}
catch (Exception ex)
{
_logger.Write(ex);
throw new PhaseException(ex);
}
}
/// <inheritdoc />
public override void OnDetectRelatedBundlePackage(object sender, DetectRelatedBundlePackageEventArgs e)
{
try
{
_logger.Write($"{nameof(DetectPhase)}: {nameof(OnDetectRelatedBundlePackage)} -------v");
_logger.Write($"{nameof(e.PackageId)} = {e.PackageId}", true);
_logger.Write($"{nameof(e.ProductCode)} = {e.ProductCode}", true);
_logger.Write($"{nameof(e.Version)} = {e.Version}", true);
_logger.Write($"{nameof(e.RelationType)} = {e.RelationType}", true);
_logger.Write($"{nameof(e.PerMachine)} = {e.PerMachine}", true);
base.OnDetectRelatedBundlePackage(sender, e);
_logger.Write($"{nameof(e.HResult)} = {ErrorHelper.HResultToMessage(e.HResult)}", true);
_logger.Write($"{nameof(DetectPhase)}: {nameof(OnDetectRelatedBundlePackage)} -------^");
}
catch (PhaseException)
{
throw;
}
catch (Exception ex)
{
_logger.Write(ex);
throw new PhaseException(ex);
}
}
/// <inheritdoc />
public override void OnDetectRelatedMsiPackage(object sender, DetectRelatedMsiPackageEventArgs e)
{
try
{
_logger.Write($"{nameof(DetectPhase)}: {nameof(OnDetectRelatedMsiPackage)} -------v");
_logger.Write($"{nameof(e.PackageId)} = {e.PackageId}", true);
_logger.Write($"{nameof(e.UpgradeCode)} = {e.UpgradeCode}", true);
_logger.Write($"{nameof(e.ProductCode)} = {e.ProductCode}", true);
_logger.Write($"{nameof(e.Version)} = {e.Version}", true);
_logger.Write($"{nameof(e.PerMachine)} = {e.PerMachine}", true);
_logger.Write($"{nameof(e.Operation)} = {e.Operation}", true);
base.OnDetectRelatedMsiPackage(sender, e);
_logger.Write($"{nameof(e.HResult)} = {ErrorHelper.HResultToMessage(e.HResult)}", true);
_logger.Write($"{nameof(DetectPhase)}: {nameof(OnDetectRelatedMsiPackage)} -------^");
}
catch (PhaseException)
{
throw;
}
catch (Exception ex)
{
_logger.Write(ex);
throw new PhaseException(ex);
}
}
/// <inheritdoc />
public override void OnDetectUpdate(object sender, DetectUpdateEventArgs e)
{
try
{
_logger.Write($"{nameof(DetectPhase)}: {nameof(OnDetectUpdate)} -------v");
_logger.Write($"{nameof(e.Title)} = {e.Title}", true);
_logger.Write($"{nameof(e.Summary)} = {e.Summary}", true);
_logger.Write($"{nameof(e.Version)} = {e.Version}", true);
_logger.Write($"{nameof(e.UpdateLocation)} = {e.UpdateLocation}", true);
base.OnDetectUpdate(sender, e);
_logger.Write($"{nameof(e.StopProcessingUpdates)} = {e.StopProcessingUpdates}");
_logger.Write($"{nameof(e.HResult)} = {ErrorHelper.HResultToMessage(e.HResult)}", true);
_logger.Write($"{nameof(DetectPhase)}: {nameof(OnDetectUpdate)} -------^");
}
catch (PhaseException)
{
throw;
}
catch (Exception ex)
{
_logger.Write(ex);
throw new PhaseException(ex);
}
}
/// <inheritdoc />
public override void OnDetectUpdateBegin(object sender, DetectUpdateBeginEventArgs e)
{
try
{
_logger.Write($"{nameof(DetectPhase)}: {nameof(OnDetectUpdateBegin)} -------v");
_logger.Write($"{nameof(e.UpdateLocation)} = {e.UpdateLocation}", true);
base.OnDetectUpdateBegin(sender, e);
_logger.Write($"{nameof(e.Skip)} = {e.Skip}", true);
_logger.Write($"{nameof(e.HResult)} = {ErrorHelper.HResultToMessage(e.HResult)}", true);
_logger.Write($"{nameof(DetectPhase)}: {nameof(OnDetectUpdateBegin)} -------^");
}
catch (PhaseException)
{
throw;
}
catch (Exception ex)
{
_logger.Write(ex);
throw new PhaseException(ex);
}
}
/// <inheritdoc />
public override void OnDetectUpdateComplete(object sender, DetectUpdateCompleteEventArgs e)
{
try
{
_logger.Write($"{nameof(DetectPhase)}: {nameof(OnDetectUpdateComplete)} -------v");
_logger.Write($"{nameof(e.Status)} = {ErrorHelper.HResultToMessage(e.Status)}", true);
base.OnDetectUpdateComplete(sender, e);
_logger.Write($"{nameof(e.IgnoreError)} = {e.IgnoreError}", true);
_logger.Write($"{nameof(e.HResult)} = {ErrorHelper.HResultToMessage(e.HResult)}", true);
_logger.Write($"{nameof(DetectPhase)}: {nameof(OnDetectUpdateComplete)} -------^");
}
catch (PhaseException)
{
throw;
}
catch (Exception ex)
{
_logger.Write(ex);
throw new PhaseException(ex);
}
}
/// <inheritdoc />
public override void OnDetectForwardCompatibleBundle(object sender, DetectForwardCompatibleBundleEventArgs e)
{
try
{
_logger.Write($"{nameof(DetectPhase)}: {nameof(OnDetectForwardCompatibleBundle)} -------v");
_logger.Write($"{nameof(e.BundleId)} = {e.BundleId}", true);
_logger.Write($"{nameof(e.Version)} = {e.Version}", true);
_logger.Write($"{nameof(e.BundleTag)} = {e.BundleTag}", true);
_logger.Write($"{nameof(e.PerMachine)} = {e.PerMachine}", true);
_logger.Write($"{nameof(e.RelationType)} = {e.RelationType}", true);
base.OnDetectForwardCompatibleBundle(sender, e);
_logger.Write($"{nameof(e.MissingFromCache)} = {e.MissingFromCache}", true);
_logger.Write($"{nameof(e.HResult)} = {ErrorHelper.HResultToMessage(e.HResult)}", true);
_logger.Write($"{nameof(DetectPhase)}: {nameof(OnDetectForwardCompatibleBundle)} -------^");
}
catch (PhaseException)
{
throw;
}
catch (Exception ex)
{
_logger.Write(ex);
throw new PhaseException(ex);
}
}
/// <inheritdoc />
public override void OnDetectPackageBegin(object sender, DetectPackageBeginEventArgs e)
{
try
{
_logger.Write($"{nameof(DetectPhase)}: {nameof(OnDetectPackageBegin)} -------v");
_logger.Write($"{nameof(e.PackageId)} = {e.PackageId}", true);
base.OnDetectPackageBegin(sender, e);
_logger.Write($"{nameof(e.HResult)} = {ErrorHelper.HResultToMessage(e.HResult)}", true);
_logger.Write($"{nameof(DetectPhase)}: {nameof(OnDetectPackageBegin)} -------^");
}
catch (PhaseException)
{
throw;
}
catch (Exception ex)
{
_logger.Write(ex);
throw new PhaseException(ex);
}
}
/// <inheritdoc />
public override void OnDetectPackageComplete(object sender, DetectPackageCompleteEventArgs e)
{
try
{
_logger.Write($"{nameof(DetectPhase)}: {nameof(OnDetectPackageComplete)} -------v");
_logger.Write($"{nameof(e.Status)} = {ErrorHelper.HResultToMessage(e.Status)}", true);
_logger.Write($"{nameof(e.PackageId)} = {e.PackageId}", true);
_logger.Write($"{nameof(e.State)} = {e.State}", true);
_logger.Write($"{nameof(e.Cached)} = {e.Cached}", true);
base.OnDetectPackageComplete(sender, e);
_logger.Write($"{nameof(e.HResult)} = {ErrorHelper.HResultToMessage(e.HResult)}", true);
_logger.Write($"{nameof(DetectPhase)}: {nameof(OnDetectPackageComplete)} -------^");
}
catch (PhaseException)
{
throw;
}
catch (Exception ex)
{
_logger.Write(ex);
throw new PhaseException(ex);
}
}
/// <inheritdoc />
public override void OnDetectPatchTarget(object sender, DetectPatchTargetEventArgs e)
{
try
{
_logger.Write($"{nameof(DetectPhase)}: {nameof(OnDetectPatchTarget)} -------v");
_logger.Write($"{nameof(e.PackageId)} = {e.PackageId}", true);
_logger.Write($"{nameof(e.ProductCode)} = {e.ProductCode}", true);
_logger.Write($"{nameof(e.State)} = {e.State}", true);
base.OnDetectPatchTarget(sender, e);
_logger.Write($"{nameof(e.HResult)} = {ErrorHelper.HResultToMessage(e.HResult)}", true);
_logger.Write($"{nameof(DetectPhase)}: {nameof(OnDetectPatchTarget)} -------^");
}
catch (PhaseException)
{
throw;
}
catch (Exception ex)
{
_logger.Write(ex);
throw new PhaseException(ex);
}
}
/// <inheritdoc />
public override void OnDetectCompatibleMsiPackage(object sender, DetectCompatibleMsiPackageEventArgs e)
{
try
{
_logger.Write($"{nameof(DetectPhase)}: {nameof(OnDetectCompatibleMsiPackage)} -------v");
_logger.Write($"{nameof(e.PackageId)} = {e.PackageId}", true);
_logger.Write($"{nameof(e.CompatiblePackageId)} = {e.CompatiblePackageId}", true);
_logger.Write($"{nameof(e.CompatiblePackageVersion)} = {e.CompatiblePackageVersion}", true);
base.OnDetectCompatibleMsiPackage(sender, e);
_logger.Write($"{nameof(e.HResult)} = {ErrorHelper.HResultToMessage(e.HResult)}", true);
_logger.Write($"{nameof(DetectPhase)}: {nameof(OnDetectCompatibleMsiPackage)} -------^");
}
catch (PhaseException)
{
throw;
}
catch (Exception ex)
{
_logger.Write(ex);
throw new PhaseException(ex);
}
}
/// <inheritdoc />
public override void OnDetectMsiFeature(object sender, DetectMsiFeatureEventArgs e)
{
try
{
_logger.Write($"{nameof(DetectPhase)}: {nameof(OnDetectMsiFeature)} -------v");
_logger.Write($"{nameof(e.PackageId)} = {e.PackageId}", true);
_logger.Write($"{nameof(e.FeatureId)} = {e.FeatureId}", true);
_logger.Write($"{nameof(e.State)} = {e.State}", true);
base.OnDetectMsiFeature(sender, e);
_logger.Write($"{nameof(e.HResult)} = {ErrorHelper.HResultToMessage(e.HResult)}", true);
_logger.Write($"{nameof(DetectPhase)}: {nameof(OnDetectMsiFeature)} -------^");
}
catch (PhaseException)
{
throw;
}
catch (Exception ex)
{
_logger.Write(ex);
throw new PhaseException(ex);
}
}
}

View File

@@ -0,0 +1,451 @@
using Bootstrapper.Models;
using Bootstrapper.Models.Util;
using System;
using WixToolset.BootstrapperApplicationApi;
namespace Bootstrapper.Phases;
internal class LoggingPlanPhase : PlanPhase
{
private readonly Log _logger;
public LoggingPlanPhase(Model model)
: base(model)
{
_logger = model.Log;
}
/// <inheritdoc />
public override void OnPlanBegin(object sender, PlanBeginEventArgs e)
{
try
{
_logger.Write($"{nameof(PlanPhase)}: {nameof(OnPlanBegin)} -------v");
_logger.Write($"{nameof(e.PackageCount)} = {e.PackageCount}", true);
base.OnPlanBegin(sender, e);
_logger.Write($"{nameof(e.HResult)} = {ErrorHelper.HResultToMessage(e.HResult)}", true);
_logger.Write($"{nameof(PlanPhase)}: {nameof(OnPlanBegin)} -------^");
}
catch (PhaseException)
{
throw;
}
catch (Exception ex)
{
_logger.Write(ex);
throw new PhaseException(ex);
}
}
/// <inheritdoc />
public override void OnPlanComplete(object sender, PlanCompleteEventArgs e)
{
try
{
_logger.Write($"{nameof(PlanPhase)}: {nameof(OnPlanComplete)} -------v");
_logger.Write($"{nameof(e.Status)} = {ErrorHelper.HResultToMessage(e.Status)}", true);
base.OnPlanComplete(sender, e);
_logger.Write($"{nameof(e.HResult)} = {ErrorHelper.HResultToMessage(e.HResult)}", true);
_logger.Write($"{nameof(PlanPhase)}: {nameof(OnPlanComplete)} -------^");
}
catch (PhaseException)
{
throw;
}
catch (Exception ex)
{
_logger.Write(ex);
throw new PhaseException(ex);
}
}
/// <inheritdoc />
public override void OnPlanPackageBegin(object sender, PlanPackageBeginEventArgs e)
{
try
{
_logger.Write($"{nameof(PlanPhase)}: {nameof(OnPlanPackageBegin)} -------v");
_logger.Write($"{nameof(e.PackageId)} = {e.PackageId}", true);
_logger.Write($"{nameof(e.CurrentState)} = {e.CurrentState}", true);
_logger.Write($"{nameof(e.RecommendedState)} = {e.RecommendedState}", true);
_logger.Write($"{nameof(e.RepairCondition)} = {e.RepairCondition}", true);
_logger.Write($"{nameof(e.Cached)} = {e.Cached}", true);
_logger.Write($"{nameof(e.RecommendedCacheType)} = {e.RecommendedCacheType}", true);
_logger.Write($"{nameof(e.InstallCondition)} = {e.InstallCondition}", true);
base.OnPlanPackageBegin(sender, e);
_logger.Write($"{nameof(e.State)} = {e.State}", true);
_logger.Write($"{nameof(e.CacheType)} = {e.CacheType}", true);
_logger.Write($"{nameof(e.HResult)} = {ErrorHelper.HResultToMessage(e.HResult)}", true);
_logger.Write($"{nameof(PlanPhase)}: {nameof(OnPlanPackageBegin)} -------^");
}
catch (PhaseException)
{
throw;
}
catch (Exception ex)
{
_logger.Write(ex);
throw new PhaseException(ex);
}
}
/// <inheritdoc />
public override void OnPlanPackageComplete(object sender, PlanPackageCompleteEventArgs e)
{
try
{
_logger.Write($"{nameof(PlanPhase)}: {nameof(OnPlanPackageComplete)} -------v");
_logger.Write($"{nameof(e.Status)} = {ErrorHelper.HResultToMessage(e.Status)}", true);
_logger.Write($"{nameof(e.PackageId)} = {e.PackageId}", true);
_logger.Write($"{nameof(e.Requested)} = {e.Requested}", true);
base.OnPlanPackageComplete(sender, e);
_logger.Write($"{nameof(e.HResult)} = {ErrorHelper.HResultToMessage(e.HResult)}", true);
_logger.Write($"{nameof(PlanPhase)}: {nameof(OnPlanPackageComplete)} -------^");
}
catch (PhaseException)
{
throw;
}
catch (Exception ex)
{
_logger.Write(ex);
throw new PhaseException(ex);
}
}
/// <inheritdoc />
public override void OnPlanCompatibleMsiPackageBegin(object sender, PlanCompatibleMsiPackageBeginEventArgs e)
{
try
{
_logger.Write($"{nameof(PlanPhase)}: {nameof(OnPlanCompatibleMsiPackageBegin)} -------v");
_logger.Write($"{nameof(e.PackageId)} = {e.PackageId}", true);
_logger.Write($"{nameof(e.CompatiblePackageId)} = {e.CompatiblePackageId}", true);
_logger.Write($"{nameof(e.CompatiblePackageVersion)} = {e.CompatiblePackageVersion}", true);
_logger.Write($"{nameof(e.RecommendedRemove)} = {e.RecommendedRemove}", true);
base.OnPlanCompatibleMsiPackageBegin(sender, e);
_logger.Write($"{nameof(e.RequestRemove)} = {e.RequestRemove}", true);
_logger.Write($"{nameof(e.HResult)} = {ErrorHelper.HResultToMessage(e.HResult)}", true);
_logger.Write($"{nameof(PlanPhase)}: {nameof(OnPlanCompatibleMsiPackageBegin)} -------^");
}
catch (PhaseException)
{
throw;
}
catch (Exception ex)
{
_logger.Write(ex);
throw new PhaseException(ex);
}
}
/// <inheritdoc />
public override void OnPlanCompatibleMsiPackageComplete(object sender, PlanCompatibleMsiPackageCompleteEventArgs e)
{
try
{
_logger.Write($"{nameof(PlanPhase)}: {nameof(OnPlanCompatibleMsiPackageComplete)} -------v");
_logger.Write($"{nameof(e.Status)} = {ErrorHelper.HResultToMessage(e.Status)}", true);
_logger.Write($"{nameof(e.PackageId)} = {e.PackageId}", true);
_logger.Write($"{nameof(e.CompatiblePackageId)} = {e.CompatiblePackageId}", true);
_logger.Write($"{nameof(e.RequestedRemove)} = {e.RequestedRemove}", true);
base.OnPlanCompatibleMsiPackageComplete(sender, e);
_logger.Write($"{nameof(e.HResult)} = {ErrorHelper.HResultToMessage(e.HResult)}", true);
_logger.Write($"{nameof(PlanPhase)}: {nameof(OnPlanCompatibleMsiPackageComplete)} -------^");
}
catch (PhaseException)
{
throw;
}
catch (Exception ex)
{
_logger.Write(ex);
throw new PhaseException(ex);
}
}
/// <inheritdoc />
public override void OnPlanRollbackBoundary(object sender, PlanRollbackBoundaryEventArgs e)
{
try
{
_logger.Write($"{nameof(PlanPhase)}: {nameof(OnPlanRollbackBoundary)} -------v");
_logger.Write($"{nameof(e.RollbackBoundaryId)} = {e.RollbackBoundaryId}", true);
_logger.Write($"{nameof(e.RecommendedTransaction)} = {e.RecommendedTransaction}", true);
base.OnPlanRollbackBoundary(sender, e);
_logger.Write($"{nameof(e.Transaction)} = {e.Transaction}", true);
_logger.Write($"{nameof(e.HResult)} = {ErrorHelper.HResultToMessage(e.HResult)}", true);
_logger.Write($"{nameof(PlanPhase)}: {nameof(OnPlanRollbackBoundary)} -------^");
}
catch (PhaseException)
{
throw;
}
catch (Exception ex)
{
_logger.Write(ex);
throw new PhaseException(ex);
}
}
/// <inheritdoc />
public override void OnPlanRelatedBundle(object sender, PlanRelatedBundleEventArgs e)
{
try
{
_logger.Write($"{nameof(PlanPhase)}: {nameof(OnPlanRelatedBundle)} -------v");
_logger.Write($"{nameof(e.BundleId)} = {e.BundleId}", true);
_logger.Write($"{nameof(e.RecommendedState)} = {e.RecommendedState}", true);
base.OnPlanRelatedBundle(sender, e);
_logger.Write($"{nameof(e.State)} = {e.State}", true);
_logger.Write($"{nameof(e.HResult)} = {ErrorHelper.HResultToMessage(e.HResult)}", true);
_logger.Write($"{nameof(PlanPhase)}: {nameof(OnPlanRelatedBundle)} -------^");
}
catch (PhaseException)
{
throw;
}
catch (Exception ex)
{
_logger.Write(ex);
throw new PhaseException(ex);
}
}
/// <inheritdoc />
public override void OnPlanRelatedBundleType(object sender, PlanRelatedBundleTypeEventArgs e)
{
try
{
_logger.Write($"{nameof(PlanPhase)}: {nameof(OnPlanRelatedBundleType)} -------v");
_logger.Write($"{nameof(e.BundleId)} = {e.BundleId}", true);
_logger.Write($"{nameof(e.RecommendedType)} = {e.RecommendedType}", true);
base.OnPlanRelatedBundleType(sender, e);
_logger.Write($"{nameof(e.Type)} = {e.Type}", true);
_logger.Write($"{nameof(e.HResult)} = {ErrorHelper.HResultToMessage(e.HResult)}", true);
_logger.Write($"{nameof(PlanPhase)}: {nameof(OnPlanRelatedBundleType)} -------^");
}
catch (PhaseException)
{
throw;
}
catch (Exception ex)
{
_logger.Write(ex);
throw new PhaseException(ex);
}
}
/// <inheritdoc />
public override void OnPlanRestoreRelatedBundle(object sender, PlanRestoreRelatedBundleEventArgs e)
{
try
{
_logger.Write($"{nameof(PlanPhase)}: {nameof(OnPlanRestoreRelatedBundle)} -------v");
_logger.Write($"{nameof(e.BundleId)} = {e.BundleId}", true);
_logger.Write($"{nameof(e.RecommendedState)} = {e.RecommendedState}", true);
base.OnPlanRestoreRelatedBundle(sender, e);
_logger.Write($"{nameof(e.State)} = {e.State}", true);
_logger.Write($"{nameof(e.HResult)} = {ErrorHelper.HResultToMessage(e.HResult)}", true);
_logger.Write($"{nameof(PlanPhase)}: {nameof(OnPlanRestoreRelatedBundle)} -------^");
}
catch (PhaseException)
{
throw;
}
catch (Exception ex)
{
_logger.Write(ex);
throw new PhaseException(ex);
}
}
/// <inheritdoc />
public override void OnPlanForwardCompatibleBundle(object sender, PlanForwardCompatibleBundleEventArgs e)
{
try
{
_logger.Write($"{nameof(PlanPhase)}: {nameof(OnPlanForwardCompatibleBundle)} -------v");
_logger.Write($"{nameof(e.BundleId)} = {e.BundleId}", true);
_logger.Write($"{nameof(e.Version)} = {e.Version}", true);
_logger.Write($"{nameof(e.PerMachine)} = {e.PerMachine}", true);
_logger.Write($"{nameof(e.RelationType)} = {e.RelationType}", true);
_logger.Write($"{nameof(e.BundleTag)} = {e.BundleTag}", true);
_logger.Write($"{nameof(e.RecommendedIgnoreBundle)} = {e.RecommendedIgnoreBundle}", true);
base.OnPlanForwardCompatibleBundle(sender, e);
_logger.Write($"{nameof(e.IgnoreBundle)} = {e.IgnoreBundle}", true);
_logger.Write($"{nameof(e.HResult)} = {ErrorHelper.HResultToMessage(e.HResult)}", true);
_logger.Write($"{nameof(PlanPhase)}: {nameof(OnPlanForwardCompatibleBundle)} -------^");
}
catch (PhaseException)
{
throw;
}
catch (Exception ex)
{
_logger.Write(ex);
throw new PhaseException(ex);
}
}
/// <inheritdoc />
public override void OnPlanMsiPackage(object sender, PlanMsiPackageEventArgs e)
{
try
{
_logger.Write($"{nameof(PlanPhase)}: {nameof(OnPlanMsiPackage)} -------v");
_logger.Write($"{nameof(e.Action)} = {e.Action}", true);
_logger.Write($"{nameof(e.PackageId)} = {e.PackageId}", true);
_logger.Write($"{nameof(e.RecommendedFileVersioning)} = {e.RecommendedFileVersioning}", true);
_logger.Write($"{nameof(e.ShouldExecute)} = {e.ShouldExecute}", true);
base.OnPlanMsiPackage(sender, e);
_logger.Write($"{nameof(e.ActionMsiProperty)} = {e.ActionMsiProperty}", true);
_logger.Write($"{nameof(e.FileVersioning)} = {e.FileVersioning}", true);
_logger.Write($"{nameof(e.DisableExternalUiHandler)} = {e.DisableExternalUiHandler}", true);
_logger.Write($"{nameof(e.UiLevel)} = {e.UiLevel}", true);
_logger.Write($"{nameof(e.HResult)} = {ErrorHelper.HResultToMessage(e.HResult)}", true);
_logger.Write($"{nameof(PlanPhase)}: {nameof(OnPlanMsiPackage)} -------^");
}
catch (PhaseException)
{
throw;
}
catch (Exception ex)
{
_logger.Write(ex);
throw new PhaseException(ex);
}
}
/// <inheritdoc />
public override void OnPlanMsiFeature(object sender, PlanMsiFeatureEventArgs e)
{
try
{
_logger.Write($"{nameof(PlanPhase)}: {nameof(OnPlanMsiFeature)} -------v");
_logger.Write($"{nameof(e.PackageId)} = {e.PackageId}", true);
_logger.Write($"{nameof(e.FeatureId)} = {e.FeatureId}", true);
_logger.Write($"{nameof(e.RecommendedState)} = {e.RecommendedState}", true);
base.OnPlanMsiFeature(sender, e);
_logger.Write($"{nameof(e.State)} = {e.State}", true);
_logger.Write($"{nameof(e.HResult)} = {ErrorHelper.HResultToMessage(e.HResult)}", true);
_logger.Write($"{nameof(PlanPhase)}: {nameof(OnPlanMsiFeature)} -------^");
}
catch (PhaseException)
{
throw;
}
catch (Exception ex)
{
_logger.Write(ex);
throw new PhaseException(ex);
}
}
/// <inheritdoc />
public override void OnPlanPatchTarget(object sender, PlanPatchTargetEventArgs e)
{
try
{
_logger.Write($"{nameof(PlanPhase)}: {nameof(OnPlanPatchTarget)} -------v");
_logger.Write($"{nameof(e.PackageId)} = {e.PackageId}", true);
_logger.Write($"{nameof(e.ProductCode)} = {e.ProductCode}", true);
_logger.Write($"{nameof(e.RecommendedState)} = {e.RecommendedState}", true);
base.OnPlanPatchTarget(sender, e);
_logger.Write($"{nameof(e.State)} = {e.State}", true);
_logger.Write($"{nameof(e.HResult)} = {ErrorHelper.HResultToMessage(e.HResult)}", true);
_logger.Write($"{nameof(PlanPhase)}: {nameof(OnPlanPatchTarget)} -------^");
}
catch (PhaseException)
{
throw;
}
catch (Exception ex)
{
_logger.Write(ex);
throw new PhaseException(ex);
}
}
/// <inheritdoc />
public override void OnPlannedPackage(object sender, PlannedPackageEventArgs e)
{
try
{
_logger.Write($"{nameof(PlanPhase)}: {nameof(OnPlannedPackage)} -------v");
_logger.Write($"{nameof(e.PackageId)} = {e.PackageId}", true);
_logger.Write($"{nameof(e.Execute)} = {e.Execute}", true);
_logger.Write($"{nameof(e.Rollback)} = {e.Rollback}", true);
_logger.Write($"{nameof(e.Cache)} = {e.Cache}", true);
_logger.Write($"{nameof(e.Uncache)} = {e.Uncache}", true);
base.OnPlannedPackage(sender, e);
_logger.Write($"{nameof(e.HResult)} = {ErrorHelper.HResultToMessage(e.HResult)}", true);
_logger.Write($"{nameof(PlanPhase)}: {nameof(OnPlannedPackage)} -------^");
}
catch (PhaseException)
{
throw;
}
catch (Exception ex)
{
_logger.Write(ex);
throw new PhaseException(ex);
}
}
/// <inheritdoc />
public override void OnPlannedCompatiblePackage(object sender, PlannedCompatiblePackageEventArgs e)
{
try
{
_logger.Write($"{nameof(PlanPhase)}: {nameof(OnPlannedCompatiblePackage)} -------v");
_logger.Write($"{nameof(e.PackageId)} = {e.PackageId}", true);
_logger.Write($"{nameof(e.CompatiblePackageId)} = {e.CompatiblePackageId}", true);
_logger.Write($"{nameof(e.Remove)} = {e.Remove}", true);
base.OnPlannedCompatiblePackage(sender, e);
_logger.Write($"{nameof(e.HResult)} = {ErrorHelper.HResultToMessage(e.HResult)}", true);
_logger.Write($"{nameof(PlanPhase)}: {nameof(OnPlannedCompatiblePackage)} -------^");
}
catch (PhaseException)
{
throw;
}
catch (Exception ex)
{
_logger.Write(ex);
throw new PhaseException(ex);
}
}
}

View File

@@ -0,0 +1,199 @@
using Bootstrapper.Models;
using Bootstrapper.Models.Util;
using System;
using WixToolset.BootstrapperApplicationApi;
namespace Bootstrapper.Phases;
internal class PlanPhase
{
private readonly Model _model;
public PlanPhase(Model model)
{
_model = model;
}
public event EventHandler<EventArgs> PlanPhaseFailed;
/// <summary>
/// Fired when the engine has begun planning the installation.
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
/// <exception cref="PhaseException"></exception>
public virtual void OnPlanBegin(object sender, PlanBeginEventArgs e)
{
try
{
_model.State.PhaseResult = 0;
_model.State.ErrorMessage = string.Empty;
if (e.Cancel)
return;
_model.State.BaStatus = BaStatus.Planning;
}
catch (Exception ex)
{
_model.Log.Write(ex);
throw new PhaseException(ex);
}
}
/// <summary>
/// Fired when the engine has completed planning the installation.
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
/// <exception cref="PhaseException"></exception>
public virtual void OnPlanComplete(object sender, PlanCompleteEventArgs e)
{
try
{
_model.State.PhaseResult = e.Status;
if (_model.State.BaStatus == BaStatus.Cancelled || e.Status == ErrorHelper.CancelHResult)
{
_model.State.BaStatus = BaStatus.Cancelled;
_model.Log.Write("User cancelled");
}
else if (ErrorHelper.HResultIsFailure(e.Status))
{
_model.State.BaStatus = BaStatus.Failed;
var msg = $"Plan failed - {ErrorHelper.HResultToMessage(e.Status)}";
if (string.IsNullOrEmpty(_model.State.ErrorMessage))
_model.State.ErrorMessage = msg;
_model.Log.Write(msg);
if (_model.UiFacade.IsUiShown)
PlanPhaseFailed?.Invoke(this, EventArgs.Empty);
else
_model.UiFacade.ShutDown();
}
else
{
_model.Log.Write("Plan succeeded, starting apply phase");
_model.Engine.Apply(_model.UiFacade.ShellWindowHandle);
}
}
catch (Exception ex)
{
_model.Log.Write(ex);
throw new PhaseException(ex);
}
}
/// <summary>
/// Fired when the engine has begun getting the BA's input for planning a package.
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
public virtual void OnPlanPackageBegin(object sender, PlanPackageBeginEventArgs e)
{ }
/// <summary>
/// Fired when the engine has completed getting the BA's input for planning a package.
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
public virtual void OnPlanPackageComplete(object sender, PlanPackageCompleteEventArgs e)
{ }
/// <summary>
/// Fired when the engine plans a new, compatible package using the same provider key.
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
public virtual void OnPlanCompatibleMsiPackageBegin(object sender, PlanCompatibleMsiPackageBeginEventArgs e)
{ }
/// <summary>
/// Fired when the engine has completed planning the installation of a specific package.
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
public virtual void OnPlanCompatibleMsiPackageComplete(object sender, PlanCompatibleMsiPackageCompleteEventArgs e)
{ }
/// <summary>
/// Fired when the engine is planning a rollback boundary.
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
public virtual void OnPlanRollbackBoundary(object sender, PlanRollbackBoundaryEventArgs e)
{ }
/// <summary>
/// Fired when the engine has begun planning for a related bundle.
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
public virtual void OnPlanRelatedBundle(object sender, PlanRelatedBundleEventArgs e)
{ }
/// <summary>
/// Fired when the engine has begun planning the related bundle relation type.
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
public virtual void OnPlanRelatedBundleType(object sender, PlanRelatedBundleTypeEventArgs e)
{ }
/// <summary>
/// Fired when the engine has begun planning an upgrade related bundle for restoring in case of failure.
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
public virtual void OnPlanRestoreRelatedBundle(object sender, PlanRestoreRelatedBundleEventArgs e)
{ }
/// <summary>
/// Fired when the engine is about to plan a forward compatible bundle.
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
public virtual void OnPlanForwardCompatibleBundle(object sender, PlanForwardCompatibleBundleEventArgs e)
{ }
/// <summary>
/// Fired when the engine is planning an MSI or MSP package.
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
public virtual void OnPlanMsiPackage(object sender, PlanMsiPackageEventArgs e)
{ }
/// <summary>
/// Fired when the engine is about to plan a feature in an MSI package.
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
public virtual void OnPlanMsiFeature(object sender, PlanMsiFeatureEventArgs e)
{ }
/// <summary>
/// Fired when the engine is about to plan a target of an MSP package.
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
public virtual void OnPlanPatchTarget(object sender, PlanPatchTargetEventArgs e)
{ }
/// <summary>
/// Fired when the engine has completed planning a package.
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
public virtual void OnPlannedPackage(object sender, PlannedPackageEventArgs e)
{ }
/// <summary>
/// Fired when the engine has completed planning a compatible package.
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
public virtual void OnPlannedCompatiblePackage(object sender, PlannedCompatiblePackageEventArgs e)
{ }
}

View File

@@ -0,0 +1,194 @@
using Bootstrapper.Models;
using Bootstrapper.Models.State;
using System;
using WixToolset.BootstrapperApplicationApi;
namespace Bootstrapper.Phases;
internal class ProgressHandler
{
private readonly Model _model;
private readonly object _progressLock = new();
private int _progressStages;
private int _cacheProgress;
private int _executeProgress;
public ProgressHandler(Model model)
{
_model = model;
}
public void OnPlanBegin(object sender, PlanBeginEventArgs e)
{
try
{
var action = _model.State.PlannedAction.ToString().ToLower();
ReportProgress($"Planning {action}");
}
catch (Exception ex)
{
_model.Log.Write(ex);
throw;
}
}
public void OnApplyBegin(object sender, ApplyBeginEventArgs e)
{
try
{
lock (_progressLock)
_progressStages = e.PhaseCount;
ReportProgress("Applying changes");
}
catch (Exception ex)
{
_model.Log.Write(ex);
throw;
}
}
public void OnCacheAcquireProgress(object sender, CacheAcquireProgressEventArgs e)
{
try
{
lock (_progressLock)
_cacheProgress = e.OverallPercentage;
ReportProgress("Retrieving", e.PackageOrContainerId);
}
catch (Exception ex)
{
_model.Log.Write(ex);
throw;
}
}
public void OnCachePayloadExtractProgress(object sender, CachePayloadExtractProgressEventArgs e)
{
try
{
lock (_progressLock)
_cacheProgress = e.OverallPercentage;
ReportProgress("Extracting", e.PackageOrContainerId);
}
catch (Exception ex)
{
_model.Log.Write(ex);
throw;
}
}
public void OnCacheVerifyProgress(object sender, CacheVerifyProgressEventArgs e)
{
try
{
lock (_progressLock)
_cacheProgress = e.OverallPercentage;
ReportProgress("Verifying", e.PackageOrContainerId);
}
catch (Exception ex)
{
_model.Log.Write(ex);
throw;
}
}
public void OnCacheContainerOrPayloadVerifyProgress(object sender, CacheContainerOrPayloadVerifyProgressEventArgs e)
{
try
{
lock (_progressLock)
_cacheProgress = e.OverallPercentage;
ReportProgress("Verifying", e.PackageOrContainerId);
}
catch (Exception ex)
{
_model.Log.Write(ex);
}
}
public void OnCacheComplete(object sender, CacheCompleteEventArgs e)
{
try
{
lock (_progressLock)
_cacheProgress = 100;
ReportProgress();
}
catch (Exception ex)
{
_model.Log.Write(ex);
throw;
}
}
public void OnApplyExecuteProgress(object sender, ExecuteProgressEventArgs e)
{
try
{
int overallProgress;
lock (_progressLock)
{
_executeProgress = e.OverallPercentage;
overallProgress = CalculateProgress();
}
ReportProgress(null, e.PackageId);
if (_model.State.Display == Display.Embedded)
_model.Engine.SendEmbeddedProgress(e.ProgressPercentage, overallProgress);
}
catch (Exception ex)
{
_model.Log.Write(ex);
throw;
}
}
public void OnExecutePackageComplete(object sender, ExecutePackageCompleteEventArgs e)
{
try
{
// clear display
ReportProgress(string.Empty, string.Empty);
}
catch (Exception ex)
{
_model.Log.Write(ex);
throw;
}
}
private void ReportProgress(string message = null, string packageId = null)
{
if (_model.UiFacade.ProgressReporter != null)
{
var report = new ProgressReport
{
Message = message,
Progress = CalculateProgress()
};
if (packageId != null)
report.PackageName = _model.State.GetPackageName(packageId);
_model.UiFacade.ProgressReporter.Report(report);
}
}
private int CalculateProgress()
{
lock (_progressLock)
{
if (_progressStages > 0)
return (_cacheProgress + _executeProgress) / _progressStages;
return 0;
}
}
}

View File

@@ -0,0 +1,25 @@
using Bootstrapper.Models.Util;
using System;
using WixToolset.BootstrapperApplicationApi;
namespace Bootstrapper;
internal class Program
{
private static int Main()
{
int exitCode;
try
{
var application = new BootstrapperApp();
ManagedBootstrapperApplication.Run(application);
exitCode = application.ExitCode;
}
catch (Exception ex)
{
exitCode = ErrorHelper.HResultToWin32(ex.HResult);
}
return exitCode;
}
}

View File

@@ -0,0 +1,29 @@
using Bootstrapper.Models;
using Bootstrapper.Models.Util;
using Bootstrapper.ViewModels.Util;
namespace Bootstrapper.ViewModels
{
internal class CancelViewModel : ViewModelBase
{
private readonly Model _model;
public CancelViewModel(Model model)
{
_model = model;
CancelCommand = new DelegateCommand(Cancel, CanCancel);
}
public IDelegateCommand CancelCommand { get; }
private void Cancel()
{
_model.State.CancelRequested = true;
}
private bool CanCancel()
{
return !_model.State.CancelRequested && (_model.State.BaStatus == BaStatus.Planning || _model.State.BaStatus == BaStatus.Applying);
}
}
}

View File

@@ -0,0 +1,41 @@
using Bootstrapper.Models;
using Bootstrapper.Models.Util;
using Bootstrapper.ViewModels.Util;
namespace Bootstrapper.ViewModels
{
internal class ConfigViewModel : ViewModelBase
{
private readonly Model _model;
public ConfigViewModel(Model model)
{
_model = model;
}
/// <summary>
/// An example exposing a bundle variable to the UI.
/// </summary>
public string SampleOption
{
get
{
if (_model.Engine.ContainsVariable("SampleOption"))
return _model.Engine.GetVariableString("SampleOption");
return string.Empty;
}
set
{
_model.Engine.SetVariableString("SampleOption", value, false);
OnPropertyChanged();
}
}
public void AfterDetect()
{
if (_model.State.RelatedBundleStatus != BundleStatus.NotInstalled)
SampleOption = "Installed";
}
}
}

View File

@@ -0,0 +1,74 @@
using Bootstrapper.Models.State;
using Bootstrapper.ViewModels.Util;
using System;
namespace Bootstrapper.ViewModels
{
internal class ProgressViewModel : ViewModelBase
{
private string _message;
private string _package;
private int _progress;
public string Message
{
get => _message;
set
{
if (_message == value)
return;
_message = value;
base.OnPropertyChanged();
}
}
public string Package
{
get => _package;
set
{
if (_package == value)
return;
_package = value;
base.OnPropertyChanged();
if (string.IsNullOrWhiteSpace(_package))
Message = string.Empty;
else
Message = $"Processing: {_package}";
}
}
public int Progress
{
get => _progress;
set
{
if (Math.Abs(_progress - value) < 0.0001)
return;
_progress = value;
base.OnPropertyChanged();
}
}
public void ProcessProgressReport(ProgressReport report)
{
Progress = report.Progress;
if (report.Message != null)
Message = report.Message;
if (report.PackageName != null)
Package = report.PackageName;
}
public void Reset()
{
Message = string.Empty;
Package = string.Empty;
Progress = 0;
}
}
}

View File

@@ -0,0 +1,375 @@
using Bootstrapper.Models;
using Bootstrapper.Models.Util;
using Bootstrapper.ViewModels.Util;
using System;
using System.ComponentModel;
using System.Windows;
using System.Windows.Input;
using WixToolset.BootstrapperApplicationApi;
namespace Bootstrapper.ViewModels;
internal class ShellViewModel : ViewModelBase
{
private readonly Model _model;
private readonly CancelViewModel _cancelVm;
private readonly IDelegateCommand _installCommand;
private readonly IDelegateCommand _updateCommand;
private readonly IDelegateCommand _uninstallCommand;
private bool _isRepairAvailable;
private IDelegateCommand _executeCommand;
private string _executeDescription;
private string _message;
public ShellViewModel(Model model)
{
_model = model;
_cancelVm = new CancelViewModel(model);
_installCommand = new DelegateCommand(Install, CanInstall);
_uninstallCommand = new DelegateCommand(Uninstall, CanUninstall);
_updateCommand = new DelegateCommand(Update, CanUpdate);
RepairCommand = new DelegateCommand(Repair, CanRepair);
ExitCommand = new DelegateCommand(Exit, CanExit);
ShowLogCommand = new DelegateCommand(ShowLog, CanShowLog);
ConfigVm = new ConfigViewModel(model);
ConfigVm.PropertyChanged += ConfigVm_PropertyChanged;
ProgressVm = new ProgressViewModel();
}
public ConfigViewModel ConfigVm { get; }
public ProgressViewModel ProgressVm { get; }
public IDelegateCommand ShowLogCommand { get; }
public IDelegateCommand ExitCommand { get; }
public IDelegateCommand RepairCommand { get; }
public IDelegateCommand CancelCommand => _cancelVm.CancelCommand;
/// <summary>
/// Is installer waiting for user input?
/// </summary>
public bool IsWaiting => _model.State.BaStatus == BaStatus.Waiting;
/// <summary>
/// Is the UI running in passive mode, only displaying a progress bar?
/// </summary>
public bool IsPassive => _model.State.Display == Display.Passive;
/// <summary>
/// The command that will install or uninstall the software
/// </summary>
public IDelegateCommand ExecuteCommand
{
get => _executeCommand;
set
{
if (_executeCommand == value)
return;
_executeCommand = value;
OnPropertyChanged();
}
}
/// <summary>
/// A brief, one-word description of what will happen when the <see cref="ExecuteCommand" /> is run.
/// Should be appropriate for button text.
/// </summary>
public string ExecuteDescription
{
get => _executeDescription;
set
{
if (_executeDescription == value)
return;
_executeDescription = value;
OnPropertyChanged();
}
}
/// <summary>
/// Display the Repair button?
/// </summary>
public bool IsRepairAvailable
{
get => _isRepairAvailable;
set
{
if (_isRepairAvailable == value)
return;
_isRepairAvailable = value;
OnPropertyChanged();
}
}
/// <summary>
/// A message to display to the user.
/// </summary>
public string Message
{
get => _message;
set
{
if (_message == value)
return;
_message = value;
OnPropertyChanged();
}
}
/// <summary>
/// Call after the detect phase completes to refresh the UI.
/// </summary>
/// <param name="followupAction">
/// Indicates which action will be planned when the BA is running silently or in passive mode.
/// Pass <see cref="LaunchAction.Unknown" /> for full UI mode.
/// </param>
public void AfterDetect(LaunchAction followupAction)
{
try
{
OnPropertyChanged(nameof(IsWaiting));
if (_model.State.RelatedBundleStatus == BundleStatus.OlderInstalled || followupAction == LaunchAction.UpdateReplace || followupAction == LaunchAction.UpdateReplaceEmbedded)
{
ExecuteCommand = _updateCommand;
ExecuteDescription = "Update";
}
else if (_model.State.RelatedBundleStatus == BundleStatus.Current || followupAction == LaunchAction.Uninstall || followupAction == LaunchAction.UnsafeUninstall)
{
ExecuteCommand = _uninstallCommand;
ExecuteDescription = "Uninstall";
}
else
{
ExecuteCommand = _installCommand;
ExecuteDescription = "Install";
}
IsRepairAvailable = _model.State.RelatedBundleStatus == BundleStatus.Current;
AssignMessage();
ConfigVm.AfterDetect();
}
catch (Exception ex)
{
_model.Log.Write(ex);
Message = $"Error: {ex.Message}";
_model.UiFacade.ShowMessageBox($"Error: {ex.Message}", MessageBoxButton.OK, MessageBoxImage.Error, MessageBoxResult.OK);
}
finally
{
CommandManager.InvalidateRequerySuggested();
}
}
/// <summary>
/// Call after the apply phase completes to refresh the UI.
/// </summary>
public void AfterApply()
{
try
{
ProgressVm.Reset();
AssignMessage();
}
catch (Exception ex)
{
_model.Log.Write(ex);
Message = $"Error: {ex.Message}";
_model.UiFacade.ShowMessageBox($"Error: {ex.Message}", MessageBoxButton.OK, MessageBoxImage.Error, MessageBoxResult.OK);
}
finally
{
CommandManager.InvalidateRequerySuggested();
}
}
private void Install()
{
_model.PlanAndApply(LaunchAction.Install);
OnPropertyChanged(nameof(IsWaiting));
CommandManager.InvalidateRequerySuggested();
}
private bool CanInstall()
{
return _model.State.RelatedBundleStatus == BundleStatus.NotInstalled && CanPlanAndApply();
}
private void Update()
{
// Any older bundles that were discovered have already been scheduled for uninstall, so an "upgrade" will be a fresh installation.
_model.PlanAndApply(LaunchAction.Install);
OnPropertyChanged(nameof(IsWaiting));
CommandManager.InvalidateRequerySuggested();
}
private bool CanUpdate()
{
return _model.State.RelatedBundleStatus == BundleStatus.OlderInstalled && CanPlanAndApply();
}
private void Uninstall()
{
_model.PlanAndApply(LaunchAction.Uninstall);
OnPropertyChanged(nameof(IsWaiting));
CommandManager.InvalidateRequerySuggested();
}
private bool CanUninstall()
{
return _model.State.RelatedBundleStatus == BundleStatus.Current && CanPlanAndApply();
}
private void Repair()
{
_model.PlanAndApply(LaunchAction.Repair);
OnPropertyChanged(nameof(IsWaiting));
CommandManager.InvalidateRequerySuggested();
}
private bool CanRepair()
{
return _model.State.RelatedBundleStatus == BundleStatus.Current && CanPlanAndApply();
}
private void Exit()
{
_model.UiFacade.ShutDown();
}
private bool CanExit()
{
return _model.State.BaStatus == BaStatus.Failed || _model.State.BaStatus == BaStatus.Cancelled || _model.State.BaStatus == BaStatus.Applied || _model.State.BaStatus == BaStatus.Waiting;
;
}
private void ShowLog()
{
_model.ShowLog();
}
private bool CanShowLog()
{
return _model.State.BaStatus == BaStatus.Failed || _model.State.BaStatus == BaStatus.Cancelled || _model.State.BaStatus == BaStatus.Applied || _model.State.BaStatus == BaStatus.Waiting;
;
}
private void ConfigVm_PropertyChanged(object sender, PropertyChangedEventArgs e)
{
RepairCommand.RaiseCanExecuteChanged();
ExecuteCommand?.RaiseCanExecuteChanged();
}
private bool CanPlanAndApply()
{
// Ensure ConfigVm is not displaying any data validation errors.
return _model.State.BaStatus == BaStatus.Waiting && string.IsNullOrEmpty(ConfigVm.Error);
}
/// <summary>
/// Will assign a value to <see cref="Message" /> based on the current state.
/// This should be called after Detect, and again after Apply.
/// </summary>
/// <exception cref="ArgumentOutOfRangeException"></exception>
private void AssignMessage()
{
switch (_model.State.BaStatus)
{
case BaStatus.Cancelled:
Message = "User cancelled";
break;
case BaStatus.Failed:
if (!string.IsNullOrWhiteSpace(_model.State.ErrorMessage))
Message = $"Failed: {_model.State.ErrorMessage}";
else if (_model.State.CancelRequested)
Message = "User cancelled";
else
Message = "An error occurred. See log for details.";
break;
case BaStatus.Planning:
case BaStatus.Applying:
case BaStatus.Waiting:
// BA will be in one of these states after successfully completing the detect phase.
if (string.IsNullOrEmpty(_model.State.RelatedBundleVersion))
Message = $"Installing v{_model.State.BundleVersion}";
else
{
switch (_model.State.RelatedBundleStatus)
{
case BundleStatus.Unknown:
case BundleStatus.NotInstalled:
Message = $"Installing v{_model.State.BundleVersion}";
break;
case BundleStatus.OlderInstalled:
Message = $"Updating v{_model.State.RelatedBundleVersion} to {_model.State.BundleVersion}";
break;
case BundleStatus.Current:
Message = $"v{_model.State.BundleVersion} is currently installed";
break;
case BundleStatus.NewerInstalled:
Message = $"There is already a newer version (v{_model.State.RelatedBundleVersion}) installed on this machine.";
break;
default:
throw new ArgumentOutOfRangeException(nameof(_model.State.RelatedBundleStatus));
}
}
break;
case BaStatus.Applied:
switch (_model.State.PlannedAction)
{
case LaunchAction.Layout:
Message = $"v{_model.State.BundleVersion} successfully laid out";
break;
case LaunchAction.UnsafeUninstall:
case LaunchAction.Uninstall:
Message = $"v{_model.State.BundleVersion} successfully removed";
break;
case LaunchAction.Modify:
Message = $"v{_model.State.BundleVersion} successfully modified";
break;
case LaunchAction.Repair:
Message = $"v{_model.State.BundleVersion} successfully repaired";
break;
case LaunchAction.UpdateReplace:
case LaunchAction.UpdateReplaceEmbedded:
Message = $"v{_model.State.RelatedBundleVersion} successfully updated to {_model.State.BundleVersion}";
break;
case LaunchAction.Unknown:
case LaunchAction.Help:
case LaunchAction.Cache:
case LaunchAction.Install:
default:
Message = $"v{_model.State.BundleVersion} successfully installed";
break;
}
break;
case BaStatus.Initializing:
case BaStatus.Detecting:
// No reason to display a message
break;
default:
throw new ArgumentOutOfRangeException(nameof(_model.State.BaStatus));
}
}
}

View File

@@ -0,0 +1,69 @@
using System;
using System.Globalization;
using System.Windows;
using System.Windows.Data;
using System.Windows.Markup;
namespace Bootstrapper.ViewModels.Util
{
/// <summary>
/// Converts a <see cref="bool" /> to a <see cref="Visibility" />. If the <see cref="Negate" /> property
/// is <see langword="false" />, then <see langword="true" /> converts to <see cref="Visibility.Visible" />
/// while <see langword="false" /> converts to <see cref="Visibility.Collapsed" />. If <see cref="Negate" />
/// is <see langword="true" />, then the negated bound value is used for the conversion.
/// </summary>
[ValueConversion(typeof(bool), typeof(Visibility))]
public class BooleanVisibilityConverter : MarkupExtension, IValueConverter
{
/// <summary>
/// If <see langword="true" />, will use the negated value of the bound property to perform the conversion. So,
/// <see langword="true" /> will convert to <see cref="Visibility.Collapsed" /> while <see langword="false" />
/// will convert to <see cref="Visibility.Visible" />.
/// </summary>
public bool Negate { get; set; }
/// <inheritdoc />
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
if (targetType != typeof(Visibility) && targetType != typeof(Visibility?))
return Visibility.Collapsed;
var b = value as bool?;
if (b == null)
return Visibility.Collapsed;
if (Negate)
{
if (b.Value)
return Visibility.Collapsed;
return Visibility.Visible;
}
if (b.Value)
return Visibility.Visible;
return Visibility.Collapsed;
}
/// <inheritdoc />
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
if ((targetType == typeof(bool) || targetType == typeof(bool?)) && value is Visibility vis)
{
if (Negate)
return vis != Visibility.Visible;
return vis == Visibility.Visible;
}
return false;
}
/// <inheritdoc />
public override object ProvideValue(IServiceProvider serviceProvider)
{
return this;
}
}
}

View File

@@ -0,0 +1,101 @@
using System;
using System.Collections.Generic;
using System.Windows.Input;
namespace Bootstrapper.ViewModels.Util
{
/// <summary>
/// This class contains methods for the CommandManager that help avoid memory leaks by
/// using weak references.
/// </summary>
internal static class CommandManagerHelper
{
internal static void CallWeakReferenceHandlers(List<WeakReference> handlers)
{
if (handlers == null)
return;
// Take a snapshot of the handlers before we call out to them since the handlers
// could cause the array to me modified while we are reading it.
var callees = new EventHandler[handlers.Count];
var count = 0;
for (var i = handlers.Count - 1; i >= 0; i--)
{
var reference = handlers[i];
if (!(reference.Target is EventHandler handler))
{
// Clean up old handlers that have been collected
handlers.RemoveAt(i);
}
else
{
callees[count] = handler;
count++;
}
}
// Call the handlers that we snapshot
for (var i = 0; i < count; i++)
{
var handler = callees[i];
handler(null, EventArgs.Empty);
}
}
internal static void AddHandlersToRequerySuggested(IEnumerable<WeakReference> handlers)
{
if (handlers == null)
return;
foreach (var handlerRef in handlers)
{
if (handlerRef.Target is EventHandler handler)
CommandManager.RequerySuggested += handler;
}
}
internal static void RemoveHandlersFromRequerySuggested(IEnumerable<WeakReference> handlers)
{
if (handlers == null)
return;
foreach (var handlerRef in handlers)
{
if (handlerRef.Target is EventHandler handler)
CommandManager.RequerySuggested -= handler;
}
}
internal static void AddWeakReferenceHandler(ref List<WeakReference> handlers, EventHandler handler, int defaultListSize)
{
if (handlers == null)
{
if (defaultListSize > 0)
handlers = new List<WeakReference>(defaultListSize);
else
handlers = new List<WeakReference>();
}
handlers.Add(new WeakReference(handler));
}
internal static void RemoveWeakReferenceHandler(List<WeakReference> handlers, EventHandler handler)
{
if (handlers == null)
return;
for (var i = handlers.Count - 1; i >= 0; i--)
{
var reference = handlers[i];
if (!(reference.Target is EventHandler existingHandler) || existingHandler == handler)
{
// Clean up old handlers that have been collected
// in addition to the handler that is to be removed.
handlers.RemoveAt(i);
}
}
}
}
}

View File

@@ -0,0 +1,280 @@
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Diagnostics;
using System.Linq.Expressions;
using System.Windows.Input;
namespace Bootstrapper.ViewModels.Util
{
/// <summary>
/// An implementation of <see cref="IDelegateCommand" /> which allows delegating the commanding
/// logic to methods passed as parameters, and enables a View to bind commands to objects that
/// are not part of the element tree.
/// </summary>
public sealed class DelegateCommand : BaseCommand, IDelegateCommand
{
private readonly Action _executeMethod;
private readonly Func<bool> _canExecuteMethod;
/// <summary>
/// Initializes a DelegateCommand with methods for execution, verification, and allows specifying
/// if the CommandManager's automatic re-query is disabled for this command.
/// </summary>
/// <param name="executeMethod">
/// Method which is called when the command is executed.
/// </param>
/// <param name="canExecuteMethod">
/// Method which is called to determine if the Execute method may be run.
/// </param>
/// <param name="isAutomaticRequeryDisabled">
/// If true then the framework will not automatically query <see cref="ICommand.CanExecute" />.
/// Queries can be triggered manually by calling <see cref="BaseCommand.RaiseCanExecuteChanged" />.
/// </param>
public DelegateCommand(Action executeMethod, Func<bool> canExecuteMethod = null, bool isAutomaticRequeryDisabled = false)
: base(isAutomaticRequeryDisabled)
{
_executeMethod = executeMethod ?? throw new ArgumentNullException(nameof(executeMethod));
_canExecuteMethod = canExecuteMethod;
}
/// <summary>
/// Method to determine if the command can be executed.
/// </summary>
[DebuggerStepThrough]
public bool CanExecute()
{
return _canExecuteMethod == null || _canExecuteMethod();
}
/// <summary>
/// Executes the command.
/// </summary>
public void Execute()
{
_executeMethod();
}
[DebuggerStepThrough]
bool ICommand.CanExecute(object obj)
{
return CanExecute();
}
void ICommand.Execute(object obj)
{
Execute();
}
public DelegateCommand ListenOn<TObservedType, TPropertyType>(TObservedType viewModel, Expression<Func<TObservedType, TPropertyType>> propertyExpression) where TObservedType : INotifyPropertyChanged
{
AddListenOn(viewModel, nameof(propertyExpression));
return this;
}
public DelegateCommand ListenOn<TObservedType>(TObservedType viewModel, string propertyName) where TObservedType : INotifyPropertyChanged
{
AddListenOn(viewModel, propertyName);
return this;
}
}
/// <summary>
/// A strongly typed implementation of <see cref="IDelegateCommand{T}" /> which allows delegating the commanding
/// logic to methods passed as parameters, and enables a View to bind commands to objects that
/// are not part of the element tree.
/// </summary>
/// <typeparam name="T">Type of the parameter passed to the delegates</typeparam>
public sealed class DelegateCommand<T> : BaseCommand, IDelegateCommand<T>
{
private readonly Action<T> _executeMethod;
private readonly Func<T, bool> _canExecuteMethod;
/// <summary>
/// Initializes a DelegateCommand with methods for execution, verification, and allows specifying
/// if the CommandManager's automatic re-query is disabled for this command.
/// </summary>
/// <param name="executeMethod">
/// Method which is called when the command is executed.
/// </param>
/// <param name="canExecuteMethod">
/// Method which is called to determine if the Execute method may be run.
/// </param>
/// <param name="isAutomaticRequeryDisabled">
/// If true then the framework will not automatically query <see cref="ICommand.CanExecute" />.
/// Queries can be triggered manually by calling <see cref="BaseCommand.RaiseCanExecuteChanged" />.
/// </param>
/// <typeparamref name="T">
/// The type of the data passed to the <see cref="Execute" /> and <see cref="CanExecute" /> methods.
/// </typeparamref>
public DelegateCommand(Action<T> executeMethod, Func<T, bool> canExecuteMethod = null, bool isAutomaticRequeryDisabled = false)
: base(isAutomaticRequeryDisabled)
{
_executeMethod = executeMethod ?? throw new ArgumentNullException(nameof(executeMethod));
_canExecuteMethod = canExecuteMethod;
}
/// <summary>
/// Method to determine if the command can be executed.
/// </summary>
/// <typeparamref name="T">
/// Type of the data passed.
/// </typeparamref>
[DebuggerStepThrough]
public bool CanExecute(T parameter)
{
return _canExecuteMethod == null || _canExecuteMethod(parameter);
}
/// <summary>
/// Execution of the command.
/// </summary>
/// <typeparamref name="T">
/// Type of the data passed.
/// </typeparamref>
public void Execute(T parameter)
{
_executeMethod(parameter);
}
/// <summary>
/// Defines the method that determines whether the command can execute in its current state.
/// </summary>
/// <returns>
/// true if this command can be executed; otherwise, false.
/// </returns>
/// <param name="parameter">
/// Data used by the command. If the command does not require data to be passed, this object can
/// be set to null.
/// </param>
[DebuggerStepThrough]
bool ICommand.CanExecute(object parameter)
{
// if T is of value type and the parameter is not
// set yet, then return false if CanExecute delegate
// exists, else return true
if (parameter == null && typeof(T).IsValueType)
return _canExecuteMethod == null;
return CanExecute((T)parameter);
}
/// <summary>
/// Defines the method to be called when the command is invoked.
/// </summary>
/// <param name="parameter">
/// Data used by the command. If the command does not require data to be passed, this object can
/// be set to null.
/// </param>
void ICommand.Execute(object parameter)
{
Execute((T)parameter);
}
public DelegateCommand<T> ListenOn<TObservedType, TPropertyType>(TObservedType viewModel, Expression<Func<TObservedType, TPropertyType>> propertyExpression) where TObservedType : INotifyPropertyChanged
{
AddListenOn(viewModel, nameof(propertyExpression));
return this;
}
public DelegateCommand<T> ListenOn<TObservedType>(TObservedType viewModel, string propertyName) where TObservedType : INotifyPropertyChanged
{
AddListenOn(viewModel, propertyName);
return this;
}
}
public abstract class BaseCommand
{
private bool _isAutomaticRequeryDisabled;
private List<WeakReference> _canExecuteChangedHandlers;
/// <summary>
/// Initializes a DelegateCommand with methods for execution, verification, and allows specifying
/// if the CommandManager's automatic re-query is disabled for this command.
/// </summary>
/// <param name="isAutomaticRequeryDisabled">
/// If true then the framework will not automatically query <see cref="ICommand.CanExecute" />.
/// Queries can be triggered manually by calling <see cref="RaiseCanExecuteChanged" />.
/// </param>
protected BaseCommand(bool isAutomaticRequeryDisabled = false)
{
_isAutomaticRequeryDisabled = isAutomaticRequeryDisabled;
}
/// <summary>
/// Occurs when changes occur that affect whether the command should execute.
/// </summary>
public event EventHandler CanExecuteChanged
{
add
{
if (!_isAutomaticRequeryDisabled)
CommandManager.RequerySuggested += value;
CommandManagerHelper.AddWeakReferenceHandler(ref _canExecuteChangedHandlers, value, 2);
}
remove
{
if (!_isAutomaticRequeryDisabled)
CommandManager.RequerySuggested -= value;
CommandManagerHelper.RemoveWeakReferenceHandler(_canExecuteChangedHandlers, value);
}
}
/// <summary>
/// If true then the framework will not automatically query <see cref="ICommand.CanExecute" />.
/// Queries can be triggered manually by calling <see cref="RaiseCanExecuteChanged" />.
/// </summary>
public bool IsAutomaticRequeryDisabled
{
get => _isAutomaticRequeryDisabled;
set
{
if (_isAutomaticRequeryDisabled == value)
return;
if (value)
CommandManagerHelper.RemoveHandlersFromRequerySuggested(_canExecuteChangedHandlers);
else
CommandManagerHelper.AddHandlersToRequerySuggested(_canExecuteChangedHandlers);
_isAutomaticRequeryDisabled = value;
}
}
/// <summary>
/// Raises the <see cref="ICommand.CanExecuteChanged" /> event.
/// </summary>
public void RaiseCanExecuteChanged()
{
CommandManagerHelper.CallWeakReferenceHandlers(_canExecuteChangedHandlers);
}
public void ListenForNotificationFrom<TObservedType>(TObservedType viewModel) where TObservedType : INotifyPropertyChanged
{
viewModel.PropertyChanged += OnObservedPropertyChanged;
}
protected void AddListenOn<TObservedType, TPropertyType>(TObservedType viewModel, Expression<Func<TObservedType, TPropertyType>> propertyExpression) where TObservedType : INotifyPropertyChanged
{
AddListenOn(viewModel, nameof(propertyExpression));
}
protected void AddListenOn<TObservedType>(TObservedType viewModel, string propertyName) where TObservedType : INotifyPropertyChanged
{
viewModel.PropertyChanged += (sender, e) =>
{
if (e.PropertyName == propertyName)
RaiseCanExecuteChanged();
};
}
private void OnObservedPropertyChanged(object sender, PropertyChangedEventArgs e)
{
RaiseCanExecuteChanged();
}
}
}

View File

@@ -0,0 +1,73 @@
using System.Windows.Input;
namespace Bootstrapper.ViewModels.Util
{
/// <summary>
/// An <see cref="ICommand" /> which does not require data passed to the Execute and CanExecute methods.
/// </summary>
public interface IDelegateCommand : ICommand
{
/// <summary>
/// If true then the framework will not automatically query <see cref="ICommand.CanExecute" />.
/// Queries can be triggered manually by calling <see cref="RaiseCanExecuteChanged" />.
/// </summary>
bool IsAutomaticRequeryDisabled { get; set; }
/// <summary>
/// Method to determine if the command can be executed.
/// </summary>
bool CanExecute();
/// <summary>
/// Execution of the command.
/// </summary>
void Execute();
/// <summary>
/// Raises the <see cref="ICommand.CanExecuteChanged" /> event.
/// </summary>
void RaiseCanExecuteChanged();
}
/// <summary>
/// A strongly typed <see cref="ICommand" />.
/// </summary>
/// <typeparamref name="T">
/// Type of data passed to the <see cref="ICommand.Execute" /> and <see cref="ICommand.CanExecute" /> methods.
/// </typeparamref>
public interface IDelegateCommand<T> : ICommand
{
/// <summary>
/// If true then the framework will not automatically query <see cref="ICommand.CanExecute" />.
/// Queries can be triggered manually by calling <see cref="RaiseCanExecuteChanged" />.
/// </summary>
bool IsAutomaticRequeryDisabled { get; set; }
/// <summary>
/// Method to determine if the command can be executed.
/// </summary>
/// <param name="parameter">
/// Data to help determine if the command can execute.
/// </param>
/// <typeparamref name="T">
/// Type of the data passed.
/// </typeparamref>
bool CanExecute(T parameter);
/// <summary>
/// Execution of the command.
/// </summary>
/// <param name="parameter">
/// Data required to execute the command.
/// </param>
/// <typeparamref name="T">
/// Type of the data passed.
/// </typeparamref>
void Execute(T parameter);
/// <summary>
/// Raises the <see cref="ICommand.CanExecuteChanged" /> event.
/// </summary>
void RaiseCanExecuteChanged();
}
}

View File

@@ -0,0 +1,28 @@
using System.ComponentModel;
using System.Runtime.CompilerServices;
namespace Bootstrapper.ViewModels.Util
{
/// <summary>
/// A <see cref="INotifyPropertyChanged" /> implementation that provides strongly typed OnPropertyChanged
/// implementations.
/// </summary>
public abstract class PropertyChanger : INotifyPropertyChanged
{
/// <summary>
/// Occurs when a property value changes.
/// </summary>
public event PropertyChangedEventHandler PropertyChanged;
/// <summary>
/// Triggers the <see cref="PropertyChanged" /> event when passed the name of a property.
/// </summary>
/// <param name="propertyName">
/// Name of the property whose value changed.
/// </param>
protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}
}

View File

@@ -0,0 +1,50 @@
using System;
using System.ComponentModel;
using System.Text;
namespace Bootstrapper.ViewModels.Util
{
internal abstract class ViewModelBase : PropertyChanger, IDataErrorInfo
{
public string this[string propertyName] => GetErrors(propertyName);
public string Error => GetErrors();
protected override void OnPropertyChanged(string propertyName = null)
{
base.OnPropertyChanged(propertyName);
base.OnPropertyChanged(nameof(Error));
}
/// <summary>
/// Override in derived classes to provide data validation.
/// </summary>
/// <param name="propertyName">Property name. If not supplied, will evaluate all properties and return all VM errors.</param>
/// <returns>
/// Returns an array of all errors found. If there are no errors, returns either an empty array or
/// <see langword="null" />.
/// </returns>
protected virtual string[] Validate(string propertyName = null)
{
return null;
}
private string GetErrors(string propertyName = null)
{
var errors = Validate(propertyName);
if (errors == null || errors.Length == 0)
return string.Empty;
var sb = new StringBuilder();
foreach (var error in errors)
{
if (sb.Length > 0)
sb.Append(Environment.NewLine);
sb.Append(error);
}
return sb.ToString();
}
}
}

View File

@@ -0,0 +1,19 @@
<UserControl
x:Class="Bootstrapper.Views.ConfigView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:vm="clr-namespace:Bootstrapper.ViewModels"
mc:Ignorable="d"
d:DesignWidth="450"
d:DataContext="{d:DesignInstance {x:Type vm:ConfigViewModel}}"
Background="Transparent"
>
<StackPanel>
<Label>Sample configuration option</Label>
<TextBox Text="{Binding SampleOption, UpdateSourceTrigger=PropertyChanged}" Width="300" />
</StackPanel>
</UserControl>

View File

@@ -0,0 +1,10 @@
namespace Bootstrapper.Views
{
public partial class ConfigView
{
public ConfigView()
{
InitializeComponent();
}
}
}

View File

@@ -0,0 +1,20 @@
<UserControl
x:Class="Bootstrapper.Views.ProgressView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:vm="clr-namespace:Bootstrapper.ViewModels"
mc:Ignorable="d"
d:DesignWidth="300"
d:DataContext="{d:DesignInstance vm:ProgressViewModel}"
>
<DockPanel Margin="0,10,0,0">
<ProgressBar Orientation="Horizontal" Value="{Binding Progress}" Height="15" DockPanel.Dock="Top"/>
<DockPanel DockPanel.Dock="Bottom">
<TextBlock Text="{Binding Package}" DockPanel.Dock="Left"/>
<TextBlock Text="{Binding Message}" HorizontalAlignment="Right" DockPanel.Dock="Right"/>
</DockPanel>
</DockPanel>
</UserControl>

View File

@@ -0,0 +1,25 @@
using System.Windows;
namespace Bootstrapper.Views
{
public partial class ProgressView
{
public ProgressView()
{
InitializeComponent();
}
public bool IsProgressVisible
{
get => (bool)GetValue(IsProgressVisibleProperty);
set => SetValue(IsProgressVisibleProperty, value);
}
public static readonly DependencyProperty IsProgressVisibleProperty = DependencyProperty
.Register(
nameof(IsProgressVisible),
typeof(bool),
typeof(ProgressView),
new PropertyMetadata(default(bool)));
}
}

View File

@@ -0,0 +1,168 @@
<Window
x:Class="Bootstrapper.Views.ShellView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:view="clr-namespace:Bootstrapper.Views"
xmlns:util="clr-namespace:Bootstrapper.ViewModels.Util"
xmlns:vm="clr-namespace:Bootstrapper.ViewModels"
Title="HelloWorld Install"
WindowStyle="None"
ShowInTaskbar="True"
AllowsTransparency="True"
Background="Transparent"
WindowStartupLocation="CenterScreen"
Topmost="False"
SizeToContent="Height"
Width="600"
Icon="pack://application:,,,/Bootstrapper;component/Assets/Icon.ico"
mc:Ignorable="d"
d:DataContext="{d:DesignInstance vm:ShellViewModel}"
>
<Border MouseLeftButtonDown="OnMouseLeftButtonDown" Background="White" BorderBrush="Black" BorderThickness="1">
<DockPanel Margin="20,40,20,10">
<TextBlock Text="Hello World" FontSize="14" HorizontalAlignment="Center" DockPanel.Dock="Top"/>
<TextBlock Text="{Binding Message, Mode=OneWay}" FontSize="10" HorizontalAlignment="Center" DockPanel.Dock="Bottom" Margin="0,10,0,0"/>
<StackPanel Orientation="Horizontal" HorizontalAlignment="Center" DockPanel.Dock="Bottom" Height="30" Margin="0,0,0,10" Panel.ZIndex="0">
<StackPanel Orientation="Horizontal">
<Button Command="{Binding CancelCommand}" IsCancel="True" Width="95" Margin="0,0,10,0">Cancel</Button>
<Grid Visibility="{Binding IsPassive, Converter={util:BooleanVisibilityConverter Negate=True}}">
<Button
x:Name="RepairButton"
Command="{Binding RepairCommand}"
Visibility="{Binding IsRepairAvailable, Converter={util:BooleanVisibilityConverter}}"
IsDefault="False"
Width="95"
Margin="10,0,10,0"
>
<StackPanel Orientation="Horizontal">
<Image Height="16" x:Name="RepairShieldIcon" Margin="0,0,5,0"/>
<TextBlock Foreground="{Binding Foreground, ElementName=RepairButton}" VerticalAlignment="Center">Repair</TextBlock>
</StackPanel>
</Button>
</Grid>
<Button
x:Name="ApplyButton"
Command="{Binding ExecuteCommand}"
Visibility="{Binding IsPassive, Converter={util:BooleanVisibilityConverter Negate=True}}"
IsDefault="True"
Width="95"
Margin="10,0,10,0"
>
<StackPanel Orientation="Horizontal">
<Image Height="16" x:Name="ApplyShieldIcon" Margin="0,0,5,0"/>
<TextBlock Text="{Binding ExecuteDescription}" Foreground="{Binding Foreground, ElementName=ApplyButton}" VerticalAlignment="Center"/>
</StackPanel>
</Button>
</StackPanel>
<Button
Command="{Binding ExitCommand}"
Visibility="{Binding IsPassive, Converter={util:BooleanVisibilityConverter Negate=True}}"
Width="95"
Margin="10,0,0,0"
>
Exit
</Button>
</StackPanel>
<TextBlock
Visibility="{Binding IsPassive, Converter={util:BooleanVisibilityConverter Negate=True}}"
DockPanel.Dock="Bottom"
HorizontalAlignment="Right"
Panel.ZIndex="2"
Margin="0,0,0,5"
>
<Hyperlink Command="{Binding ShowLogCommand}">Log</Hyperlink>
</TextBlock>
<view:ProgressView DataContext="{Binding ProgressVm}" DockPanel.Dock="Bottom" Margin="0,0,0,10"/>
<Grid
IsEnabled="{Binding IsWaiting}"
Visibility="{Binding IsPassive, Converter={util:BooleanVisibilityConverter Negate=True}}"
Panel.ZIndex="1"
Margin="0,20,0,10"
DockPanel.Dock="Top"
>
<view:ConfigView DataContext="{Binding ConfigVm}" HorizontalAlignment="Center"/>
</Grid>
</DockPanel>
</Border>
</Window>

View File

@@ -0,0 +1,46 @@
using System.Drawing;
using System.Windows;
using System.Windows.Input;
using System.Windows.Interop;
using System.Windows.Media.Imaging;
namespace Bootstrapper.Views
{
public partial class ShellView
{
public ShellView()
{
InitializeComponent();
try
{
BitmapSource applyIconSource = null;
BitmapSource repairIconSource = null;
try
{
applyIconSource = Imaging.CreateBitmapSourceFromHIcon(SystemIcons.Shield.Handle, Int32Rect.Empty, BitmapSizeOptions.FromEmptyOptions());
repairIconSource = Imaging.CreateBitmapSourceFromHIcon(SystemIcons.Shield.Handle, Int32Rect.Empty, BitmapSizeOptions.FromEmptyOptions());
}
catch
{
// ignored
}
if (applyIconSource != null)
ApplyShieldIcon.Source = applyIconSource;
if (repairIconSource != null)
RepairShieldIcon.Source = repairIconSource;
}
catch
{
// ignore
}
}
private void OnMouseLeftButtonDown(object sender, MouseButtonEventArgs e)
{
DragMove();
}
}
}

View File

@@ -0,0 +1,223 @@
using Bootstrapper.Models;
using Bootstrapper.Phases;
using System;
using WixToolset.BootstrapperApplicationApi;
namespace Bootstrapper;
internal class WpfBaFactory
{
public Model Create(IDefaultBootstrapperApplication ba, IEngine engine, IBootstrapperCommand commandInfo)
{
try
{
var uiFacade = new WpfFacade(new Log(engine), commandInfo.Display);
var model = new Model(engine, commandInfo, uiFacade);
SubscribeCancelEvents(ba, model);
SubscribeProgressEvents(ba, model);
SubscribeDetectEvents(ba, model);
SubscribePlanEvents(ba, model);
SubscribeApplyEvents(ba, model);
model.Log.RemoveEmbeddedLog();
return model;
}
catch (Exception ex)
{
engine.Log(LogLevel.Error, ex.ToString());
throw;
}
}
private void SubscribeDetectEvents(IDefaultBootstrapperApplication ba, Model model)
{
// Adds a lot of logging, but reviewing the output can be educational
var debug = new LoggingDetectPhase(model);
var release = new DetectPhase(model);
#if DEBUG
var detectPhase = debug;
#else
var detectPhase = release;
#endif
detectPhase.DetectPhaseComplete += model.UiFacade.OnDetectPhaseComplete;
ba.Startup += detectPhase.OnStartup;
ba.Shutdown += detectPhase.OnShutdown;
ba.DetectBegin += detectPhase.OnDetectBegin;
ba.DetectComplete += detectPhase.OnDetectComplete;
ba.DetectRelatedBundle += detectPhase.OnDetectRelatedBundle;
ba.DetectRelatedBundlePackage += detectPhase.OnDetectRelatedBundlePackage;
ba.DetectRelatedMsiPackage += detectPhase.OnDetectRelatedMsiPackage;
ba.DetectUpdate += detectPhase.OnDetectUpdate;
ba.DetectUpdateBegin += detectPhase.OnDetectUpdateBegin;
ba.DetectUpdateComplete += detectPhase.OnDetectUpdateComplete;
ba.DetectForwardCompatibleBundle += detectPhase.OnDetectForwardCompatibleBundle;
ba.DetectPackageBegin += detectPhase.OnDetectPackageBegin;
ba.DetectPackageComplete += detectPhase.OnDetectPackageComplete;
ba.DetectPatchTarget += detectPhase.OnDetectPatchTarget;
ba.DetectCompatibleMsiPackage += detectPhase.OnDetectCompatibleMsiPackage;
ba.DetectMsiFeature += detectPhase.OnDetectMsiFeature;
}
private void SubscribePlanEvents(IDefaultBootstrapperApplication ba, Model model)
{
// Adds a lot of logging, but reviewing the output can be educational
var debug = new LoggingPlanPhase(model);
var release = new PlanPhase(model);
#if DEBUG
var planPhase = debug;
#else
var planPhase = release;
#endif
planPhase.PlanPhaseFailed += model.UiFacade.OnApplyPhaseComplete;
ba.PlanBegin += planPhase.OnPlanBegin;
ba.PlanComplete += planPhase.OnPlanComplete;
ba.PlanPackageBegin += planPhase.OnPlanPackageBegin;
ba.PlanPackageComplete += planPhase.OnPlanPackageComplete;
ba.PlanRollbackBoundary += planPhase.OnPlanRollbackBoundary;
ba.PlanRelatedBundle += planPhase.OnPlanRelatedBundle;
ba.PlanRelatedBundleType += planPhase.OnPlanRelatedBundleType;
ba.PlanRestoreRelatedBundle += planPhase.OnPlanRestoreRelatedBundle;
ba.PlanForwardCompatibleBundle += planPhase.OnPlanForwardCompatibleBundle;
ba.PlanCompatibleMsiPackageBegin += planPhase.OnPlanCompatibleMsiPackageBegin;
ba.PlanCompatibleMsiPackageComplete += planPhase.OnPlanCompatibleMsiPackageComplete;
ba.PlanMsiPackage += planPhase.OnPlanMsiPackage;
ba.PlanPatchTarget += planPhase.OnPlanPatchTarget;
ba.PlanMsiFeature += planPhase.OnPlanMsiFeature;
ba.PlannedPackage += planPhase.OnPlannedPackage;
ba.PlannedCompatiblePackage += planPhase.OnPlannedCompatiblePackage;
}
private void SubscribeApplyEvents(IDefaultBootstrapperApplication ba, Model model)
{
// Adds a lot of logging, but reviewing the output can be educational
var debug = new LoggingApplyPhase(model);
var release = new ApplyPhase(model);
#if DEBUG
var applyPhase = debug;
#else
var applyPhase = release;
#endif
applyPhase.ApplyPhaseComplete += model.UiFacade.OnApplyPhaseComplete;
ba.ApplyBegin += applyPhase.OnApplyBegin;
ba.ApplyComplete += applyPhase.OnApplyComplete;
ba.ApplyDowngrade += applyPhase.OnApplyDowngrade;
ba.ExecuteBegin += applyPhase.OnExecuteBegin;
ba.ExecuteComplete += applyPhase.OnExecuteComplete;
//ba.SetUpdateBegin += applyPhase.OnSetUpdateBegin;
//ba.SetUpdateComplete += applyPhase.OnSetUpdateComplete;
ba.ElevateBegin += applyPhase.OnElevateBegin;
ba.ElevateComplete += applyPhase.OnElevateComplete;
ba.ExecutePatchTarget += applyPhase.OnExecutePatchTarget;
ba.BeginMsiTransactionBegin += applyPhase.OnBeginMsiTransactionBegin;
ba.BeginMsiTransactionComplete += applyPhase.OnBeginMsiTransactionComplete;
ba.CommitMsiTransactionBegin += applyPhase.OnCommitMsiTransactionBegin;
ba.CommitMsiTransactionComplete += applyPhase.OnCommitMsiTransactionComplete;
ba.RollbackMsiTransactionBegin += applyPhase.OnRollbackMsiTransactionBegin;
ba.RollbackMsiTransactionComplete += applyPhase.OnRollbackMsiTransactionComplete;
ba.CacheBegin += applyPhase.OnCacheBegin;
ba.CacheComplete += applyPhase.OnCacheComplete;
ba.CacheAcquireBegin += applyPhase.OnCacheAcquireBegin;
ba.CacheAcquireComplete += applyPhase.OnCacheAcquireComplete;
ba.CacheAcquireResolving += applyPhase.OnCacheAcquireResolving;
ba.CacheAcquireProgress += applyPhase.OnCacheAcquireProgress;
ba.CacheContainerOrPayloadVerifyBegin += applyPhase.OnCacheContainerOrPayloadVerifyBegin;
ba.CacheContainerOrPayloadVerifyComplete += applyPhase.OnCacheContainerOrPayloadVerifyComplete;
ba.CacheContainerOrPayloadVerifyProgress += applyPhase.OnCacheContainerOrPayloadVerifyProgress;
ba.CachePackageBegin += applyPhase.OnCachePackageBegin;
ba.CachePackageComplete += applyPhase.OnCachePackageComplete;
ba.CachePackageNonVitalValidationFailure += applyPhase.OnCachePackageNonVitalValidationFailure;
ba.CachePayloadExtractBegin += applyPhase.OnCachePayloadExtractBegin;
ba.CachePayloadExtractComplete += applyPhase.OnCachePayloadExtractComplete;
ba.CachePayloadExtractProgress += applyPhase.OnCachePayloadExtractProgress;
ba.CacheVerifyBegin += applyPhase.OnCacheVerifyBegin;
ba.CacheVerifyComplete += applyPhase.OnCacheVerifyComplete;
ba.CacheVerifyProgress += applyPhase.OnCacheVerifyProgress;
ba.ExecutePackageBegin += applyPhase.OnExecutePackageBegin;
ba.ExecutePackageComplete += applyPhase.OnExecutePackageComplete;
ba.ExecuteProgress += applyPhase.OnExecuteProgress;
ba.PauseAutomaticUpdatesBegin += applyPhase.OnPauseAutomaticUpdatesBegin;
ba.PauseAutomaticUpdatesComplete += applyPhase.OnPauseAutomaticUpdatesComplete;
ba.SystemRestorePointBegin += applyPhase.OnSystemRestorePointBegin;
ba.SystemRestorePointComplete += applyPhase.OnSystemRestorePointComplete;
ba.LaunchApprovedExeBegin += applyPhase.OnLaunchApprovedExeBegin;
ba.LaunchApprovedExeComplete += applyPhase.OnLaunchApprovedExeComplete;
ba.RegisterBegin += applyPhase.OnRegisterBegin;
ba.RegisterComplete += applyPhase.OnRegisterComplete;
ba.UnregisterBegin += applyPhase.OnUnregisterBegin;
ba.UnregisterComplete += applyPhase.OnUnregisterComplete;
ba.Progress += applyPhase.OnProgress;
ba.ExecuteMsiMessage += applyPhase.OnExecuteMsiMessage;
ba.ExecuteProcessCancel += applyPhase.OnExecuteProcessCancel;
ba.ExecuteFilesInUse += applyPhase.OnExecuteFilesInUse;
ba.Error += applyPhase.OnError;
}
private void SubscribeCancelEvents(IDefaultBootstrapperApplication ba, Model model)
{
var cancelHandler = new CancelHandler(model);
ba.ElevateBegin += cancelHandler.CheckForCancel;
ba.PlanBegin += cancelHandler.CheckForCancel;
ba.PlanPackageBegin += cancelHandler.CheckForCancel;
ba.PlanPatchTarget += cancelHandler.CheckForCancel;
ba.PlanMsiFeature += cancelHandler.CheckForCancel;
ba.PlanMsiPackage += cancelHandler.CheckForCancel;
ba.PlanCompatibleMsiPackageBegin += cancelHandler.CheckForCancel;
ba.PlanForwardCompatibleBundle += cancelHandler.CheckForCancel;
ba.PlanRollbackBoundary += cancelHandler.CheckForCancel;
ba.PlanRelatedBundle += cancelHandler.CheckForCancel;
ba.PlanRelatedBundleType += cancelHandler.CheckForCancel;
ba.PlanRestoreRelatedBundle += cancelHandler.CheckForCancel;
ba.ApplyBegin += cancelHandler.CheckForCancel;
ba.LaunchApprovedExeBegin += cancelHandler.CheckForCancel;
ba.ExecuteBegin += cancelHandler.CheckForCancel;
ba.ExecutePackageBegin += cancelHandler.CheckForCancel;
ba.ExecutePatchTarget += cancelHandler.CheckForCancel;
ba.ExecuteProgress += cancelHandler.CheckForCancel;
ba.BeginMsiTransactionBegin += cancelHandler.CheckForCancel;
ba.CommitMsiTransactionBegin += cancelHandler.CheckForCancel;
ba.CacheBegin += cancelHandler.CheckForCancel;
ba.CacheAcquireBegin += cancelHandler.CheckForCancel;
ba.CacheAcquireProgress += cancelHandler.CheckForCancel;
ba.CacheAcquireResolving += cancelHandler.CheckForCancel;
ba.CachePackageBegin += cancelHandler.CheckForCancel;
ba.CacheContainerOrPayloadVerifyBegin += cancelHandler.CheckForCancel;
ba.CacheContainerOrPayloadVerifyProgress += cancelHandler.CheckForCancel;
ba.CachePayloadExtractBegin += cancelHandler.CheckForCancel;
ba.CachePayloadExtractProgress += cancelHandler.CheckForCancel;
ba.CacheVerifyBegin += cancelHandler.CheckForCancel;
ba.CacheVerifyProgress += cancelHandler.CheckForCancel;
ba.RegisterBegin += cancelHandler.CheckForCancel;
ba.Progress += cancelHandler.CheckForCancel;
ba.ExecuteMsiMessage += cancelHandler.CheckResult;
ba.ExecuteFilesInUse += cancelHandler.CheckResult;
}
private void SubscribeProgressEvents(IDefaultBootstrapperApplication ba, Model model)
{
var progressHandler = new ProgressHandler(model);
ba.PlanBegin += progressHandler.OnPlanBegin;
ba.ApplyBegin += progressHandler.OnApplyBegin;
ba.CacheAcquireProgress += progressHandler.OnCacheAcquireProgress;
ba.CachePayloadExtractProgress += progressHandler.OnCachePayloadExtractProgress;
ba.CacheVerifyProgress += progressHandler.OnCacheVerifyProgress;
ba.CacheContainerOrPayloadVerifyProgress += progressHandler.OnCacheContainerOrPayloadVerifyProgress;
ba.CacheComplete += progressHandler.OnCacheComplete;
ba.ExecutePackageComplete += progressHandler.OnExecutePackageComplete;
ba.ExecuteProgress += progressHandler.OnApplyExecuteProgress;
}
}

View File

@@ -1,14 +1,13 @@
<?define UpgradeCode="6341382d-c0a9-4238-9188-be9607e3fab2"?>
<Wix xmlns="http://wixtoolset.org/schemas/v4/wxs" xmlns:util="http://wixtoolset.org/schemas/v4/wxs/util" xmlns:bal="http://wixtoolset.org/schemas/v4/wxs/bal">
<?include $(sys.CURRENTDIR)\Common.wxi?>
<Bundle Name="PowerToys (Preview) $(var.PowerToysPlatform)" Version="$(var.Version)" Manufacturer="Microsoft Corporation" IconSourceFile="$(var.BinDir)svgs\icon.ico" UpgradeCode="$(var.UpgradeCode)">
<BootstrapperApplication>
<bal:WixStandardBootstrapperApplication LicenseFile="$(var.RepoDir)\installer\License.rtf" LogoFile="$(var.RepoDir)\installer\PowerToysSetupVNext\Images\logo44.png" SuppressOptionsUI="no" SuppressRepair="yes" Theme="rtfLicense" />
</BootstrapperApplication>
<BootstrapperApplication SourceFile="$(var.Bootstrapper.TargetDir)Bootstrapper.exe" bal:CommandLineVariables="caseInsensitive">
<PayloadGroupRef Id="BA.publish" />
</BootstrapperApplication>
<util:RegistrySearch Variable="HasWebView2PerMachine" Root="HKLM" Key="SOFTWARE\WOW6432Node\Microsoft\EdgeUpdate\Clients\{F3017226-FE2A-4295-8BDF-00C3A9A7E4C5}" Result="exists" />
<util:RegistrySearch Variable="HasWebView2PerUser" Root="HKCU" Key="Software\Microsoft\EdgeUpdate\Clients\{F3017226-FE2A-4295-8BDF-00C3A9A7E4C5}" Result="exists" />
@@ -18,7 +17,7 @@
<Variable Name="InstallFolder" Type="formatted" Value="$(var.PlatformProgramFiles)PowerToys" bal:Overridable="yes" />
<?endif?>
<Variable Name="MsiLogFolder" Type="formatted" Value="[LocalAppDataFolder]\Microsoft\PowerToys\" />
<Variable Name="MsiLogFolder" Type="formatted" Value="[LocalAppDataFolder]\Microsoft\PowerToys\" />
<Log Disable="no" Prefix="powertoys-bootstrapper-msi-$(var.Version)" Extension=".log" />
<!-- Only install/upgrade if the version is greater or equal than the currently installed version of PowerToys, to handle the case in which PowerToys was installed from old MSI (before WiX bootstrapper was used) -->
@@ -42,13 +41,13 @@
<Variable Name="DetectedWindowsBuildNumber" Type="version" Value="0" />
<util:RegistrySearch Id="SearchWindowsBuildNumber" Root="HKLM" Key="SOFTWARE\Microsoft\Windows NT\CurrentVersion" Value="CurrentBuildNumber" Result="value" Variable="DetectedWindowsBuildNumber" />
<bal:Condition Message="This application is only supported on Windows 10 version v2004 (build 19041) or higher." Condition="DetectedWindowsBuildNumber &gt;= 19041 OR WixBundleInstalled" />
<Chain>
<ExePackage DisplayName="Closing PowerToys application" Name="terminate_powertoys.cmd" Cache="remove" Compressed="yes" Id="TerminatePowerToys" SourceFile="terminate_powertoys.cmd" Permanent="yes" PerMachine="$(var.PerMachineYesNo)" Vital="no">
</ExePackage>
<ExePackage DisplayName="Microsoft Edge WebView2" Name="MicrosoftEdgeWebview2Setup.exe" Compressed="yes" Id="WebView2" DetectCondition="HasWebView2PerMachine OR HasWebView2PerUser" SourceFile="WebView2\MicrosoftEdgeWebview2Setup.exe" Permanent="yes" PerMachine="$(var.PerMachineYesNo)" InstallArguments="/silent /install" RepairArguments="/repair /passive" UninstallArguments="/silent /uninstall">
</ExePackage>
<MsiPackage DisplayName="PowerToys MSI" SourceFile="$(var.PowerToysPlatform)\Release\$(var.MSIPath)\$(var.MSIName)" Compressed="yes" >
<MsiPackage DisplayName="PowerToys MSI" SourceFile="$(var.PowerToysPlatform)\Release\$(var.MSIPath)\$(var.MSIName)" Compressed="yes" bal:DisplayInternalUICondition="false">
<MsiProperty Name="BOOTSTRAPPERINSTALLFOLDER" Value="[InstallFolder]" />
</MsiPackage>
</Chain>

View File

@@ -25,6 +25,7 @@
<OutputName>PowerToysSetupVNext-$(Version)-$(Platform)</OutputName>
<OutputType>Bundle</OutputType>
<SuppressAclReset>True</SuppressAclReset>
<HarvestDirectoryAdditionalOptions>-generate payloadgroup</HarvestDirectoryAdditionalOptions>
<OutputName Condition=" '$(PerUser)' != 'true' ">PowerToysSetupVNext-$(Version)-$(Platform)</OutputName>
<OutputName Condition=" '$(PerUser)' == 'true' ">PowerToysUserSetupVNext-$(Version)-$(Platform)</OutputName>
<OutputPath Condition=" '$(PerUser)' != 'true' ">$(Platform)\$(Configuration)\MachineSetup</OutputPath>
@@ -32,6 +33,13 @@
<IntermediateOutputPath Condition=" '$(PerUser)' != 'true' ">$(BaseIntermediateOutputPath)$(Platform)\$(Configuration)\MachineSetup</IntermediateOutputPath>
<IntermediateOutputPath Condition=" '$(PerUser)' == 'true' ">$(BaseIntermediateOutputPath)$(Platform)\$(Configuration)\UserSetup</IntermediateOutputPath>
</PropertyGroup>
<ItemGroup>
<BindInputPaths Include="$(MSBuildThisFileDirectory)\Bootstrapper\bin\publish" />
<HarvestDirectory Include="$(MSBuildThisFileDirectory)Bootstrapper\bin\publish">
<DirectoryRefId>BA.publish</DirectoryRefId>
<Transforms>ba.xslt</Transforms>
</HarvestDirectory>
</ItemGroup>
<ItemGroup>
<Compile Include="PowerToys.wxs" />
</ItemGroup>
@@ -40,6 +48,7 @@
<PackageReference Include="WixToolset.UI.wixext" />
<PackageReference Include="WixToolset.NetFx.wixext" />
<PackageReference Include="WixToolset.Bal.wixext" />
<PackageReference Include="WixToolset.Heat" />
</ItemGroup>
<ItemGroup>
<Folder Include="CustomDialogs" />
@@ -51,4 +60,12 @@
</ItemGroup>
</Target>
<Target Name="Restore" />
<Target Name="PreBuild" BeforeTargets="PreBuildEvent">
<Message Text="== running dotnet publish for Bootstrapper ==" Importance="high" />
<Exec Command="dotnet publish &quot;$(MSBuildThisFileDirectory)\Bootstrapper\Bootstrapper.csproj&quot; -o &quot;$(MSBuildThisFileDirectory)\Bootstrapper\bin\publish&quot; -c $(Configuration) --self-contained true" />
</Target>
<PropertyGroup>
<BootstrapperPublishDir>$(MSBuildThisFileDirectory)Bootstrapper\bin\publish\</BootstrapperPublishDir>
<DefineConstants>$(DefineConstants);Bootstrapper.TargetDir=$(BootstrapperPublishDir)</DefineConstants>
</PropertyGroup>
</Project>

View File

@@ -3,6 +3,9 @@
<PropertyGroup>
<EnableDefaultCompileItems>false</EnableDefaultCompileItems>
</PropertyGroup>
<PropertyGroup>
<EnableProjectHarvesting>true</EnableProjectHarvesting>
</PropertyGroup>
<PropertyGroup Condition="'$(Platform)' == 'x64'">
<DefineConstants>Version=$(Version);MonacoSRCHarvestPath=$(ProjectDir)..\..\x64\$(Configuration)\Assets\Monaco\monacoSRC;CmdPalVersion=$(CmdPalVersion)</DefineConstants> <!-- THIS IS AN INNER LOOP OPTIMIZATION
The build pipeline builds the Settings and Launcher projects for Publication

View File

@@ -119,7 +119,6 @@
<Custom Action="CheckGPO" After="InstallInitialize" Condition="NOT Installed" />
<Custom Action="ApplyModulesRegistryChangeSets" After="InstallFiles" Condition="NOT Installed" />
<Custom Action="InstallCmdPalPackage" After="InstallFiles" Condition="NOT Installed" />
<Custom Action="override Wix4CloseApplications_$(sys.BUILDARCHSHORT)" Before="RemoveFiles" />
<Custom Action="RemovePowerToysSchTasks" After="RemoveFiles" />
<!-- TODO: Use to activate embedded MSIX -->
<!--<Custom Action="InstallEmbeddedMSIXTask" After="InstallFinalize">
@@ -235,7 +234,7 @@
<!-- Close 'PowerToys.exe' before uninstall-->
<Property Id="MSIRESTARTMANAGERCONTROL" Value="DisableShutdown" />
<Property Id="MSIFASTINSTALL" Value="DisableShutdown" />
<util:CloseApplication CloseMessage="yes" Target="PowerToys.exe" ElevatedCloseMessage="yes" RebootPrompt="no" TerminateProcess="0" />
<!-- <util:CloseApplication CloseMessage="yes" Target="PowerToys.exe" ElevatedCloseMessage="yes" RebootPrompt="no" TerminateProcess="0" /> -->
</Package>
<Fragment>

View File

@@ -0,0 +1,47 @@
# PowerShell script to list all files in the SilentFilesInUseBA directory
# Usage: .\ListFiles.ps1
$directory = "c:\PowerToys\installer\PowerToysSetupVNext\SilentFilesInUseBA"
Write-Host "=== Files in SilentFilesInUseBA Directory ===" -ForegroundColor Green
Write-Host "Directory: $directory" -ForegroundColor Yellow
Write-Host ""
if (Test-Path $directory) {
# Get all files (not directories) recursively
$files = Get-ChildItem -Path $directory -File -Recurse | Sort-Object FullName
if ($files.Count -eq 0) {
Write-Host "No files found in the directory." -ForegroundColor Red
} else {
Write-Host "Found $($files.Count) file(s):" -ForegroundColor Cyan
Write-Host ""
foreach ($file in $files) {
$relativePath = $file.FullName.Replace($directory, "").TrimStart('\')
$size = if ($file.Length -lt 1KB) { "$($file.Length) bytes" }
elseif ($file.Length -lt 1MB) { "{0:N1} KB" -f ($file.Length / 1KB) }
else { "{0:N1} MB" -f ($file.Length / 1MB) }
Write-Host " $relativePath" -ForegroundColor White
Write-Host " Size: $size" -ForegroundColor Gray
Write-Host " Modified: $($file.LastWriteTime.ToString('yyyy-MM-dd HH:mm:ss'))" -ForegroundColor Gray
Write-Host ""
}
}
# Also list directories
$directories = Get-ChildItem -Path $directory -Directory -Recurse | Sort-Object FullName
if ($directories.Count -gt 0) {
Write-Host "Directories found:" -ForegroundColor Cyan
foreach ($dir in $directories) {
$relativePath = $dir.FullName.Replace($directory, "").TrimStart('\')
Write-Host " $relativePath\" -ForegroundColor Magenta
}
}
} else {
Write-Host "Directory does not exist: $directory" -ForegroundColor Red
}
Write-Host ""
Write-Host "=== End of File List ===" -ForegroundColor Green

View File

@@ -0,0 +1,19 @@
<?xml version="1.0" encoding="utf-8"?>
<xsl:stylesheet
version="1.0"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
xmlns:msxsl="urn:schemas-microsoft-com:xslt"
exclude-result-prefixes="msxsl"
xmlns:wix="http://wixtoolset.org/schemas/v4/wxs"
>
<xsl:output method="xml" indent="yes"/>
<xsl:template match="@* | node()">
<xsl:copy>
<xsl:apply-templates select="@* | node()"/>
</xsl:copy>
</xsl:template>
<xsl:template match="wix:Payload[@SourceFile='SourceDir\Bootstrapper.exe']" />
</xsl:stylesheet>

View File

@@ -1,7 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="4.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<PropertyGroup>
<Version>0.0.1</Version>
<Version>0.93.0</Version>
<DevEnvironment>Local</DevEnvironment>
<!-- Forcing for every DLL on by default -->

View File

@@ -1,122 +1,12 @@
<#
.SYNOPSIS
Build and package PowerToys (CmdPal and installer) for a specific platform and configuration LOCALLY.
.DESCRIPTION
This script automates the end-to-end build and packaging process for PowerToys, including:
- Restoring and building all necessary solutions (CmdPal, BugReportTool, StylesReportTool, etc.)
- Cleaning up old output
- Signing generated .msix packages
- Building the WiX-based MSI and bootstrapper installers
It is designed to work in local development.
.PARAMETER Platform
Specifies the target platform for the build (e.g., 'arm64', 'x64'). Default is 'arm64'.
.PARAMETER Configuration
Specifies the build configuration (e.g., 'Debug', 'Release'). Default is 'Release'.
.EXAMPLE
.\build-installer.ps1
Runs the installer build pipeline for ARM64 Release (default).
.EXAMPLE
.\build-installer.ps1 -Platform x64 -Configuration Release
Runs the pipeline for x64 Debug.
.NOTES
- Requires MSBuild, WiX Toolset, and Git to be installed and accessible from your environment.
- Make sure to run this script from a Developer PowerShell (e.g., VS2022 Developer PowerShell).
- Generated MSIX files will be signed using cert-sign-package.ps1.
- This script will clean previous outputs under the build directories and installer directory (except *.exe files).
- First time run need admin permission to trust the certificate.
- The built installer will be placed under: installer/PowerToysSetup/[Platform]/[Configuration]/UserSetup
relative to the solution root directory.
- The installer can't be run right after the build, I need to copy it to another file before it can be run.
#>
param (
[string]$Platform = 'arm64',
[string]$Configuration = 'Release'
)
$repoRoot = Resolve-Path "$PSScriptRoot\..\.."
Set-Location $repoRoot
function RunMSBuild {
param (
[string]$Solution,
[string]$ExtraArgs
)
$base = @(
$Solution
"/p:Platform=`"$Platform`""
"/p:Configuration=$Configuration"
'/verbosity:normal'
'/clp:Summary;PerformanceSummary;ErrorsOnly;WarningsOnly'
'/nologo'
)
$cmd = $base + ($ExtraArgs -split ' ')
Write-Host ("[MSBUILD] {0} {1}" -f $Solution, ($cmd -join ' '))
& msbuild.exe @cmd
if ($LASTEXITCODE -ne 0) {
Write-Error ("Build failed: {0} {1}" -f $Solution, $ExtraArgs)
exit $LASTEXITCODE
}
}
function RestoreThenBuild {
param ([string]$Solution)
# 1) restore
RunMSBuild $Solution '/t:restore /p:RestorePackagesConfig=true'
# 2) build -------------------------------------------------
RunMSBuild $Solution '/m'
}
Write-Host ("Make sure wix is installed and available")
& "$PSScriptRoot\ensure-wix.ps1"
Write-Host ("[PIPELINE] Start | Platform={0} Configuration={1}" -f $Platform, $Configuration)
Write-Host ''
$cmdpalOutputPath = Join-Path $repoRoot "$Platform\$Configuration\WinUI3Apps\CmdPal"
if (Test-Path $cmdpalOutputPath) {
Write-Host "[CLEAN] Removing previous output: $cmdpalOutputPath"
Remove-Item $cmdpalOutputPath -Recurse -Force -ErrorAction Ignore
}
RestoreThenBuild '.\PowerToys.sln'
$msixSearchRoot = Join-Path $repoRoot "$Platform\$Configuration"
$msixFiles = Get-ChildItem -Path $msixSearchRoot -Recurse -Filter *.msix |
Select-Object -ExpandProperty FullName
if ($msixFiles.Count) {
Write-Host ("[SIGN] .msix file(s): {0}" -f ($msixFiles -join '; '))
& "$PSScriptRoot\cert-sign-package.ps1" -TargetPaths $msixFiles
}
else {
Write-Warning "[SIGN] No .msix files found in $msixSearchRoot"
}
RestoreThenBuild '.\tools\BugReportTool\BugReportTool.sln'
RestoreThenBuild '.\tools\StylesReportTool\StylesReportTool.sln'
Write-Host 'Make sure powertoys build is complete and available'
Write-Host '[CLEAN] installer (keep *.exe)'
git clean -xfd -e '*.exe' -- .\installer\ | Out-Null
RunMSBuild '.\installer\PowerToysSetup.sln' '/t:restore /p:RestorePackagesConfig=true'
MSBuild -t:restore .\installer\PowerToysSetup.sln -p:RestorePackagesConfig=true
RunMSBuild '.\installer\PowerToysSetup.sln' '/m /t:PowerToysInstaller /p:PerUser=true'
MSBuild -m .\installer\PowerToysSetup.sln /t:PowerToysInstallerVNext /p:Configuration=Release /p:Platform="x64" /p:PerUser=true
RunMSBuild '.\installer\PowerToysSetup.sln' '/m /t:PowerToysBootstrapper /p:PerUser=true'
MSBuild -m .\installer\PowerToysSetup.sln /t:PowerToysBootstrapperVNext /p:Configuration=Release /p:Platform="x64" /p:PerUser=true
Write-Host '[PIPELINE] Completed'

123
tools/build/full-build.ps1 Normal file
View File

@@ -0,0 +1,123 @@
<#
.SYNOPSIS
Build and package PowerToys (CmdPal and installer) for a specific platform and configuration LOCALLY.
.DESCRIPTION
This script automates the end-to-end build and packaging process for PowerToys, including:
- Restoring and building all necessary solutions (CmdPal, BugReportTool, StylesReportTool, etc.)
- Cleaning up old output
- Signing generated .msix packages
- Building the WiX-based MSI and bootstrapper installers
It is designed to work in local development.
.PARAMETER Platform
Specifies the target platform for the build (e.g., 'arm64', 'x64'). Default is 'arm64'.
.PARAMETER Configuration
Specifies the build configuration (e.g., 'Debug', 'Release'). Default is 'Release'.
.EXAMPLE
.\build-installer.ps1
Runs the installer build pipeline for ARM64 Release (default).
.EXAMPLE
.\build-installer.ps1 -Platform x64 -Configuration Release
Runs the pipeline for x64 Debug.
.NOTES
- Requires MSBuild, WiX Toolset, and Git to be installed and accessible from your environment.
- Make sure to run this script from a Developer PowerShell (e.g., VS2022 Developer PowerShell).
- Generated MSIX files will be signed using cert-sign-package.ps1.
- This script will clean previous outputs under the build directories and installer directory (except *.exe files).
- First time run need admin permission to trust the certificate.
- The built installer will be placed under: installer/PowerToysSetup/[Platform]/[Configuration]/UserSetup
relative to the solution root directory.
- The installer can't be run right after the build, I need to copy it to another file before it can be run.
#>
param (
[string]$Platform = 'x64',
[string]$Configuration = 'Release'
)
$repoRoot = Resolve-Path "$PSScriptRoot\..\.."
Set-Location $repoRoot
function RunMSBuild {
param (
[string]$Solution,
[string]$ExtraArgs
)
$base = @(
$Solution
"/p:Platform=`"$Platform`""
"/p:Configuration=$Configuration"
"/p:CIBuild=true"
'/verbosity:normal'
'/clp:Summary;PerformanceSummary;ErrorsOnly;WarningsOnly'
'/nologo'
)
$cmd = $base + ($ExtraArgs -split ' ')
Write-Host ("[MSBUILD] {0} {1}" -f $Solution, ($cmd -join ' '))
& msbuild.exe @cmd
if ($LASTEXITCODE -ne 0) {
Write-Error ("Build failed: {0} {1}" -f $Solution, $ExtraArgs)
exit $LASTEXITCODE
}
}
function RestoreThenBuild {
param ([string]$Solution)
# 1) restore
RunMSBuild $Solution '/t:restore /p:RestorePackagesConfig=true'
# 2) build -------------------------------------------------
RunMSBuild $Solution '/m'
}
Write-Host ("Make sure wix is installed and available")
& "$PSScriptRoot\ensure-wix.ps1"
Write-Host ("[PIPELINE] Start | Platform={0} Configuration={1}" -f $Platform, $Configuration)
Write-Host ''
$cmdpalOutputPath = Join-Path $repoRoot "$Platform\$Configuration\WinUI3Apps\CmdPal"
if (Test-Path $cmdpalOutputPath) {
Write-Host "[CLEAN] Removing previous output: $cmdpalOutputPath"
Remove-Item $cmdpalOutputPath -Recurse -Force -ErrorAction Ignore
}
RestoreThenBuild '.\PowerToys.sln'
$msixSearchRoot = Join-Path $repoRoot "$Platform\$Configuration"
$msixFiles = Get-ChildItem -Path $msixSearchRoot -Recurse -Filter *.msix |
Select-Object -ExpandProperty FullName
if ($msixFiles.Count) {
Write-Host ("[SIGN] .msix file(s): {0}" -f ($msixFiles -join '; '))
& "$PSScriptRoot\cert-sign-package.ps1" -TargetPaths $msixFiles
}
else {
Write-Warning "[SIGN] No .msix files found in $msixSearchRoot"
}
RestoreThenBuild '.\tools\BugReportTool\BugReportTool.sln'
RestoreThenBuild '.\tools\StylesReportTool\StylesReportTool.sln'
Write-Host '[CLEAN] installer (keep *.exe)'
git clean -xfd -e '*.exe' -- .\installer\ | Out-Null
MSBuild -t:restore .\installer\PowerToysSetup.sln -p:RestorePackagesConfig=true
MSBuild -m .\installer\PowerToysSetup.sln /t:PowerToysInstallerVNext /p:Configuration=Release /p:Platform="x64" /p:PerUser=true
MSBuild -m .\installer\PowerToysSetup.sln /t:PowerToysBootstrapperVNext /p:Configuration=Release /p:Platform="x64" /p:PerUser=true
Write-Host '[PIPELINE] Completed'