自定义msi安装包的执行过程

Posted

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了自定义msi安装包的执行过程相关的知识,希望对你有一定的参考价值。

有时候我们需要在程序中执行另一个程序的安装,这就需要我们去自定义msi安装包的执行过程。


 


比如我要做一个安装管理程序,可以根据用户的选择安装不同的子产品。当用户选择了三个产品时,如果分别显示这三个产品的安装交互UI显然是不恰当的。我们期望用一个统一的自定义UI去取代每个产品各自的UI


 

平时使用msiexec.exe习惯了,所以最直接的想法就是在一个子进程中执行:

         msiexec.exe /qn



这样固然是能够完成任务,但是不是太简陋了? 安装开始后我们想取消这次安装怎么办? 或者我们还想要拿到一些安装进度的信息。


 


其实可以通过调用三个windowsAPI 轻松搞定这个事儿!下面的C# demo用一个自定义Form来指示多个MSI文件的安装过程。Form上放的是一个滚动条,并且配合一个不断更新的label


 

下面是安装过程中的UI

技术分享




点击Cancel按钮取消安装后的UI:


技术分享


先看一下这三个API:


[DllImport("msi.dll", CharSet = CharSet.Auto)]

internal static extern int MsiSetInternalUI(int dwUILevel, IntPtr phWnd);



在调用msiexec.exe时,我们通过指定 /q参数让安装过程显示不同的UI。如果不显示UI的话就要使用参数 /qn 。MsiSetInternalUI方法就是干这个事儿的。通过下面的调用就可以去掉msi中自带的UI:

NativeMethods.MsiSetInternalUI(2, IntPtr.Zero)



[DllImport("msi.dll", CharSet = CharSet.Auto)]

internal static extern MsiInstallUIHandler MsiSetExternalUI([MarshalAs(UnmanagedType.FunctionPtr)] MsiInstallUIHandlerpuiHandler, NativeMethods.InstallLogMode dwMessageFilter, IntPtr pvContext);


MsiSetExternalUI 函数允许指定一个用户定义的外部UI handler用来处理安装过程中产生的消息。这个外部的UI handler会在内部的UI handler被调用前调用。 如果在外部的UI handler中返回非0的值,就说明这个消息已经被处理。

 

这个外部的UI handler就是MsiSetExternalUI方法的第一个参数,我们通过实现这个handler来处理自己感兴趣的消息, 比如当安装进度变化后去更新进度条。或者通过它传递我们的消息给msi,比如说告诉msi,停止安装,执行cancel操作。使用这个方法需要注意的是,当你完成安装后一定要把原来的handler设回去。否则以后执行msi安装包可能会出问题。


MSDN上有一个MsiInstallUIHandler 的demo,感兴趣的同学可以看看。


[DllImport("msi.dll", CharSet = CharSet.Auto)]

internal static extern uint MsiInstallProduct([MarshalAs(UnmanagedType.LPWStr)] string szPackagePath,[MarshalAs(UnmanagedType.LPWStr)] string szCommandLine);



正如其名,这个是真正干活儿的方法。


实在忍不住要介绍第四个方法,虽然它对实现当前的功能来说是可选的,但对一个产品来说,它却是用来救命的。

[DllImport("msi.dll", CharSet = CharSet.Auto)]

internal static extern uint MsiEnableLog(GcMsiUtil.NativeMethods.InstallLogMode dwLogMode,[MarshalAs(UnmanagedType.LPWStr)] string szLogFile, uint dwLogAttributes);

 

这个方法会把安装log保存到你传递给它的文件路径。有了它生活就会happy很多,很多… 否则当用户告诉你安装失败时,你一定会抓狂的。

 

好了,下面是MyInstaller demo的主要代码:

 

InstallProcessForm.cs
publicpartialclassInstallProcessForm : Form
    {
        privateMyInstaller _installer = null;
        privateBackgroundWorker _installerBGWorker = newBackgroundWorker();
        internalInstallProcessForm()
        {
           InitializeComponent();
 
            _installer = newMyInstaller();
 
           _installerBGWorker.WorkerReportsProgress = true;
           _installerBGWorker.WorkerSupportsCancellation = true;
 
           _installerBGWorker.DoWork += _installerBGWorker_DoWork;
            _installerBGWorker.RunWorkerCompleted+= _installerBGWorker_RunWorkerCompleted;
           _installerBGWorker.ProgressChanged +=_installerBGWorker_ProgressChanged;
 
            this.Shown+= InstallProcessForm_Shown;
        }
 
        privatevoidInstallProcessForm_Shown(object sender, EventArgs e)
        {
            // 当窗口打开后就开始后台的安装
           _installerBGWorker.RunWorkerAsync();
        }
 
        privatevoid_installerBGWorker_ProgressChanged(object sender, ProgressChangedEventArgs e)
        {
            // 消息通过 e.UserState 传回,并通过label显示在窗口上
            string message= e.UserState.ToString();
            this.label1.Text= message;
            if(message == "正在取消安装...")
            {
                this.CancelButton.Enabled= false;
            }
        }
 
        privatevoid_installerBGWorker_RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e)
        {
            // 安装过程结束
        }
 
        privatevoid_installerBGWorker_DoWork(object sender, DoWorkEventArgs e)
        {
            BackgroundWorker bgWorker = sender asBackgroundWorker;
 
            // 开始执行安装方法
           _installer = newMyInstaller();
            stringmsiFilePath = "xxx.msi"; // msifile path
           _installer.Install(bgWorker, msiFilePath);
        }
 
        privatevoidCancelButton_Click(object sender, EventArgs e)
        {
           _installer.Canceled = true;
     _installerBGWorker.CancelAsync();
        }
}
MyInstaller.cs
internalclassMyInstaller
    {
        privateBackgroundWorker _bgWorker = null;
 
        publicboolCanceled { get; set; }
 
        publicvoidInstall(BackgroundWorker bgWorker, string msiFileName)
        {
           _bgWorker = bgWorker;
 
            NativeMethods.MyMsiInstallUIHandler oldHandler = null;
            try
            {
                string logPath= "test.log";
                NativeMethods.MsiEnableLog(NativeMethods.LogMode.Verbose, logPath, 0u);
                NativeMethods.MsiSetInternalUI(2, IntPtr.Zero);
 
               oldHandler = NativeMethods.MsiSetExternalUI(newNativeMethods.MyMsiInstallUIHandler(MsiProgressHandler),
                                               NativeMethods.LogMode.ExternalUI,
                                               IntPtr.Zero);
                string param ="ACTION=INSTALL";
               _bgWorker.ReportProgress(0, "正在安装 xxx...");
                NativeMethods.MsiInstallProduct(msiFileName, param);
            }
            catch(Exception e)
            {
                // todo
            }
            finally
            {
                // 一定要把默认的handler设回去。
                if(oldHandler!= null)
                {
                    NativeMethods.MsiSetExternalUI(oldHandler, NativeMethods.LogMode.None, IntPtr.Zero);
                }
            }
        }
 
        //最重要的就是这个方法了,这里仅演示了如何cancel一个安装,更多详情请参考MSDN文档
        privateintMsiProgressHandler(IntPtr context, int messageType, stringmessage)
        {
            if (this.Canceled)
            {
                if(_bgWorker != null)
                {
                   _bgWorker.ReportProgress(0, "正在取消安装...");
                }
                // 这个返回值会告诉msi, cancel当前的安装
                return 2;
            }
            return 1;
        }
    }
 
    internalstaticclassNativeMethods
    {
        [DllImport("msi.dll", CharSet = CharSet.Auto)]
        internalstaticexternintMsiSetInternalUI(int dwUILevel, IntPtr phWnd);
 
        [DllImport("msi.dll", CharSet = CharSet.Auto)]
        internalstaticexternMyMsiInstallUIHandler MsiSetExternalUI([MarshalAs(UnmanagedType.FunctionPtr)] MyMsiInstallUIHandler puiHandler, NativeMethods.LogMode dwMessageFilter, IntPtrpvContext);
 
        [DllImport("msi.dll", CharSet = CharSet.Auto)]
        internalstaticexternuint MsiInstallProduct([MarshalAs(UnmanagedType.LPWStr)] string szPackagePath, [MarshalAs(UnmanagedType.LPWStr)] string szCommandLine);
 
        [DllImport("msi.dll", CharSet = CharSet.Auto)]
        internalstaticexternuintMsiEnableLog(NativeMethods.LogMode dwLogMode, [MarshalAs(UnmanagedType.LPWStr)] string szLogFile, uintdwLogAttributes);
 
        internaldelegateintMyMsiInstallUIHandler(IntPtr context, int messageType, [MarshalAs(UnmanagedType.LPWStr)] string message);
 
        [Flags]
        internalenumLogMode : uint
        {
            None =0u,
            Verbose= 4096u,
           ExternalUI = 20239u
        }
    }




 简单说明一下,用户定义的UI运行在主线程中,使用BackgroundWorker执行安装任务。在安装进行的过程中可以把cancel信息传递给MsiProgressHandler,当MsiProgressHandler检测到cancel信息后通过返回值告诉msi的执行引擎,执行cancel操作(msi的安装过程是相当严谨的,可不能简单的杀掉安装进程了事!)。

这样,一个支持cancel的自定义UI的安装控制程序就OK了(demo哈)。如果要安装多个msi只需在Install方法中循环就可以了。

 

 总结一下,通过调用几个windows API,我们可以实现对msi安装过程的控制。这比调用msiexec.exe更灵活,也为程序日后添加新的功能打下了基础。


感谢葡萄哥Nick 投稿


本文出自 “葡萄城控件技术团队博客” 博客,请务必保留此出处http://powertoolsteam.blog.51cto.com/2369428/1754834

以上是关于自定义msi安装包的执行过程的主要内容,如果未能解决你的问题,请参考以下文章

MSI安装被自定义操作DLL中断

InstallShield 执行卸载自定义动作

如何找到已安装 MSI 文件的升级代码?

Wix静默安装MSI我们可以停止创建目录吗

在引导程序期间在 Msi 包之间执行脚本

自定义操作不起作用 msiexec /a - Visual Studio 安装项目