有時候我們需要在程序中執行另一個程序的安裝,這就需要我們去自定義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
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更靈活,也為程序日后添加新的功能打下了基礎。
感謝葡萄哥Nick 投稿
