自定義msi安裝包的執行過程

77rou發表於2016-03-24

有時候我們需要在程式中執行另一個程式的安裝,這就需要我們去自定義msi安裝包的執行過程。


比如我要做一個安裝管理程式,可以根據使用者的選擇安裝不同的子產品。當使用者選擇了三個產品時,如果分別顯示這三個產品的安裝互動UI顯然是不恰當的。我們期望用一個統一的自定義UI去取代每個產品各自的UI。


平時使用msiexec.exe習慣了,所以最直接的想法就是在一個子程式中執行:

         msiexec.exe /qn


這樣固然是能夠完成任務,但是不是太簡陋了? 安裝開始後我們想取消這次安裝怎麼辦? 或者我們還想要拿到一些安裝進度的資訊。


其實可以通過呼叫三個windowsAPI 輕鬆搞定這個事兒!下面的C# demo用一個自定義Form來指示多個MSI檔案的安裝過程。Form上放的是一個滾動條,並且配合一個不斷更新的label。


下面是安裝過程中的UI:

自定義msi安裝包的執行過程



點選Cancel按鈕取消安裝後的UI:


自定義msi安裝包的執行過程


先看一下這三個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 投稿

來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/28298702/viewspace-2063238/,如需轉載,請註明出處,否則將追究法律責任。

相關文章