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