// Copyright (c) .NET Foundation and contributors. All rights reserved. Licensed under the Microsoft Reciprocal License. See LICENSE.TXT file in the project root for full license information.
namespace WixToolset.WixBA
{
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Reflection;
using System.Windows;
using System.Windows.Input;
using IO = System.IO;
using WixToolset.Mba.Core;
///
/// The states of detection.
///
public enum DetectionState
{
Absent,
Present,
}
///
/// The states of upgrade detection.
///
public enum UpgradeDetectionState
{
// There are no Upgrade related bundles installed.
None,
// All Upgrade related bundles that are installed are older than or the same version as this bundle.
Older,
// At least one Upgrade related bundle is installed that is newer than this bundle.
Newer,
}
///
/// The states of installation.
///
public enum InstallationState
{
Initializing,
Detecting,
Waiting,
Planning,
Applying,
Applied,
Failed,
}
///
/// The model of the installation view in WixBA.
///
public class InstallationViewModel : PropertyNotifyBase
{
private RootViewModel root;
private Dictionary downloadRetries;
private bool downgrade;
private string downgradeMessage;
private ICommand licenseCommand;
private ICommand launchHomePageCommand;
private ICommand launchNewsCommand;
private ICommand launchVSExtensionPageCommand;
private ICommand installCommand;
private ICommand repairCommand;
private ICommand uninstallCommand;
private ICommand openLogCommand;
private ICommand openLogFolderCommand;
private ICommand tryAgainCommand;
private string message;
private DateTime cachePackageStart;
private DateTime executePackageStart;
///
/// Creates a new model of the installation view.
///
public InstallationViewModel(RootViewModel root)
{
this.root = root;
this.downloadRetries = new Dictionary();
this.root.PropertyChanged += new System.ComponentModel.PropertyChangedEventHandler(this.RootPropertyChanged);
WixBA.Model.Bootstrapper.DetectBegin += this.DetectBegin;
WixBA.Model.Bootstrapper.DetectRelatedBundle += this.DetectedRelatedBundle;
WixBA.Model.Bootstrapper.DetectComplete += this.DetectComplete;
WixBA.Model.Bootstrapper.PlanPackageBegin += this.PlanPackageBegin;
WixBA.Model.Bootstrapper.PlanComplete += this.PlanComplete;
WixBA.Model.Bootstrapper.ApplyBegin += this.ApplyBegin;
WixBA.Model.Bootstrapper.CacheAcquireBegin += this.CacheAcquireBegin;
WixBA.Model.Bootstrapper.CacheAcquireResolving += this.CacheAcquireResolving;
WixBA.Model.Bootstrapper.CacheAcquireComplete += this.CacheAcquireComplete;
WixBA.Model.Bootstrapper.ExecutePackageBegin += this.ExecutePackageBegin;
WixBA.Model.Bootstrapper.ExecutePackageComplete += this.ExecutePackageComplete;
WixBA.Model.Bootstrapper.Error += this.ExecuteError;
WixBA.Model.Bootstrapper.ApplyComplete += this.ApplyComplete;
WixBA.Model.Bootstrapper.SetUpdateComplete += this.SetUpdateComplete;
}
void RootPropertyChanged(object sender, PropertyChangedEventArgs e)
{
if (("DetectState" == e.PropertyName) || ("UpgradeDetectState" == e.PropertyName) || ("InstallState" == e.PropertyName))
{
base.OnPropertyChanged("RepairEnabled");
base.OnPropertyChanged("InstallEnabled");
base.OnPropertyChanged("IsComplete");
base.OnPropertyChanged("IsSuccessfulCompletion");
base.OnPropertyChanged("IsFailedCompletion");
base.OnPropertyChanged("StatusText");
base.OnPropertyChanged("UninstallEnabled");
}
}
///
/// Gets the version for the application.
///
public string Version
{
get { return String.Concat("v", WixBA.Model.Version.ToString()); }
}
///
/// The Publisher of this bundle.
///
public string Publisher
{
get
{
string company = "[AssemblyCompany]";
return WixDistribution.ReplacePlaceholders(company, typeof(WixBA).Assembly);
}
}
///
/// The Publisher of this bundle.
///
public string SupportUrl
{
get
{
return WixDistribution.SupportUrl;
}
}
public string VSExtensionUrl
{
get
{
return WixDistribution.VSExtensionsLandingUrl;
}
}
public string Message
{
get
{
return this.message;
}
set
{
if (this.message != value)
{
this.message = value;
base.OnPropertyChanged("Message");
}
}
}
///
/// Gets and sets whether the view model considers this install to be a downgrade.
///
public bool Downgrade
{
get
{
return this.downgrade;
}
set
{
if (this.downgrade != value)
{
this.downgrade = value;
base.OnPropertyChanged("Downgrade");
}
}
}
public string DowngradeMessage
{
get
{
return this.downgradeMessage;
}
set
{
if (this.downgradeMessage != value)
{
this.downgradeMessage = value;
base.OnPropertyChanged("DowngradeMessage");
}
}
}
public ICommand LaunchHomePageCommand
{
get
{
if (this.launchHomePageCommand == null)
{
this.launchHomePageCommand = new RelayCommand(param => WixBA.LaunchUrl(this.SupportUrl), param => true);
}
return this.launchHomePageCommand;
}
}
public ICommand LaunchNewsCommand
{
get
{
if (this.launchNewsCommand == null)
{
this.launchNewsCommand = new RelayCommand(param => WixBA.LaunchUrl(WixDistribution.NewsUrl), param => true);
}
return this.launchNewsCommand;
}
}
public ICommand LaunchVSExtensionPageCommand
{
get
{
if (this.launchVSExtensionPageCommand == null)
{
this.launchVSExtensionPageCommand = new RelayCommand(param => WixBA.LaunchUrl(WixDistribution.VSExtensionsLandingUrl), param => true);
}
return this.launchVSExtensionPageCommand;
}
}
public ICommand LicenseCommand
{
get
{
if (this.licenseCommand == null)
{
this.licenseCommand = new RelayCommand(param => this.LaunchLicense(), param => true);
}
return this.licenseCommand;
}
}
public bool LicenseEnabled
{
get { return this.LicenseCommand.CanExecute(this); }
}
public ICommand CloseCommand
{
get { return this.root.CloseCommand; }
}
public bool IsComplete
{
get { return this.IsSuccessfulCompletion || this.IsFailedCompletion; }
}
public bool IsSuccessfulCompletion
{
get { return InstallationState.Applied == this.root.InstallState; }
}
public bool IsFailedCompletion
{
get { return InstallationState.Failed == this.root.InstallState; }
}
public ICommand InstallCommand
{
get
{
if (this.installCommand == null)
{
this.installCommand = new RelayCommand(
param => WixBA.Plan(LaunchAction.Install),
param => this.root.DetectState == DetectionState.Absent && this.root.UpgradeDetectState != UpgradeDetectionState.Newer && this.root.InstallState == InstallationState.Waiting);
}
return this.installCommand;
}
}
public bool InstallEnabled
{
get { return this.InstallCommand.CanExecute(this); }
}
public ICommand RepairCommand
{
get
{
if (this.repairCommand == null)
{
this.repairCommand = new RelayCommand(param => WixBA.Plan(LaunchAction.Repair), param => this.root.DetectState == DetectionState.Present && this.root.InstallState == InstallationState.Waiting);
}
return this.repairCommand;
}
}
public bool RepairEnabled
{
get { return this.RepairCommand.CanExecute(this); }
}
public ICommand UninstallCommand
{
get
{
if (this.uninstallCommand == null)
{
this.uninstallCommand = new RelayCommand(param => WixBA.Plan(LaunchAction.Uninstall), param => this.root.DetectState == DetectionState.Present && this.root.InstallState == InstallationState.Waiting);
}
return this.uninstallCommand;
}
}
public bool UninstallEnabled
{
get { return this.UninstallCommand.CanExecute(this); }
}
public ICommand OpenLogCommand
{
get
{
if (this.openLogCommand == null)
{
this.openLogCommand = new RelayCommand(param => WixBA.OpenLog(new Uri(WixBA.Model.Engine.GetVariableString("WixBundleLog"))));
}
return this.openLogCommand;
}
}
public ICommand OpenLogFolderCommand
{
get
{
if (this.openLogFolderCommand == null)
{
string logFolder = IO.Path.GetDirectoryName(WixBA.Model.Engine.GetVariableString("WixBundleLog"));
this.openLogFolderCommand = new RelayCommand(param => WixBA.OpenLogFolder(logFolder));
}
return this.openLogFolderCommand;
}
}
public ICommand TryAgainCommand
{
get
{
if (this.tryAgainCommand == null)
{
this.tryAgainCommand = new RelayCommand(param =>
{
this.root.Canceled = false;
WixBA.Plan(WixBA.Model.PlannedAction);
}, param => this.IsFailedCompletion);
}
return this.tryAgainCommand;
}
}
public string StatusText
{
get
{
switch(this.root.InstallState)
{
case InstallationState.Applied:
return "Complete";
case InstallationState.Failed:
return this.root.Canceled ? "Cancelled" : "Failed";
default:
return "Unknown"; // this shouldn't be shown in the UI.
}
}
}
///
/// Launches the license in the default viewer.
///
private void LaunchLicense()
{
string folder = IO.Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location);
WixBA.LaunchUrl(IO.Path.Combine(folder, "License.txt"));
}
private void DetectBegin(object sender, DetectBeginEventArgs e)
{
this.root.DetectState = e.Installed ? DetectionState.Present : DetectionState.Absent;
WixBA.Model.PlannedAction = LaunchAction.Unknown;
}
private void DetectedRelatedBundle(object sender, DetectRelatedBundleEventArgs e)
{
if (e.RelationType == RelationType.Upgrade)
{
if (WixBA.Model.Engine.CompareVersions(this.Version, e.Version) > 0)
{
if (this.root.UpgradeDetectState == UpgradeDetectionState.None)
{
this.root.UpgradeDetectState = UpgradeDetectionState.Older;
}
}
else
{
this.root.UpgradeDetectState = UpgradeDetectionState.Newer;
}
}
if (!WixBA.Model.BAManifest.Bundle.Packages.ContainsKey(e.ProductCode))
{
WixBA.Model.BAManifest.Bundle.AddRelatedBundleAsPackage(e);
}
}
private void SetUpdateComplete(object sender, SetUpdateCompleteEventArgs e)
{
if (!String.IsNullOrEmpty(e.NewPackageId) && !WixBA.Model.BAManifest.Bundle.Packages.ContainsKey(e.NewPackageId))
{
WixBA.Model.BAManifest.Bundle.AddUpdateBundleAsPackage(e);
}
}
private void DetectComplete(object sender, DetectCompleteEventArgs e)
{
// Parse the command line string before any planning.
this.ParseCommandLine();
this.root.InstallState = InstallationState.Waiting;
if (LaunchAction.Uninstall == WixBA.Model.Command.Action &&
ResumeType.Arp != WixBA.Model.Command.Resume) // MSI and WixStdBA require some kind of confirmation before proceeding so WixBA should, too.
{
WixBA.Model.Engine.Log(LogLevel.Verbose, "Invoking automatic plan for uninstall");
WixBA.Plan(LaunchAction.Uninstall);
}
else if (Hresult.Succeeded(e.Status))
{
if (this.root.UpgradeDetectState == UpgradeDetectionState.Newer)
{
this.Downgrade = true;
this.DowngradeMessage = "There is already a newer version of WiX installed on this machine.";
}
if (LaunchAction.Layout == WixBA.Model.Command.Action)
{
WixBA.PlanLayout();
}
else if (WixBA.Model.Command.Display != Display.Full)
{
// If we're not waiting for the user to click install, dispatch plan with the default action.
WixBA.Model.Engine.Log(LogLevel.Verbose, "Invoking automatic plan for non-interactive mode.");
WixBA.Plan(WixBA.Model.Command.Action);
}
}
else
{
this.root.InstallState = InstallationState.Failed;
}
// Force all commands to reevaluate CanExecute.
// InvalidateRequerySuggested must be run on the UI thread.
this.root.Dispatcher.Invoke(new Action(CommandManager.InvalidateRequerySuggested));
}
private void PlanPackageBegin(object sender, PlanPackageBeginEventArgs e)
{
// If we're able to run our BA, we don't want to install .NET since the one on the machine is already good enough.
if (e.PackageId.StartsWith("NetFx4", StringComparison.OrdinalIgnoreCase) || e.PackageId.StartsWith("DesktopNetCoreRuntime", StringComparison.OrdinalIgnoreCase))
{
e.State = RequestState.None;
}
}
private void PlanComplete(object sender, PlanCompleteEventArgs e)
{
if (Hresult.Succeeded(e.Status))
{
this.root.PreApplyState = this.root.InstallState;
this.root.InstallState = InstallationState.Applying;
WixBA.Model.Engine.Apply(this.root.ViewWindowHandle);
}
else
{
this.root.InstallState = InstallationState.Failed;
}
}
private void ApplyBegin(object sender, ApplyBeginEventArgs e)
{
this.downloadRetries.Clear();
}
private void CacheAcquireBegin(object sender, CacheAcquireBeginEventArgs e)
{
this.cachePackageStart = DateTime.Now;
}
private void CacheAcquireResolving(object sender, CacheAcquireResolvingEventArgs e)
{
if (e.Action == CacheResolveOperation.Download && !this.downloadRetries.ContainsKey(e.PackageOrContainerId))
{
this.downloadRetries.Add(e.PackageOrContainerId, 0);
}
}
private void CacheAcquireComplete(object sender, CacheAcquireCompleteEventArgs e)
{
this.AddPackageTelemetry("Cache", e.PackageOrContainerId ?? String.Empty, DateTime.Now.Subtract(this.cachePackageStart).TotalMilliseconds, e.Status);
if (e.Status < 0 && this.downloadRetries.TryGetValue(e.PackageOrContainerId, out var retries) && retries < 3)
{
this.downloadRetries[e.PackageOrContainerId] = retries + 1;
switch (e.Status)
{
case -2147023294: //HRESULT_FROM_WIN32(ERROR_INSTALL_USEREXIT)
case -2147024894: //HRESULT_FROM_WIN32(ERROR_FILE_NOT_FOUND)
case -2147012889: //HRESULT_FROM_WIN32(ERROR_INTERNET_NAME_NOT_RESOLVED)
break;
default:
e.Action = BOOTSTRAPPER_CACHEACQUIRECOMPLETE_ACTION.Retry;
break;
}
}
}
private void ExecutePackageBegin(object sender, ExecutePackageBeginEventArgs e)
{
lock (this)
{
this.executePackageStart = e.ShouldExecute ? DateTime.Now : DateTime.MinValue;
}
}
private void ExecutePackageComplete(object sender, ExecutePackageCompleteEventArgs e)
{
lock (this)
{
if (DateTime.MinValue < this.executePackageStart)
{
this.AddPackageTelemetry("Execute", e.PackageId ?? String.Empty, DateTime.Now.Subtract(this.executePackageStart).TotalMilliseconds, e.Status);
this.executePackageStart = DateTime.MinValue;
}
}
}
private void ExecuteError(object sender, ErrorEventArgs e)
{
lock (this)
{
if (!this.root.Canceled)
{
// If the error is a cancel coming from the engine during apply we want to go back to the preapply state.
if (InstallationState.Applying == this.root.InstallState && (int)Error.UserCancelled == e.ErrorCode)
{
this.root.InstallState = this.root.PreApplyState;
}
else
{
this.Message = e.ErrorMessage;
if (Display.Full == WixBA.Model.Command.Display)
{
// On HTTP authentication errors, have the engine try to do authentication for us.
if (ErrorType.HttpServerAuthentication == e.ErrorType || ErrorType.HttpProxyAuthentication == e.ErrorType)
{
e.Result = Result.TryAgain;
}
else // show an error dialog.
{
MessageBoxButton msgbox = MessageBoxButton.OK;
switch (e.UIHint & 0xF)
{
case 0:
msgbox = MessageBoxButton.OK;
break;
case 1:
msgbox = MessageBoxButton.OKCancel;
break;
// There is no 2! That would have been MB_ABORTRETRYIGNORE.
case 3:
msgbox = MessageBoxButton.YesNoCancel;
break;
case 4:
msgbox = MessageBoxButton.YesNo;
break;
// default: stay with MBOK since an exact match is not available.
}
MessageBoxResult result = MessageBoxResult.None;
WixBA.View.Dispatcher.Invoke((Action)delegate()
{
result = MessageBox.Show(WixBA.View, e.ErrorMessage, "WiX Toolset", msgbox, MessageBoxImage.Error);
}
);
// If there was a match from the UI hint to the msgbox value, use the result from the
// message box. Otherwise, we'll ignore it and return the default to Burn.
if ((e.UIHint & 0xF) == (int)msgbox)
{
e.Result = (Result)result;
}
}
}
}
}
else // canceled, so always return cancel.
{
e.Result = Result.Cancel;
}
}
}
private void ApplyComplete(object sender, ApplyCompleteEventArgs e)
{
WixBA.Model.Result = e.Status; // remember the final result of the apply.
// Set the state to applied or failed unless the state has already been set back to the preapply state
// which means we need to show the UI as it was before the apply started.
if (this.root.InstallState != this.root.PreApplyState)
{
this.root.InstallState = Hresult.Succeeded(e.Status) ? InstallationState.Applied : InstallationState.Failed;
}
// If we're not in Full UI mode, we need to alert the dispatcher to stop and close the window for passive.
if (Display.Full != WixBA.Model.Command.Display)
{
// If its passive, send a message to the window to close.
if (Display.Passive == WixBA.Model.Command.Display)
{
WixBA.Model.Engine.Log(LogLevel.Verbose, "Automatically closing the window for non-interactive install");
WixBA.Dispatcher.BeginInvoke(new Action(WixBA.View.Close));
}
else
{
WixBA.Dispatcher.InvokeShutdown();
}
return;
}
else if (Hresult.Succeeded(e.Status) && LaunchAction.UpdateReplace == WixBA.Model.PlannedAction) // if we successfully applied an update close the window since the new Bundle should be running now.
{
WixBA.Model.Engine.Log(LogLevel.Verbose, "Automatically closing the window since update successful.");
WixBA.Dispatcher.BeginInvoke(new Action(WixBA.View.Close));
return;
}
else if (this.root.AutoClose)
{
// Automatically closing since the user clicked the X button.
WixBA.Dispatcher.BeginInvoke(new Action(WixBA.View.Close));
return;
}
// Force all commands to reevaluate CanExecute.
// InvalidateRequerySuggested must be run on the UI thread.
this.root.Dispatcher.Invoke(new Action(CommandManager.InvalidateRequerySuggested));
}
private void ParseCommandLine()
{
// Get array of arguments based on the system parsing algorithm.
string[] args = BootstrapperCommand.ParseCommandLineToArgs(WixBA.Model.Command.CommandLine);
for (int i = 0; i < args.Length; ++i)
{
if (args[i].StartsWith("InstallFolder=", StringComparison.InvariantCultureIgnoreCase))
{
// Allow relative directory paths. Also validates.
string[] param = args[i].Split(new char[] {'='}, 2);
this.root.InstallDirectory = IO.Path.Combine(Environment.CurrentDirectory, param[1]);
}
}
}
private void AddPackageTelemetry(string prefix, string id, double time, int result)
{
lock (this)
{
string key = String.Format("{0}Time_{1}", prefix, id);
string value = time.ToString();
WixBA.Model.Telemetry.Add(new KeyValuePair(key, value));
key = String.Format("{0}Result_{1}", prefix, id);
value = String.Concat("0x", result.ToString("x"));
WixBA.Model.Telemetry.Add(new KeyValuePair(key, value));
}
}
}
}