oleg-shilo / wixsharp

Framework for building a complete MSI or WiX source code by using script files written with C# syntax.
MIT License
1.12k stars 175 forks source link

How to execute rollback imeediately If i click on cancel after install execute sequence has started ? #1591

Closed Aaswin1996 closed 3 months ago

Aaswin1996 commented 4 months ago

If I click on cancel in the progress Dialog (WPF) the installer installs the application and then runs the rollback script .I want it to run the rollback immediately after I Click on cancel .

My Progress Dialog :

using System;
using System.Security.Principal;
using System.Windows.Forms;
using System.Windows.Media.Imaging;
using Caliburn.Micro;
using Microsoft.Deployment.WindowsInstaller;
using WixSharp;
using WixSharp.CommonTasks;
using WixSharp.UI.Forms;
using WixSharp.UI.WPF;

namespace ScoutWpfInstaller
{
    /// <summary>
    /// The standard ProgressDialog.
    /// <para>Follows the design of the canonical Caliburn.Micro View (MVVM).</para>
    /// <para>See https://caliburnmicro.com/documentation/cheat-sheet</para>
    /// </summary>
    /// <seealso cref="WixSharp.UI.WPF.WpfDialog" />
    /// <seealso cref="WixSharp.IWpfDialog" />
    /// <seealso cref="System.Windows.Markup.IComponentConnector" />
    public partial class ProgressDialog : WpfDialog, IWpfDialog, IProgressDialog
    {
        bool isUninstallDialog = false;
        /// <summary>
        /// Initializes a new instance of the <see cref="ProgressDialog" /> class.
        /// </summary>
        public ProgressDialog()
        {
            InitializeComponent();
        }

        /// <summary>
        /// This method is invoked by WixSharp runtime when the custom dialog content is internally fully initialized.
        /// This is a convenient place to do further initialization activities (e.g. localization).
        /// </summary>
        public void Init()
        {
            UpdateTitles(ManagedFormHost.Runtime.Session);

            model = new ProgressDialogModel { Host = ManagedFormHost };
            ViewModelBinder.Bind(model, this, null);

            ScoutVersion.Text = ManagedFormHost.MsiRuntime.Data["VERSION"]; ;
            model.StartExecute();
        }

        /// <summary>
        /// Updates the titles of the dialog depending on what type of installation action MSI is performing.
        /// </summary>
        /// <param name="session">The session.</param>
        public void UpdateTitles(ISession session)
        {
            if (session.IsUninstalling())
            {
                isUninstallDialog = session.IsUninstalling();
                DialogTitleLabel.Text = "[ProgressDlgTitleRemoving]";
                DialogDescription.Text = "[ProgressDlgTextRemoving]";
            }
            else if (session.IsRepairing())
            {
                DialogTitleLabel.Text = "[ProgressDlgTextRepairing]";
                DialogDescription.Text = "[ProgressDlgTitleRepairing]";
            }
            else if (session.IsInstalling())
            {
                DialogTitleLabel.Text = "[ProgressDlgTitleInstalling]";
                DialogDescription.Text = "[ProgressDlgTextInstalling]";
            }

            // `Localize` resolves [...] titles and descriptions into the localized strings stored in MSI resources tables
            this.Localize();
        }

        ProgressDialogModel model;

        /// <summary>
        /// Processes information and progress messages sent to the user interface.
        /// <para> This method directly mapped to the
        /// <see cref="T:Microsoft.Deployment.WindowsInstaller.IEmbeddedUI.ProcessMessage" />.</para>
        /// </summary>
        /// <param name="messageType">Type of the message.</param>
        /// <param name="messageRecord">The message record.</param>
        /// <param name="buttons">The buttons.</param>
        /// <param name="icon">The icon.</param>
        /// <param name="defaultButton">The default button.</param>
        /// <returns></returns>
        public override MessageResult ProcessMessage(InstallMessage messageType, Record messageRecord, MessageButtons buttons, MessageIcon icon, MessageDefaultButton defaultButton)
            => model?.ProcessMessage(messageType, messageRecord, CurrentStatus.Text) ??
            MessageResult.None;

        /// <summary>
        /// Called when MSI execution is complete.
        /// </summary>
        public override void OnExecuteComplete()
        {

            model?.OnExecuteComplete(isUninstallDialog);
        }

        /// <summary>
        /// Called when MSI execution progress is changed.
        /// </summary>
        /// <param name="progressPercentage">The progress percentage.</param>
        public override void OnProgress(int progressPercentage)
        {
            if (model != null)
                model.ProgressValue = progressPercentage;
        }
    }

    /// <summary>
    /// ViewModel for standard ProgressDialog.
    /// <para>Follows the design of the canonical Caliburn.Micro ViewModel (MVVM).</para>
    /// <para>See https://caliburnmicro.com/documentation/cheat-sheet</para>
    /// </summary>
    /// <seealso cref="Caliburn.Micro.Screen" />
    internal class ProgressDialogModel : Caliburn.Micro.Screen
    {
        public ManagedForm Host;

        ISession session => Host?.Runtime.Session;
        IManagedUIShell shell => Host?.Shell;

        public BitmapImage Banner => session?.GetResourceBitmap("banner").ToImageSource();

        public bool UacPromptIsVisible => (!WindowsIdentity.GetCurrent().IsAdmin() && Uac.IsEnabled() && !uacPromptActioned);

        public string CurrentAction { get => currentAction; set { currentAction = value; base.NotifyOfPropertyChange(() => CurrentAction); } }
        public int ProgressValue { get => progressValue; set { progressValue = value; base.NotifyOfPropertyChange(() => ProgressValue); } }

        public string ScoutVersion => session["VERSION"];
        bool uacPromptActioned = false;
        string currentAction;
        int progressValue;
        bool cancelClicked = false;

        public string UacPrompt
        {
            get
            {
                if (Uac.IsEnabled())
                {
                    var prompt = session?.Property("UAC_WARNING");
                    if (prompt.IsNotEmpty())
                        return prompt;
                    else
                        return
                            "Please wait for UAC prompt to appear. " +
                            "If it appears minimized then activate it from the taskbar.";
                }
                else
                    return null;
            }
        }

        public void StartExecute()
        {

            shell?.StartExecute();
            //MessageBox.Show("Execute Done");
        }

        public void Cancel()
        {
            cancelClicked = true;
            shell.Cancel();
        }

        public MessageResult ProcessMessage(InstallMessage messageType, Record messageRecord, string currentStatus)
        {
            switch (messageType)
            {
                case InstallMessage.InstallStart:
                case InstallMessage.InstallEnd:
                    {
                        uacPromptActioned = true;
                        base.NotifyOfPropertyChange(() => UacPromptIsVisible);
                    }
                    break;

                case InstallMessage.ActionStart:
                    {
                        try
                        {
                            //messageRecord[0] - is reserved for FormatString value

                            /*
                            messageRecord[2] unconditionally contains the string to display

                            Examples:

                               messageRecord[0]    "Action 23:14:50: [1]. [2]"
                               messageRecord[1]    "InstallFiles"
                               messageRecord[2]    "Copying new files"
                               messageRecord[3]    "File: [1],  Directory: [9],  Size: [6]"

                               messageRecord[0]    "Action 23:15:21: [1]. [2]"
                               messageRecord[1]    "RegisterUser"
                               messageRecord[2]    "Registering user"
                               messageRecord[3]    "[1]"

                            */

                            if (messageRecord.FieldCount >= 3)
                                CurrentAction = messageRecord[2].ToString();
                            else
                                CurrentAction = null;
                        }
                        catch
                        {
                            //Catch all, we don't want the installer to crash in an attempt to process message.
                        }
                    }
                    break;

            }
            return MessageResult.OK;
        }

        public void OnExecuteComplete(bool isUninstalling)
        {
            try
            {
                CurrentAction = null;
                if (isUninstalling)
                {
                    shell.GoTo<InstallationCancellationDialog>();

                }
                else
                {
                    if (cancelClicked)
                    {
                        shell.GoTo<InstallationCancellationDialog>();

                    }

                    if (Utils.IsAdminInstalling())
                    {
                        shell.GoTo<AdminInstallationSuccesfullDialog>();
                    }
                    else
                    {
                        shell.GoTo<InstallationSuccesfullDialog>();
                    }
                }
            }
            catch (Exception ex)
            {
#if DEBUG
                MessageBox.Show(ex.ToString() + ex.StackTrace);
#endif
                session.Log($"Installer error : {ex.ToString()} {ex.StackTrace}");
            }

        }
    }
}
oleg-shilo commented 4 months ago

The MSI session and its transactional nature are the responsibility of MSI runtime. So your user actions should only trigger the input for the runtime like "user requested exit". And then the runtime will ensure the rollback is executed.

Thus I would not try to execute rollback but only use the existing mechanisms (e.g. Cancel button) .

Though I cannot exclude that there is a way to do what you ask. I just do not know

Aaswin1996 commented 4 months ago

@oleg-shilo While we are trying to cancel an ongoing installation using the cancel button the rollback is failing with this error :

MSI (s) (70:4C) [19:03:06:780]: Error in rollback skipped. Return: 3 MSI (s) (70:4C) [19:03:06:781]: Executing op: RegOpenKey(Root=-2147483645,Key=S-1-12-1-3524205167-1077887235-3177422527-2638052887\Software\Microsoft\Installer\Products\AAB6844C287B0004F8D38CF4BFC13FA4\SourceList\Media,SecurityDescriptor=BinaryData,BinaryType=1,,) MSI (s) (70:4C) [19:03:06:783]: Executing op: RegRemoveValue(Name=240,Value=;,) MSI (s) (70:4C) [19:03:06:783]: Note: 1: 1402 2: HKEY_USERS\S-1-12-1-3524205167-1077887235-3177422527-2638052887\Software\Microsoft\Installer\Products\AAB6844C287B0004F8D38CF4BFC13FA4\SourceList\Media 3: 5 Info 1403. Could not delete value 240 from key \S-1-12-1-3524205167-1077887235-3177422527-2638052887\Software\Microsoft\Installer\Products\AAB6844C287B0004F8D38CF4BFC13FA4\SourceList\Media. System error . Verify that you have sufficient access to that key, or contact your support personnel. MSI (s) (70:4C) [19:03:06:784]: Executing op: RegCreateKey()

We have a dual purpose MSI (both per user and per machine ) .Any pointers ? Installation is per user type

oleg-shilo commented 4 months ago

@Aaswin1996, Not sure what is happening there.