diff options
Diffstat (limited to 'src/dtf/WixToolset.Dtf.WindowsInstaller/InstallerUtils.cs')
-rw-r--r-- | src/dtf/WixToolset.Dtf.WindowsInstaller/InstallerUtils.cs | 472 |
1 files changed, 472 insertions, 0 deletions
diff --git a/src/dtf/WixToolset.Dtf.WindowsInstaller/InstallerUtils.cs b/src/dtf/WixToolset.Dtf.WindowsInstaller/InstallerUtils.cs new file mode 100644 index 00000000..8d9cf0a1 --- /dev/null +++ b/src/dtf/WixToolset.Dtf.WindowsInstaller/InstallerUtils.cs | |||
@@ -0,0 +1,472 @@ | |||
1 | // Copyright (c) .NET Foundation and contributors. All rights reserved. Licensed under the Microsoft Reciprocal License. See LICENSE.TXT file in the project root for full license information. | ||
2 | |||
3 | namespace WixToolset.Dtf.WindowsInstaller | ||
4 | { | ||
5 | using System; | ||
6 | using System.IO; | ||
7 | using System.Text; | ||
8 | using System.Resources; | ||
9 | using System.Reflection; | ||
10 | using System.Collections.Generic; | ||
11 | using System.Globalization; | ||
12 | using System.Runtime.InteropServices; | ||
13 | using System.Diagnostics.CodeAnalysis; | ||
14 | |||
15 | public static partial class Installer | ||
16 | { | ||
17 | /// <summary> | ||
18 | /// Gets the current version of the installer. | ||
19 | /// </summary> | ||
20 | public static Version Version | ||
21 | { | ||
22 | get | ||
23 | { | ||
24 | // TODO: Use the extended form of version info to get the 4th component of the verison. | ||
25 | uint[] dllVersionInfo = new uint[5]; | ||
26 | dllVersionInfo[0] = 20; | ||
27 | int hr = NativeMethods.DllGetVersion(dllVersionInfo); | ||
28 | if (hr != 0) | ||
29 | { | ||
30 | Marshal.ThrowExceptionForHR(hr); | ||
31 | } | ||
32 | |||
33 | return new Version((int) dllVersionInfo[1], (int) dllVersionInfo[2], (int) dllVersionInfo[3]); | ||
34 | } | ||
35 | } | ||
36 | |||
37 | internal static ResourceManager ErrorResources | ||
38 | { | ||
39 | get | ||
40 | { | ||
41 | if (errorResources == null) | ||
42 | { | ||
43 | errorResources = new ResourceManager(typeof(Installer).Namespace + ".Errors", typeof(Installer).Assembly); | ||
44 | } | ||
45 | return errorResources; | ||
46 | } | ||
47 | } | ||
48 | |||
49 | /// <summary> | ||
50 | /// Gets a Windows Installer error message in the system default language. | ||
51 | /// </summary> | ||
52 | /// <param name="errorNumber">The error number.</param> | ||
53 | /// <returns>The message string, or null if the error message is not found.</returns> | ||
54 | /// <remarks><p> | ||
55 | /// The returned string may have tokens such as [2] and [3] that are meant to be substituted | ||
56 | /// with context-specific values. | ||
57 | /// </p><p> | ||
58 | /// Error numbers greater than 2000 refer to MSI "internal" errors, and are always | ||
59 | /// returned in English. | ||
60 | /// </p></remarks> | ||
61 | public static string GetErrorMessage(int errorNumber) | ||
62 | { | ||
63 | return Installer.GetErrorMessage(errorNumber, null); | ||
64 | } | ||
65 | |||
66 | /// <summary> | ||
67 | /// Gets a Windows Installer error message in a specified language. | ||
68 | /// </summary> | ||
69 | /// <param name="errorNumber">The error number.</param> | ||
70 | /// <param name="culture">The locale for the message.</param> | ||
71 | /// <returns>The message string, or null if the error message or locale is not found.</returns> | ||
72 | /// <remarks><p> | ||
73 | /// The returned string may have tokens such as [2] and [3] that are meant to be substituted | ||
74 | /// with context-specific values. | ||
75 | /// </p><p> | ||
76 | /// Error numbers greater than 2000 refer to MSI "internal" errors, and are always | ||
77 | /// returned in English. | ||
78 | /// </p></remarks> | ||
79 | [SuppressMessage("Microsoft.Security", "CA2122:DoNotIndirectlyExposeMethodsWithLinkDemands")] | ||
80 | public static string GetErrorMessage(int errorNumber, CultureInfo culture) | ||
81 | { | ||
82 | if (culture == null) | ||
83 | { | ||
84 | culture = CultureInfo.CurrentCulture; | ||
85 | } | ||
86 | |||
87 | string msg = Installer.ErrorResources.GetString( | ||
88 | errorNumber.ToString(CultureInfo.InvariantCulture.NumberFormat), | ||
89 | culture); | ||
90 | if (msg == null) | ||
91 | { | ||
92 | string msiMsgModule = Path.Combine( | ||
93 | Environment.SystemDirectory, "msimsg.dll"); | ||
94 | msg = Installer.GetMessageFromModule( | ||
95 | msiMsgModule, errorNumber, culture); | ||
96 | } | ||
97 | return msg; | ||
98 | } | ||
99 | |||
100 | private static string GetMessageFromModule( | ||
101 | string modulePath, int errorNumber, CultureInfo culture) | ||
102 | { | ||
103 | const uint LOAD_LIBRARY_AS_DATAFILE = 2; | ||
104 | const int RT_RCDATA = 10; | ||
105 | |||
106 | IntPtr msgModule = NativeMethods.LoadLibraryEx( | ||
107 | modulePath, IntPtr.Zero, LOAD_LIBRARY_AS_DATAFILE); | ||
108 | if (msgModule == IntPtr.Zero) | ||
109 | { | ||
110 | return null; | ||
111 | } | ||
112 | |||
113 | try | ||
114 | { | ||
115 | // On pre-Vista systems, the messages are stored as RCDATA resources. | ||
116 | |||
117 | int lcid = (culture == CultureInfo.InvariantCulture) ? | ||
118 | 0 : culture.LCID; | ||
119 | IntPtr resourceInfo = NativeMethods.FindResourceEx( | ||
120 | msgModule, | ||
121 | new IntPtr(RT_RCDATA), | ||
122 | new IntPtr(errorNumber), | ||
123 | (ushort) lcid); | ||
124 | if (resourceInfo != IntPtr.Zero) | ||
125 | { | ||
126 | IntPtr resourceData = NativeMethods.LoadResource( | ||
127 | msgModule, resourceInfo); | ||
128 | IntPtr resourcePtr = NativeMethods.LockResource(resourceData); | ||
129 | |||
130 | if (lcid == 0) | ||
131 | { | ||
132 | string msg = Marshal.PtrToStringAnsi(resourcePtr); | ||
133 | return msg; | ||
134 | } | ||
135 | else | ||
136 | { | ||
137 | int len = 0; | ||
138 | while (Marshal.ReadByte(resourcePtr, len) != 0) | ||
139 | { | ||
140 | len++; | ||
141 | } | ||
142 | byte[] msgBytes = new byte[len + 1]; | ||
143 | Marshal.Copy(resourcePtr, msgBytes, 0, msgBytes.Length); | ||
144 | Encoding encoding = Encoding.GetEncoding( | ||
145 | culture.TextInfo.ANSICodePage); | ||
146 | string msg = encoding.GetString(msgBytes); | ||
147 | return msg; | ||
148 | } | ||
149 | } | ||
150 | else | ||
151 | { | ||
152 | // On Vista (and above?), the messages are stored in the module message table. | ||
153 | // They're actually in MUI files, and the redirection happens automatically here. | ||
154 | |||
155 | const uint FORMAT_MESSAGE_IGNORE_INSERTS = 0x00000200; | ||
156 | const uint FORMAT_MESSAGE_FROM_HMODULE = 0x00000800; | ||
157 | const uint MESSAGE_OFFSET = 20000; // Not documented, but observed on Vista | ||
158 | |||
159 | StringBuilder buf = new StringBuilder(1024); | ||
160 | uint formatCount = NativeMethods.FormatMessage( | ||
161 | FORMAT_MESSAGE_FROM_HMODULE | FORMAT_MESSAGE_IGNORE_INSERTS, | ||
162 | msgModule, | ||
163 | (uint) errorNumber + MESSAGE_OFFSET, | ||
164 | (ushort) lcid, | ||
165 | buf, | ||
166 | (uint) buf.Capacity, | ||
167 | IntPtr.Zero); | ||
168 | |||
169 | return formatCount != 0 ? buf.ToString().Trim() : null; | ||
170 | } | ||
171 | } | ||
172 | finally | ||
173 | { | ||
174 | NativeMethods.FreeLibrary(msgModule); | ||
175 | } | ||
176 | } | ||
177 | |||
178 | /// <summary> | ||
179 | /// Gets a formatted Windows Installer error message in the system default language. | ||
180 | /// </summary> | ||
181 | /// <param name="errorRecord">Error record containing the error number in the first field, and | ||
182 | /// error-specific parameters in the other fields.</param> | ||
183 | /// <returns>The message string, or null if the error message is not found.</returns> | ||
184 | /// <remarks><p> | ||
185 | /// Error numbers greater than 2000 refer to MSI "internal" errors, and are always | ||
186 | /// returned in English. | ||
187 | /// </p></remarks> | ||
188 | public static string GetErrorMessage(Record errorRecord) { return Installer.GetErrorMessage(errorRecord, null); } | ||
189 | |||
190 | /// <summary> | ||
191 | /// Gets a formatted Windows Installer error message in a specified language. | ||
192 | /// </summary> | ||
193 | /// <param name="errorRecord">Error record containing the error number in the first field, and | ||
194 | /// error-specific parameters in the other fields.</param> | ||
195 | /// <param name="culture">The locale for the message.</param> | ||
196 | /// <returns>The message string, or null if the error message or locale is not found.</returns> | ||
197 | /// <remarks><p> | ||
198 | /// Error numbers greater than 2000 refer to MSI "internal" errors, and are always | ||
199 | /// returned in English. | ||
200 | /// </p></remarks> | ||
201 | public static string GetErrorMessage(Record errorRecord, CultureInfo culture) | ||
202 | { | ||
203 | if (errorRecord == null) | ||
204 | { | ||
205 | throw new ArgumentNullException("errorRecord"); | ||
206 | } | ||
207 | int errorNumber; | ||
208 | if (errorRecord.FieldCount < 1 || (errorNumber = (int) errorRecord.GetInteger(1)) == 0) | ||
209 | { | ||
210 | throw new ArgumentOutOfRangeException("errorRecord"); | ||
211 | } | ||
212 | |||
213 | string msg = Installer.GetErrorMessage(errorNumber, culture); | ||
214 | if (msg != null) | ||
215 | { | ||
216 | errorRecord.FormatString = msg; | ||
217 | msg = errorRecord.ToString((IFormatProvider)null); | ||
218 | } | ||
219 | return msg; | ||
220 | } | ||
221 | |||
222 | /// <summary> | ||
223 | /// Gets the version string of the path specified using the format that the installer | ||
224 | /// expects to find it in in the database. | ||
225 | /// </summary> | ||
226 | /// <param name="path">Path to the file</param> | ||
227 | /// <returns>Version string in the "#.#.#.#" format, or an empty string if the file | ||
228 | /// does not contain version information</returns> | ||
229 | /// <exception cref="FileNotFoundException">the file does not exist or could not be read</exception> | ||
230 | /// <remarks><p> | ||
231 | /// Win32 MSI API: | ||
232 | /// <a href="http://msdn.microsoft.com/library/en-us/msi/setup/msigetfileversion.asp">MsiGetFileVersion</a> | ||
233 | /// </p></remarks> | ||
234 | public static string GetFileVersion(string path) | ||
235 | { | ||
236 | StringBuilder version = new StringBuilder(20); | ||
237 | uint verBufSize = 0, langBufSize = 0; | ||
238 | uint ret = NativeMethods.MsiGetFileVersion(path, version, ref verBufSize, null, ref langBufSize); | ||
239 | if (ret == (uint) NativeMethods.Error.MORE_DATA) | ||
240 | { | ||
241 | version.Capacity = (int) ++verBufSize; | ||
242 | ret = NativeMethods.MsiGetFileVersion(path, version, ref verBufSize, null, ref langBufSize); | ||
243 | } | ||
244 | |||
245 | if (ret != 0 && ret != (uint) NativeMethods.Error.FILE_INVALID) | ||
246 | { | ||
247 | if (ret == (uint) NativeMethods.Error.FILE_NOT_FOUND || | ||
248 | ret == (uint) NativeMethods.Error.ACCESS_DENIED) | ||
249 | { | ||
250 | throw new FileNotFoundException(null, path); | ||
251 | } | ||
252 | else | ||
253 | { | ||
254 | throw InstallerException.ExceptionFromReturnCode(ret); | ||
255 | } | ||
256 | } | ||
257 | return version.ToString(); | ||
258 | } | ||
259 | |||
260 | /// <summary> | ||
261 | /// Gets the language string of the path specified using the format that the installer | ||
262 | /// expects to find them in in the database. | ||
263 | /// </summary> | ||
264 | /// <param name="path">Path to the file</param> | ||
265 | /// <returns>Language string in the form of a decimal language ID, or an empty string if the file | ||
266 | /// does not contain a language ID</returns> | ||
267 | /// <exception cref="FileNotFoundException">the file does not exist or could not be read</exception> | ||
268 | /// <remarks><p> | ||
269 | /// Win32 MSI API: | ||
270 | /// <a href="http://msdn.microsoft.com/library/en-us/msi/setup/msigetfileversion.asp">MsiGetFileVersion</a> | ||
271 | /// </p></remarks> | ||
272 | public static string GetFileLanguage(string path) | ||
273 | { | ||
274 | StringBuilder language = new StringBuilder("", 10); | ||
275 | uint verBufSize = 0, langBufSize = 0; | ||
276 | uint ret = NativeMethods.MsiGetFileVersion(path, null, ref verBufSize, language, ref langBufSize); | ||
277 | if (ret == (uint) NativeMethods.Error.MORE_DATA) | ||
278 | { | ||
279 | language.Capacity = (int) ++langBufSize; | ||
280 | ret = NativeMethods.MsiGetFileVersion(path, null, ref verBufSize, language, ref langBufSize); | ||
281 | } | ||
282 | |||
283 | if (ret != 0 && ret != (uint) NativeMethods.Error.FILE_INVALID) | ||
284 | { | ||
285 | if (ret == (uint) NativeMethods.Error.FILE_NOT_FOUND || | ||
286 | ret == (uint) NativeMethods.Error.ACCESS_DENIED) | ||
287 | { | ||
288 | throw new FileNotFoundException(null, path); | ||
289 | } | ||
290 | else | ||
291 | { | ||
292 | throw InstallerException.ExceptionFromReturnCode(ret); | ||
293 | } | ||
294 | } | ||
295 | return language.ToString(); | ||
296 | } | ||
297 | |||
298 | /// <summary> | ||
299 | /// Gets a 128-bit hash of the specified file. | ||
300 | /// </summary> | ||
301 | /// <param name="path">Path to the file</param> | ||
302 | /// <param name="hash">Integer array of length 4 which receives the | ||
303 | /// four 32-bit parts of the hash value.</param> | ||
304 | /// <exception cref="FileNotFoundException">the file does not exist or | ||
305 | /// could not be read</exception> | ||
306 | /// <remarks><p> | ||
307 | /// Win32 MSI API: | ||
308 | /// <a href="http://msdn.microsoft.com/library/en-us/msi/setup/msigetfilehash.asp">MsiGetFileHash</a> | ||
309 | /// </p></remarks> | ||
310 | public static void GetFileHash(string path, int[] hash) | ||
311 | { | ||
312 | if (hash == null) | ||
313 | { | ||
314 | throw new ArgumentNullException("hash"); | ||
315 | } | ||
316 | |||
317 | uint[] tempHash = new uint[5]; | ||
318 | tempHash[0] = 20; | ||
319 | uint ret = NativeMethods.MsiGetFileHash(path, 0, tempHash); | ||
320 | if (ret != 0) | ||
321 | { | ||
322 | if (ret == (uint) NativeMethods.Error.FILE_NOT_FOUND || | ||
323 | ret == (uint) NativeMethods.Error.ACCESS_DENIED) | ||
324 | { | ||
325 | throw new FileNotFoundException(null, path); | ||
326 | } | ||
327 | else | ||
328 | { | ||
329 | throw InstallerException.ExceptionFromReturnCode(ret); | ||
330 | } | ||
331 | } | ||
332 | |||
333 | for (int i = 0; i < 4; i++) | ||
334 | { | ||
335 | hash[i] = unchecked ((int) tempHash[i + 1]); | ||
336 | } | ||
337 | } | ||
338 | |||
339 | /// <summary> | ||
340 | /// Examines a shortcut and returns its product, feature name, and component if available. | ||
341 | /// </summary> | ||
342 | /// <param name="shortcut">Full path to a shortcut</param> | ||
343 | /// <returns>ShortcutTarget structure containing target product code, feature, and component code</returns> | ||
344 | /// <remarks><p> | ||
345 | /// Win32 MSI API: | ||
346 | /// <a href="http://msdn.microsoft.com/library/en-us/msi/setup/msigetshortcuttarget.asp">MsiGetShortcutTarget</a> | ||
347 | /// </p></remarks> | ||
348 | public static ShortcutTarget GetShortcutTarget(string shortcut) | ||
349 | { | ||
350 | StringBuilder productBuf = new StringBuilder(40); | ||
351 | StringBuilder featureBuf = new StringBuilder(40); | ||
352 | StringBuilder componentBuf = new StringBuilder(40); | ||
353 | |||
354 | uint ret = NativeMethods.MsiGetShortcutTarget(shortcut, productBuf, featureBuf, componentBuf); | ||
355 | if (ret != 0) | ||
356 | { | ||
357 | throw InstallerException.ExceptionFromReturnCode(ret); | ||
358 | } | ||
359 | return new ShortcutTarget( | ||
360 | productBuf.Length > 0 ? productBuf.ToString() : null, | ||
361 | featureBuf.Length > 0 ? featureBuf.ToString() : null, | ||
362 | componentBuf.Length > 0 ? componentBuf.ToString() : null); | ||
363 | } | ||
364 | |||
365 | /// <summary> | ||
366 | /// Verifies that the given file is an installation package. | ||
367 | /// </summary> | ||
368 | /// <param name="packagePath">Path to the package</param> | ||
369 | /// <returns>True if the file is an installation package; false otherwise.</returns> | ||
370 | /// <exception cref="FileNotFoundException">the specified package file does not exist</exception> | ||
371 | /// <exception cref="InstallerException">the package file could not be opened</exception> | ||
372 | /// <remarks><p> | ||
373 | /// Win32 MSI API: | ||
374 | /// <a href="http://msdn.microsoft.com/library/en-us/msi/setup/msiverifypackage.asp">MsiVerifyPackage</a> | ||
375 | /// </p></remarks> | ||
376 | public static bool VerifyPackage(string packagePath) | ||
377 | { | ||
378 | if (String.IsNullOrEmpty(packagePath)) | ||
379 | { | ||
380 | throw new ArgumentNullException("packagePath"); | ||
381 | } | ||
382 | |||
383 | if (!File.Exists(packagePath)) | ||
384 | { | ||
385 | throw new FileNotFoundException(null, packagePath); | ||
386 | } | ||
387 | |||
388 | uint ret = NativeMethods.MsiVerifyPackage(packagePath); | ||
389 | if (ret == (uint) NativeMethods.Error.INSTALL_PACKAGE_INVALID) | ||
390 | { | ||
391 | return false; | ||
392 | } | ||
393 | else if (ret != 0) | ||
394 | { | ||
395 | throw InstallerException.ExceptionFromReturnCode(ret); | ||
396 | } | ||
397 | return true; | ||
398 | } | ||
399 | |||
400 | /// <summary> | ||
401 | /// [MSI 4.0] Gets the list of files that can be updated by one or more patches. | ||
402 | /// </summary> | ||
403 | /// <param name="productCode">ProductCode (GUID) of the product which is | ||
404 | /// the target of the patches</param> | ||
405 | /// <param name="patches">list of file paths of one or more patches to be | ||
406 | /// analyzed</param> | ||
407 | /// <returns>List of absolute paths of files that can be updated when the | ||
408 | /// patches are applied on this system.</returns> | ||
409 | /// <remarks><p> | ||
410 | /// Win32 MSI API: | ||
411 | /// <a href="http://msdn.microsoft.com/library/en-us/msi/setup/msigetpatchfilelist.asp">MsiGetPatchFileList</a> | ||
412 | /// </p></remarks> | ||
413 | public static IList<string> GetPatchFileList(string productCode, IList<string> patches) | ||
414 | { | ||
415 | if (String.IsNullOrEmpty(productCode)) | ||
416 | { | ||
417 | throw new ArgumentNullException("productCode"); | ||
418 | } | ||
419 | |||
420 | if (patches == null || patches.Count == 0) | ||
421 | { | ||
422 | throw new ArgumentNullException("patches"); | ||
423 | } | ||
424 | |||
425 | StringBuilder patchList = new StringBuilder(); | ||
426 | foreach (string patch in patches) | ||
427 | { | ||
428 | if (patch != null) | ||
429 | { | ||
430 | if (patchList.Length != 0) | ||
431 | { | ||
432 | patchList.Append(';'); | ||
433 | } | ||
434 | |||
435 | patchList.Append(patch); | ||
436 | } | ||
437 | } | ||
438 | |||
439 | if (patchList.Length == 0) | ||
440 | { | ||
441 | throw new ArgumentNullException("patches"); | ||
442 | } | ||
443 | |||
444 | IntPtr phFileRecords; | ||
445 | uint cFiles; | ||
446 | |||
447 | uint ret = NativeMethods.MsiGetPatchFileList( | ||
448 | productCode, | ||
449 | patchList.ToString(), | ||
450 | out cFiles, | ||
451 | out phFileRecords); | ||
452 | if (ret != 0) | ||
453 | { | ||
454 | throw InstallerException.ExceptionFromReturnCode(ret); | ||
455 | } | ||
456 | |||
457 | List<string> files = new List<string>(); | ||
458 | |||
459 | for (uint i = 0; i < cFiles; i++) | ||
460 | { | ||
461 | int hFileRec = Marshal.ReadInt32(phFileRecords, (int) i); | ||
462 | |||
463 | using (Record fileRec = new Record(hFileRec, true, null)) | ||
464 | { | ||
465 | files.Add(fileRec.GetString(1)); | ||
466 | } | ||
467 | } | ||
468 | |||
469 | return files; | ||
470 | } | ||
471 | } | ||
472 | } | ||