天天看点

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

有时候我们需要在程序中执行另一个程序的安装,这就需要我们去自定义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)]

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

NativeMethods.MsiSetInternalUI(2, IntPtr.Zero)

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

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

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

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

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

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

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

45

46

47

48

49

50

51

52

53

54

55

56

57

58

59

60

61

62

63

64

65

66

67

68

69

70

71

72

73

74

75

76

77

78

79

80

81

82

83

84

85

86

87

88

89

90

91

92

93

94

95

96

97

98

99

100

101

102

103

104

105

106

107

108

109

110

111

112

113

114

115

116

117

118

119

120

121

122

123

124

125

126

127

128

129

130

131

132

133

134

135

136

137

138

<code>InstallProcessForm.cs</code>

<code>publicpartialclassInstallProcessForm : Form</code>

<code>    </code><code>{</code>

<code>        </code><code>privateMyInstaller _installer = </code><code>null</code><code>;</code>

<code>        </code><code>privateBackgroundWorker _installerBGWorker = newBackgroundWorker();</code>

<code>        </code><code>internalInstallProcessForm()</code>

<code>        </code><code>{</code>

<code>           </code><code>InitializeComponent();</code>

<code> </code> 

<code>            </code><code>_installer = newMyInstaller();</code>

<code>           </code><code>_installerBGWorker.WorkerReportsProgress = </code><code>true</code><code>;</code>

<code>           </code><code>_installerBGWorker.WorkerSupportsCancellation = </code><code>true</code><code>;</code>

<code>           </code><code>_installerBGWorker.DoWork += _installerBGWorker_DoWork;</code>

<code>            </code><code>_installerBGWorker.RunWorkerCompleted+= _installerBGWorker_RunWorkerCompleted;</code>

<code>           </code><code>_installerBGWorker.ProgressChanged +=_installerBGWorker_ProgressChanged;</code>

<code>            </code><code>this</code><code>.Shown+= InstallProcessForm_Shown;</code>

<code>        </code><code>}</code>

<code>        </code><code>privatevoidInstallProcessForm_Shown(</code><code>object</code> <code>sender, EventArgs e)</code>

<code>            </code><code>// 当窗口打开后就开始后台的安装</code>

<code>           </code><code>_installerBGWorker.RunWorkerAsync();</code>

<code>        </code><code>privatevoid_installerBGWorker_ProgressChanged(</code><code>object</code> <code>sender, ProgressChangedEventArgs e)</code>

<code>            </code><code>// 消息通过 e.UserState 传回,并通过label显示在窗口上</code>

<code>            </code><code>string</code> <code>message= e.UserState.ToString();</code>

<code>            </code><code>this</code><code>.label1.Text= message;</code>

<code>            </code><code>if</code><code>(message == </code><code>"正在取消安装..."</code><code>)</code>

<code>            </code><code>{</code>

<code>                </code><code>this</code><code>.CancelButton.Enabled= </code><code>false</code><code>;</code>

<code>            </code><code>}</code>

<code>        </code><code>privatevoid_installerBGWorker_RunWorkerCompleted(</code><code>object</code> <code>sender, RunWorkerCompletedEventArgs e)</code>

<code>            </code><code>// 安装过程结束</code>

<code>        </code><code>privatevoid_installerBGWorker_DoWork(</code><code>object</code> <code>sender, DoWorkEventArgs e)</code>

<code>            </code><code>BackgroundWorker bgWorker = sender asBackgroundWorker;</code>

<code>            </code><code>// 开始执行安装方法</code>

<code>           </code><code>_installer = newMyInstaller();</code>

<code>            </code><code>stringmsiFilePath = </code><code>"xxx.msi"</code><code>; </code><code>// msifile path</code>

<code>           </code><code>_installer.Install(bgWorker, msiFilePath);</code>

<code>        </code><code>privatevoidCancelButton_Click(</code><code>object</code> <code>sender, EventArgs e)</code>

<code>           </code><code>_installer.Canceled = </code><code>true</code><code>;</code>

<code>     </code><code>_installerBGWorker.CancelAsync();</code>

<code>}</code>

<code>MyInstaller.cs</code>

<code>internalclassMyInstaller</code>

<code>        </code><code>privateBackgroundWorker _bgWorker = </code><code>null</code><code>;</code>

<code>        </code><code>publicboolCanceled { </code><code>get</code><code>; </code><code>set</code><code>; }</code>

<code>        </code><code>publicvoidInstall(BackgroundWorker bgWorker, </code><code>string</code> <code>msiFileName)</code>

<code>           </code><code>_bgWorker = bgWorker;</code>

<code>            </code><code>NativeMethods.MyMsiInstallUIHandler oldHandler = </code><code>null</code><code>;</code>

<code>            </code><code>try</code>

<code>                </code><code>string</code> <code>logPath= </code><code>"test.log"</code><code>;</code>

<code>                </code><code>NativeMethods.MsiEnableLog(NativeMethods.LogMode.Verbose, logPath, 0u);</code>

<code>                </code><code>NativeMethods.MsiSetInternalUI(2, IntPtr.Zero);</code>

<code>               </code><code>oldHandler = NativeMethods.MsiSetExternalUI(newNativeMethods.MyMsiInstallUIHandler(MsiProgressHandler),</code>

<code>                                               </code><code>NativeMethods.LogMode.ExternalUI,</code>

<code>                                               </code><code>IntPtr.Zero);</code>

<code>                </code><code>string</code> <code>param =</code><code>"ACTION=INSTALL"</code><code>;</code>

<code>               </code><code>_bgWorker.ReportProgress(0, </code><code>"正在安装 xxx..."</code><code>);</code>

<code>                </code><code>NativeMethods.MsiInstallProduct(msiFileName, param);</code>

<code>            </code><code>catch</code><code>(Exception e)</code>

<code>                </code><code>// todo</code>

<code>            </code><code>finally</code>

<code>                </code><code>// 一定要把默认的handler设回去。</code>

<code>                </code><code>if</code><code>(oldHandler!= </code><code>null</code><code>)</code>

<code>                </code><code>{</code>

<code>                    </code><code>NativeMethods.MsiSetExternalUI(oldHandler, NativeMethods.LogMode.None, IntPtr.Zero);</code>

<code>                </code><code>}</code>

<code>        </code><code>//最重要的就是这个方法了,这里仅演示了如何cancel一个安装,更多详情请参考MSDN文档</code>

<code>        </code><code>privateintMsiProgressHandler(IntPtr context, </code><code>int</code> <code>messageType, stringmessage)</code>

<code>            </code><code>if</code> <code>(</code><code>this</code><code>.Canceled)</code>

<code>                </code><code>if</code><code>(_bgWorker != </code><code>null</code><code>)</code>

<code>                   </code><code>_bgWorker.ReportProgress(0, </code><code>"正在取消安装..."</code><code>);</code>

<code>                </code><code>// 这个返回值会告诉msi, cancel当前的安装</code>

<code>                </code><code>return</code> <code>2;</code>

<code>            </code><code>return</code> <code>1;</code>

<code>    </code><code>}</code>

<code>    </code><code>internalstaticclassNativeMethods</code>

<code>        </code><code>[DllImport(</code><code>"msi.dll"</code><code>, CharSet = CharSet.Auto)]</code>

<code>        </code><code>internalstaticexternintMsiSetInternalUI(</code><code>int</code> <code>dwUILevel, IntPtr phWnd);</code>

<code>        </code><code>internalstaticexternMyMsiInstallUIHandler MsiSetExternalUI([MarshalAs(UnmanagedType.FunctionPtr)] MyMsiInstallUIHandler puiHandler, NativeMethods.LogMode dwMessageFilter, IntPtrpvContext);</code>

<code>        </code><code>internalstaticexternuint MsiInstallProduct([MarshalAs(UnmanagedType.LPWStr)] </code><code>string</code> <code>szPackagePath, [MarshalAs(UnmanagedType.LPWStr)] </code><code>string</code> <code>szCommandLine);</code>

<code>        </code><code>internalstaticexternuintMsiEnableLog(NativeMethods.LogMode dwLogMode, [MarshalAs(UnmanagedType.LPWStr)] </code><code>string</code> <code>szLogFile, uintdwLogAttributes);</code>

<code>        </code><code>internaldelegateintMyMsiInstallUIHandler(IntPtr context, </code><code>int</code> <code>messageType, [MarshalAs(UnmanagedType.LPWStr)] </code><code>string</code> <code>message);</code>

<code>        </code><code>[Flags]</code>

<code>        </code><code>internalenumLogMode : </code><code>uint</code>

<code>            </code><code>None =0u,</code>

<code>            </code><code>Verbose= 4096u,</code>

<code>           </code><code>ExternalUI = 20239u</code>

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

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

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

本文转自 powertoolsteam 51CTO博客,原文链接:http://blog.51cto.com/powertoolsteam/1754834,如需转载请自行联系原作者

继续阅读