@joebigelow / wix-1 / commits / ecf0f8e0

Implement validation and fix abandoned validation mutex

Fixes wixtoolset/issues#5946 Fixes wixtoolset/issues#6366

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