1. 程式人生 > >在 C# 中執行 msi 安裝

在 C# 中執行 msi 安裝

gen 怎麽辦 uga air rrd adt md5 aaa qt5

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

需求

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

實現思路

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

msiexec.exe /qn

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

其實可以通過調用三個 windows API 輕松搞定這個事兒!下面的 C# demo 用一個自定義 Form 來指示多個 MSI 文件的安裝過程。Form 上放的是一個滾動條,並且配合一個不斷更新的 label。先看看 demo 長什麽樣子。
下面是安裝過程中的 UI:

技術分享

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

技術分享

主要接口介紹

我們先來了解一下主要用到的幾個 win32 API。

首先是 MsiSetInternalUI 方法:

[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)

接下來是 MsiSetExternalUI 方法:

[DllImport("msi.dll", CharSet = CharSet.Auto)]
internal static extern MsiInstallUIHandler MsiSetExternalUI([MarshalAs(UnmanagedType.FunctionPtr)] MsiInstallUIHandler puiHandler, 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,感興趣的同學可以去看看。

下面是 MsiInstallProduct 方法:

[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
public partial class InstallProcessForm : Form
{
    private MyInstaller _installer = null;
    private BackgroundWorker _installerBGWorker = new BackgroundWorker();
    internal InstallProcessForm()
    {
        InitializeComponent();

        _installer = new MyInstaller();

        _installerBGWorker.WorkerReportsProgress = true;
        _installerBGWorker.WorkerSupportsCancellation = true;

        _installerBGWorker.DoWork += _installerBGWorker_DoWork;
        _installerBGWorker.RunWorkerCompleted += _installerBGWorker_RunWorkerCompleted;
        _installerBGWorker.ProgressChanged += _installerBGWorker_ProgressChanged;

        this.Shown += InstallProcessForm_Shown;
    }

    private void InstallProcessForm_Shown(object sender, EventArgs e)
    {
        // 當窗口打開後就開始後臺的安裝
        _installerBGWorker.RunWorkerAsync();
    }

    private void _installerBGWorker_ProgressChanged(object sender, ProgressChangedEventArgs e)
    {
        // 消息通過 e.UserState 傳回,並通過label顯示在窗口上
        string message = e.UserState.ToString();
        this.label1.Text = message;
        if (message == "正在取消安裝 ...")
        {
            this.CancelButton.Enabled = false;
        }
    }

    private void _installerBGWorker_RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e)
    {
        // 安裝過程結束
    }

    private void _installerBGWorker_DoWork(object sender, DoWorkEventArgs e)
    {
        BackgroundWorker bgWorker = sender as BackgroundWorker;

        // 開始執行安裝方法
        _installer = new MyInstaller();
        string msiFilePath = "xxx.msi"; // msi file path
        _installer.Install(bgWorker, msiFilePath);
    }

    private void CancelButton_Click(object sender, EventArgs e)
    {
        _installer.Canceled = true;
     _installerBGWorker.CancelAsync();
    }
}
MyInstaller.cs
internal class MyInstaller
{
    private BackgroundWorker _bgWorker = null;

    public bool Canceled { get; set; }

    public void Install(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(new NativeMethods.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文檔
    private int MsiProgressHandler(IntPtr context, int messageType, string message)
    {
        if (this.Canceled)
        {
            if (_bgWorker != null)
            {
                _bgWorker.ReportProgress(0, "正在取消安裝 ...");
            }
            // 這個返回值會告訴msi, cancel當前的安裝
            return 2;
        }
        return 1;
    }
}

internal static class NativeMethods
{
    [DllImport("msi.dll", CharSet = CharSet.Auto)]
    internal static extern int MsiSetInternalUI(int dwUILevel, IntPtr phWnd);

    [DllImport("msi.dll", CharSet = CharSet.Auto)]
    internal static extern MyMsiInstallUIHandler MsiSetExternalUI([MarshalAs(UnmanagedType.FunctionPtr)] MyMsiInstallUIHandler puiHandler, NativeMethods.LogMode dwMessageFilter, IntPtr pvContext);

    [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(NativeMethods.LogMode dwLogMode, [MarshalAs(UnmanagedType.LPWStr)] string szLogFile, uint dwLogAttributes);

    internal delegate int MyMsiInstallUIHandler(IntPtr context, int messageType, [MarshalAs(UnmanagedType.LPWStr)] string message);

    [Flags]
    internal enum LogMode : 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 更靈活,也為程序日後添加新的功能打下了基礎。

在 C# 中執行 msi 安裝