main
cs 451 lines 18.9 KB
Raw
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.Core.Native
4 {
5 using System;
6 using System.Collections.Generic;
7 using System.ComponentModel;
8 using System.IO;
9 using System.Linq;
10 using System.Threading;
11 using WixToolset.Core.Native.Msi;
12 using WixToolset.Data;
13
14 /// <summary>
15 /// Windows installer validation implementation.
16 /// </summary>
17 public class WindowsInstallerValidator
18 {
19 private const string CubesFolder = "cubes";
20
21 private readonly InstallUIHandler validationUIHandlerDelegate;
22
23 /// <summary>
24 /// Creates a new Windows Installer validator.
25 /// </summary>
26 /// <param name="callback">Callback interface to handle messages.</param>
27 /// <param name="databasePath">Database to validate.</param>
28 /// <param name="cubeFiles">Set of CUBe files to merge.</param>
29 /// <param name="ices">ICEs to execute.</param>
30 /// <param name="suppressedIces">Suppressed ICEs.</param>
31 public WindowsInstallerValidator(IWindowsInstallerValidatorCallback callback, string databasePath, IEnumerable<string> cubeFiles, IEnumerable<string> ices, IEnumerable<string> suppressedIces)
32 {
33 this.Callback = callback;
34 this.DatabasePath = databasePath;
35 this.CubeFiles = cubeFiles;
36 this.Ices = new SortedSet<string>(ices);
37 this.SuppressedIces = new SortedSet<string>(suppressedIces);
38
39 // Hold a reference to our callback beyond when the external UI handler is reset.
40 this.validationUIHandlerDelegate = new InstallUIHandler(this.ValidationUIHandler);
41 }
42
43 private IWindowsInstallerValidatorCallback Callback { get; }
44
45 private string DatabasePath { get; }
46
47 private IEnumerable<string> CubeFiles { get; }
48
49 private SortedSet<string> Ices { get; }
50
51 private SortedSet<string> SuppressedIces { get; }
52
53 private bool ValidationSessionInProgress { get; set; }
54
55 private string CurrentIce { get; set; }
56
57 /// <summary>
58 /// Execute the validations.
59 /// </summary>
60 public void Execute()
61 {
62 using (var mutex = new Mutex(false, "WixValidator"))
63 {
64 try
65 {
66 if (!mutex.WaitOne(0))
67 {
68 this.Callback.WriteMessage(VerboseMessages.ValidationSerialized());
69 mutex.WaitOne();
70 }
71 }
72 catch (AbandonedMutexException)
73 {
74 // Another validation process was probably killed, we own the mutex now.
75 }
76
77 try
78 {
79 this.RunValidations();
80 }
81 finally
82 {
83 mutex.ReleaseMutex();
84 }
85 }
86 }
87
88 private void RunValidations()
89 {
90 var previousUILevel = (int)InstallUILevels.Basic;
91 var previousHwnd = IntPtr.Zero;
92 InstallUIHandler previousUIHandler = null;
93
94 try
95 {
96 using (var database = new Database(this.DatabasePath, OpenDatabase.Direct))
97 {
98 var propertyTableExists = database.TableExists("Property");
99 string productCode = null;
100
101 // Remove the product code from the database before opening a session to prevent opening an installed product.
102 if (propertyTableExists)
103 {
104 using (var view = database.OpenExecuteView("SELECT `Value` FROM `Property` WHERE Property = 'ProductCode'"))
105 {
106 using (var record = view.Fetch())
107 {
108 if (null != record)
109 {
110 productCode = record.GetString(1);
111
112 using (var dropProductCodeView = database.OpenExecuteView("DELETE FROM `Property` WHERE `Property` = 'ProductCode'"))
113 {
114 }
115 }
116 }
117 }
118 }
119
120 // Merge in the cube databases.
121 foreach (var cubeFile in this.CubeFiles)
122 {
123 var findCubeFile = typeof(WindowsInstallerValidator).Assembly.FindFileRelativeToAssembly(Path.Combine(CubesFolder, cubeFile), searchNativeDllDirectories: false);
124
125 if (!findCubeFile.Found)
126 {
127 this.Callback.WriteMessage(ErrorMessages.CubeFileNotFound(findCubeFile.Path));
128 continue;
129 }
130
131 try
132 {
133 using (var cubeDatabase = new Database(findCubeFile.Path, OpenDatabase.ReadOnly))
134 {
135 try
136 {
137 database.Merge(cubeDatabase, "MergeConflicts");
138 }
139 catch
140 {
141 // ignore merge errors since they are expected in the _Validation table
142 }
143 }
144 }
145 catch (Win32Exception e)
146 {
147 if (0x6E == e.NativeErrorCode) // ERROR_OPEN_FAILED
148 {
149 this.Callback.WriteMessage(ErrorMessages.CubeFileNotFound(findCubeFile.Path));
150 }
151 else
152 {
153 this.Callback.WriteMessage(ErrorMessages.UnexpectedException($"Unexpected exception while merging CUB: {findCubeFile.Path}, detail: {e.Message}", e.GetType().ToString(), e.StackTrace));
154 }
155 }
156 }
157
158 // Commit the database before proceeding to ensure the streams don't get confused.
159 database.Commit();
160
161 // The property table may have been added to the database from a cub database without the proper validation rows.
162 if (!propertyTableExists)
163 {
164 using (var view = database.OpenExecuteView("DROP table `Property`"))
165 {
166 }
167 }
168
169 // Get all the action names for ICEs which have not been suppressed.
170 var actions = new List<string>();
171 using (var view = database.OpenExecuteView("SELECT `Action` FROM `_ICESequence` ORDER BY `Sequence`"))
172 {
173 foreach (var record in view.Records)
174 {
175 var action = record.GetString(1);
176
177 if (!this.SuppressedIces.Contains(action) && (this.Ices.Count == 0 || this.Ices.Contains(action)))
178 {
179 actions.Add(action);
180 }
181 }
182 }
183
184 // Disable the internal UI handler and set an external UI handler.
185 previousUILevel = Installer.SetInternalUI((int)InstallUILevels.None, ref previousHwnd);
186 previousUIHandler = Installer.SetExternalUI(this.validationUIHandlerDelegate, (int)InstallLogModes.Error | (int)InstallLogModes.Warning | (int)InstallLogModes.User, IntPtr.Zero);
187
188 // Create a session for running the ICEs.
189 this.ValidationSessionInProgress = true;
190
191 using (var session = new Session(database))
192 {
193 // Some CUBs erroneously have a ProductCode property, so delete it if we just picked one up.
194 if (propertyTableExists)
195 {
196 using (var dropProductCodeView = database.OpenExecuteView("DELETE FROM `Property` WHERE `Property` = 'ProductCode'"))
197 {
198 }
199 }
200
201 // Add the product code back into the database.
202 if (null != productCode)
203 {
204 using (var view = database.OpenExecuteView($"INSERT INTO `Property` (`Property`, `Value`) VALUES ('ProductCode', '{productCode}')"))
205 {
206 }
207 }
208
209 foreach (var action in actions)
210 {
211 this.CurrentIce = action;
212
213 try
214 {
215 session.DoAction(action);
216 }
217 catch (Win32Exception e)
218 {
219 if (!this.Callback.EncounteredError)
220 {
221 this.Callback.WriteMessage(ErrorMessages.UnexpectedException($"Unexpected exception while executing ICE: {action}, detail: {e.Message}", e.GetType().ToString(), e.StackTrace));
222 }
223 }
224
225 this.CurrentIce = null;
226 }
227
228 // Mark the validation session complete so we ignore any messages that MSI may fire
229 // during session clean-up.
230 this.ValidationSessionInProgress = false;
231 }
232 }
233 }
234 catch (Win32Exception e)
235 {
236 // Avoid displaying errors twice since one may have already occurred in the UI handler.
237 if (!this.Callback.EncounteredError)
238 {
239 if (0x6E == e.NativeErrorCode) // ERROR_OPEN_FAILED
240 {
241 // The database path is not passed to this exception since inside wix.exe
242 // this would be the temporary copy and there would be no final output becasue
243 // this error occured; and during standalone validation they should know the path
244 // passed in.
245 this.Callback.WriteMessage(ErrorMessages.ValidationFailedToOpenDatabase());
246 }
247 else if (0x64D == e.NativeErrorCode)
248 {
249 this.Callback.WriteMessage(ErrorMessages.ValidationFailedDueToLowMsiEngine());
250 }
251 else if (0x654 == e.NativeErrorCode)
252 {
253 this.Callback.WriteMessage(ErrorMessages.ValidationFailedDueToInvalidPackage());
254 }
255 else if (0x658 == e.NativeErrorCode)
256 {
257 this.Callback.WriteMessage(ErrorMessages.ValidationFailedDueToMultilanguageMergeModule());
258 }
259 else if (0x659 == e.NativeErrorCode)
260 {
261 this.Callback.WriteMessage(WarningMessages.ValidationFailedDueToSystemPolicy());
262 }
263 else
264 {
265 var msg = String.IsNullOrEmpty(this.CurrentIce) ? e.Message : $"Action - '{this.CurrentIce}' {e.Message}";
266
267 this.Callback.WriteMessage(ErrorMessages.Win32Exception(e.NativeErrorCode, msg));
268 }
269 }
270 }
271 finally
272 {
273 this.ValidationSessionInProgress = false;
274
275 Installer.SetExternalUI(previousUIHandler, 0, IntPtr.Zero);
276 Installer.SetInternalUI(previousUILevel, ref previousHwnd);
277 }
278 }
279
280 /// <summary>
281 /// The validation external UI handler.
282 /// </summary>
283 /// <param name="context">Pointer to an application context.
284 /// This parameter can be used for error checking.</param>
285 /// <param name="messageType">Specifies a combination of one message box style,
286 /// one message box icon type, one default button, and one installation message type.</param>
287 /// <param name="message">Specifies the message text.</param>
288 /// <returns>-1 for an error, 0 if no action was taken, 1 if OK, 3 to abort.</returns>
289 private int ValidationUIHandler(IntPtr context, uint messageType, string message)
290 {
291 var continueValidation = true;
292
293 // If we're getting messges during the validation session, log them.
294 // Otherwise, ignore the messages.
295 if (this.ValidationSessionInProgress)
296 {
297 try
298 {
299 var parsedMessage = ParseValidationMessage(message, this.CurrentIce);
300
301 continueValidation = this.Callback.ValidationMessage(parsedMessage);
302 }
303 catch (WixException e)
304 {
305 this.Callback.WriteMessage(e.Error);
306 return -1;
307 }
308 catch (Exception e)
309 {
310 this.Callback.WriteMessage(ErrorMessages.UnexpectedException($"Unexpected exception while executing action: {this.CurrentIce}, detail: {e.Message}", e.GetType().ToString(), e.StackTrace));
311 return -1;
312 }
313 }
314
315 return continueValidation ? 1 : 3;
316 }
317
318 /// <summary>
319 /// Parses a message from the Validator.
320 /// </summary>
321 /// <param name="message">A <see cref="String"/> of tab-delmited tokens
322 /// in the validation message.</param>
323 /// <param name="currentIce">The name of the action to which the message
324 /// belongs.</param>
325 /// <exception cref="ArgumentNullException">The message cannot be null.
326 /// </exception>
327 /// <exception cref="WixException">The message does not contain four (4)
328 /// or more tab-delimited tokens.</exception>
329 /// <remarks>
330 /// <para><paramref name="message"/> a tab-delimited set of tokens,
331 /// formatted according to Windows Installer guidelines for ICE
332 /// message. The following table lists what each token by index
333 /// should mean.</para>
334 /// <para><paramref name="currentIce"/> a name that represents the ICE
335 /// action that was executed (e.g. 'ICE08').</para>
336 /// <list type="table">
337 /// <listheader>
338 /// <term>Index</term>
339 /// <description>Description</description>
340 /// </listheader>
341 /// <item>
342 /// <term>0</term>
343 /// <description>Name of the ICE.</description>
344 /// </item>
345 /// <item>
346 /// <term>1</term>
347 /// <description>Message type. See the following list.</description>
348 /// </item>
349 /// <item>
350 /// <term>2</term>
351 /// <description>Detailed description.</description>
352 /// </item>
353 /// <item>
354 /// <term>3</term>
355 /// <description>Help URL or location.</description>
356 /// </item>
357 /// <item>
358 /// <term>4</term>
359 /// <description>Table name.</description>
360 /// </item>
361 /// <item>
362 /// <term>5</term>
363 /// <description>Column name.</description>
364 /// </item>
365 /// <item>
366 /// <term>6</term>
367 /// <description>This and remaining fields are primary keys
368 /// to identify a row.</description>
369 /// </item>
370 /// </list>
371 /// <para>The message types are one of the following value.</para>
372 /// <list type="table">
373 /// <listheader>
374 /// <term>Value</term>
375 /// <description>Message Type</description>
376 /// </listheader>
377 /// <item>
378 /// <term>0</term>
379 /// <description>Failure message reporting the failure of the
380 /// ICE custom action.</description>
381 /// </item>
382 /// <item>
383 /// <term>1</term>
384 /// <description>Error message reporting database authoring that
385 /// case incorrect behavior.</description>
386 /// </item>
387 /// <item>
388 /// <term>2</term>
389 /// <description>Warning message reporting database authoring that
390 /// causes incorrect behavior in certain cases. Warnings can also
391 /// report unexpected side-effects of database authoring.
392 /// </description>
393 /// </item>
394 /// <item>
395 /// <term>3</term>
396 /// <description>Informational message.</description>
397 /// </item>
398 /// </list>
399 /// </remarks>
400 private static ValidationMessage ParseValidationMessage(string message, string currentIce)
401 {
402 if (message == null)
403 {
404 throw new ArgumentNullException(nameof(message));
405 }
406
407 var messageParts = message.Split('\t');
408 if (messageParts.Length < 3)
409 {
410 if (null == currentIce)
411 {
412 throw new WixException(ErrorMessages.UnexpectedExternalUIMessage(message));
413 }
414 else
415 {
416 throw new WixException(ErrorMessages.UnexpectedExternalUIMessage(message, currentIce));
417 }
418 }
419
420 var type = ParseValidationMessageType(messageParts[1]);
421
422 return new ValidationMessage
423 {
424 IceName = messageParts[0],
425 Type = type,
426 Description = messageParts[2],
427 HelpUrl = messageParts.Length > 3 ? messageParts[3] : null,
428 Table = messageParts.Length > 4 ? messageParts[4] : null,
429 Column = messageParts.Length > 5 ? messageParts[4] : null,
430 PrimaryKeys = messageParts.Length > 6 ? messageParts.Skip(6).ToArray() : null
431 };
432 }
433
434 private static ValidationMessageType ParseValidationMessageType(string type)
435 {
436 switch (type)
437 {
438 case "0":
439 return ValidationMessageType.InternalFailure;
440 case "1":
441 return ValidationMessageType.Error;
442 case "2":
443 return ValidationMessageType.Warning;
444 case "3":
445 return ValidationMessageType.Info;
446 default:
447 throw new WixException(ErrorMessages.InvalidValidatorMessageType(type));
448 }
449 }
450 }
451 }