aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorRob Mensching <rob@firegiant.com>2021-03-16 10:48:29 -0700
committerRob Mensching <rob@firegiant.com>2021-03-16 11:01:46 -0700
commitecf0f8e0a3038e65d18cb3ace71b845af27407ae (patch)
tree1c4d337e7d9790e52f47fe42b80e5a1577498e1d
parent089a08fd6b9398b0e1040f96b8e24ba81acfe05b (diff)
downloadwix-ecf0f8e0a3038e65d18cb3ace71b845af27407ae.tar.gz
wix-ecf0f8e0a3038e65d18cb3ace71b845af27407ae.tar.bz2
wix-ecf0f8e0a3038e65d18cb3ace71b845af27407ae.zip
Implement validation and fix abandoned validation mutex
Fixes wixtoolset/issues#5946 Fixes wixtoolset/issues#6366
-rw-r--r--src/WixToolset.Core.Native/IWindowsInstallerValidatorCallback.cs27
-rw-r--r--src/WixToolset.Core.Native/ValidationMessage.cs47
-rw-r--r--src/WixToolset.Core.Native/ValidationMessageType.cs31
-rw-r--r--src/WixToolset.Core.Native/WindowsInstallerValidator.cs423
-rw-r--r--src/WixToolset.Core.Native/WixToolset.Core.Native.csproj7
-rw-r--r--src/WixToolset.Core.Native/WixToolset.Core.Native.nuspec3
-rw-r--r--src/WixToolset.Core.Native/cubes/darice.cubbin0 -> 684032 bytes
-rw-r--r--src/WixToolset.Core.Native/cubes/mergemod.cubbin0 -> 483328 bytes
8 files changed, 537 insertions, 1 deletions
diff --git a/src/WixToolset.Core.Native/IWindowsInstallerValidatorCallback.cs b/src/WixToolset.Core.Native/IWindowsInstallerValidatorCallback.cs
new file mode 100644
index 00000000..f4aff134
--- /dev/null
+++ b/src/WixToolset.Core.Native/IWindowsInstallerValidatorCallback.cs
@@ -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
3namespace 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}
diff --git a/src/WixToolset.Core.Native/ValidationMessage.cs b/src/WixToolset.Core.Native/ValidationMessage.cs
new file mode 100644
index 00000000..d7137326
--- /dev/null
+++ b/src/WixToolset.Core.Native/ValidationMessage.cs
@@ -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
3namespace 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}
diff --git a/src/WixToolset.Core.Native/ValidationMessageType.cs b/src/WixToolset.Core.Native/ValidationMessageType.cs
new file mode 100644
index 00000000..98635294
--- /dev/null
+++ b/src/WixToolset.Core.Native/ValidationMessageType.cs
@@ -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
3namespace 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}
diff --git a/src/WixToolset.Core.Native/WindowsInstallerValidator.cs b/src/WixToolset.Core.Native/WindowsInstallerValidator.cs
new file mode 100644
index 00000000..d013e5f9
--- /dev/null
+++ b/src/WixToolset.Core.Native/WindowsInstallerValidator.cs
@@ -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
3namespace 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}
diff --git a/src/WixToolset.Core.Native/WixToolset.Core.Native.csproj b/src/WixToolset.Core.Native/WixToolset.Core.Native.csproj
index 41e75f99..4069b6b4 100644
--- a/src/WixToolset.Core.Native/WixToolset.Core.Native.csproj
+++ b/src/WixToolset.Core.Native/WixToolset.Core.Native.csproj
@@ -11,6 +11,11 @@
11 <CreateDocumentationFile>true</CreateDocumentationFile> 11 <CreateDocumentationFile>true</CreateDocumentationFile>
12 </PropertyGroup> 12 </PropertyGroup>
13 13
14 <ItemGroup>
15 <None Include="cubes\darice.cub" CopyToOutputDirectory="PreserveNewest" />
16 <None Include="cubes\mergemod.cub" CopyToOutputDirectory="PreserveNewest" />
17 </ItemGroup>
18
14 <ItemGroup Condition=" '$(NCrunch)'=='' "> 19 <ItemGroup Condition=" '$(NCrunch)'=='' ">
15 <ProjectReference Include="..\wixnative\wixnative.vcxproj" ReferenceOutputAssembly="false" PrivateAssets="All" Properties="Platform=ARM64" /> 20 <ProjectReference Include="..\wixnative\wixnative.vcxproj" ReferenceOutputAssembly="false" PrivateAssets="All" Properties="Platform=ARM64" />
16 <ProjectReference Include="..\wixnative\wixnative.vcxproj" ReferenceOutputAssembly="false" PrivateAssets="All" Properties="Platform=Win32" /> 21 <ProjectReference Include="..\wixnative\wixnative.vcxproj" ReferenceOutputAssembly="false" PrivateAssets="All" Properties="Platform=Win32" />
@@ -29,7 +34,7 @@
29 </ItemGroup> 34 </ItemGroup>
30 35
31 <ItemGroup> 36 <ItemGroup>
32 <PackageReference Include="WixToolset.Data" Version="4.0.*" PrivateAssets="all" /> 37 <PackageReference Include="WixToolset.Data" Version="4.0.*" />
33 </ItemGroup> 38 </ItemGroup>
34 39
35 <ItemGroup> 40 <ItemGroup>
diff --git a/src/WixToolset.Core.Native/WixToolset.Core.Native.nuspec b/src/WixToolset.Core.Native/WixToolset.Core.Native.nuspec
index b6fd9790..cbc4f1be 100644
--- a/src/WixToolset.Core.Native/WixToolset.Core.Native.nuspec
+++ b/src/WixToolset.Core.Native/WixToolset.Core.Native.nuspec
@@ -20,6 +20,9 @@
20 <file src="netstandard2.0\$id$.dll" target="lib\netstandard2.0" /> 20 <file src="netstandard2.0\$id$.dll" target="lib\netstandard2.0" />
21 <file src="netstandard2.0\$id$.xml" target="lib\netstandard2.0" /> 21 <file src="netstandard2.0\$id$.xml" target="lib\netstandard2.0" />
22 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
23 <file src="ARM64\mergemod.dll" target="runtimes\win-arm64\native" /> 26 <file src="ARM64\mergemod.dll" target="runtimes\win-arm64\native" />
24 <file src="ARM64\wixnative.exe" target="runtimes\win-arm64\native" /> 27 <file src="ARM64\wixnative.exe" target="runtimes\win-arm64\native" />
25 <file src="ARM64\wixnative.pdb" target="runtimes\win-arm64\native" /> 28 <file src="ARM64\wixnative.pdb" target="runtimes\win-arm64\native" />
diff --git a/src/WixToolset.Core.Native/cubes/darice.cub b/src/WixToolset.Core.Native/cubes/darice.cub
new file mode 100644
index 00000000..4292fede
--- /dev/null
+++ b/src/WixToolset.Core.Native/cubes/darice.cub
Binary files differ
diff --git a/src/WixToolset.Core.Native/cubes/mergemod.cub b/src/WixToolset.Core.Native/cubes/mergemod.cub
new file mode 100644
index 00000000..def6dd1a
--- /dev/null
+++ b/src/WixToolset.Core.Native/cubes/mergemod.cub
Binary files differ