From ecf0f8e0a3038e65d18cb3ace71b845af27407ae Mon Sep 17 00:00:00 2001 From: Rob Mensching Date: Tue, 16 Mar 2021 10:48:29 -0700 Subject: Implement validation and fix abandoned validation mutex Fixes wixtoolset/issues#5946 Fixes wixtoolset/issues#6366 --- .../IWindowsInstallerValidatorCallback.cs | 27 ++ src/WixToolset.Core.Native/ValidationMessage.cs | 47 +++ .../ValidationMessageType.cs | 31 ++ .../WindowsInstallerValidator.cs | 423 +++++++++++++++++++++ .../WixToolset.Core.Native.csproj | 7 +- .../WixToolset.Core.Native.nuspec | 3 + src/WixToolset.Core.Native/cubes/darice.cub | Bin 0 -> 684032 bytes src/WixToolset.Core.Native/cubes/mergemod.cub | Bin 0 -> 483328 bytes 8 files changed, 537 insertions(+), 1 deletion(-) create mode 100644 src/WixToolset.Core.Native/IWindowsInstallerValidatorCallback.cs create mode 100644 src/WixToolset.Core.Native/ValidationMessage.cs create mode 100644 src/WixToolset.Core.Native/ValidationMessageType.cs create mode 100644 src/WixToolset.Core.Native/WindowsInstallerValidator.cs create mode 100644 src/WixToolset.Core.Native/cubes/darice.cub create mode 100644 src/WixToolset.Core.Native/cubes/mergemod.cub (limited to 'src') 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 @@ +// 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. + +namespace WixToolset.Core.Native +{ + /// + /// Callbacks during validation. + /// + public interface IWindowsInstallerValidatorCallback + { + /// + /// Indicates if the validator callback encountered an error. + /// + bool EncounteredError { get; } + + /// + /// Validation blocked by another Windows Installer operation. + /// + void ValidationBlocked(); + + /// + /// Validation message from an ICE. + /// + /// The validation message. + /// True if validation should continue; otherwise cancel the validation. + bool ValidationMessage(ValidationMessage message); + } +} 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 @@ +// 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. + +namespace WixToolset.Core.Native +{ + using System.Collections.Generic; + + /// + /// Message from ICE + /// + public class ValidationMessage + { + /// + /// Name of the ICE providing the message. + /// + public string IceName { get; set; } + + /// + /// Validation type. + /// + public ValidationMessageType Type { get; set; } + + /// + /// Message text. + /// + public string Description { get; set; } + + /// + /// Optional help URL for the message. + /// + public string HelpUrl { get; set; } + + /// + /// Optional table causing the message. + /// + public string Table { get; set; } + + /// + /// Optional column causing the message. + /// + public string Column { get; set; } + + /// + /// Optional primary keys causing the message. + /// + public IEnumerable PrimaryKeys { get; set; } + } +} 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 @@ +// 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. + +namespace WixToolset.Core.Native +{ + /// + /// Validation message type. + /// + public enum ValidationMessageType + { + /// + /// Failure message reporting the failure of the ICE custom action. + /// + InternalFailure = 0, + + /// + /// Error message reporting database authoring that case incorrect behavior. + /// + Error = 1, + + /// + /// Warning message reporting database authoring that causes incorrect behavior in certain cases. + /// Warnings can also report unexpected side-effects of database authoring. + /// + Warning = 2, + + /// + /// Informational message. + /// + Info = 3, + }; +} 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 @@ +// 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. + +namespace WixToolset.Core.Native +{ + using System; + using System.Collections.Generic; + using System.ComponentModel; + using System.IO; + using System.Linq; + using System.Threading; + using WixToolset.Core.Native.Msi; + using WixToolset.Data; + + /// + /// Windows installer validation implementation. + /// + public class WindowsInstallerValidator + { + private const string CubesFolder = "cubes"; + + /// + /// Creates a new Windows Installer validator. + /// + /// Callback interface to handle messages. + /// Database to validate. + /// Set of CUBe files to merge. + /// ICEs to execute. + /// Suppressed ICEs. + public WindowsInstallerValidator(IWindowsInstallerValidatorCallback callback, string databasePath, IEnumerable cubeFiles, IEnumerable ices, IEnumerable suppressedIces) + { + this.Callback = callback; + this.DatabasePath = databasePath; + this.CubeFiles = cubeFiles; + this.Ices = new SortedSet(ices); + this.SuppressedIces = new SortedSet(suppressedIces); + } + + private IWindowsInstallerValidatorCallback Callback { get; } + + private string DatabasePath { get; } + + private IEnumerable CubeFiles { get; } + + private SortedSet Ices { get; } + + private SortedSet SuppressedIces { get; } + + private bool ValidationSessionInProgress { get; set; } + + private string CurrentIce { get; set; } + + /// + /// Execute the validations. + /// + public void Execute() + { + using (var mutex = new Mutex(false, "WixValidator")) + { + try + { + if (!mutex.WaitOne(0)) + { + this.Callback.ValidationBlocked(); + mutex.WaitOne(); + } + } + catch (AbandonedMutexException) + { + // Another validation process was probably killed, we own the mutex now. + } + + try + { + this.RunValidations(); + } + finally + { + mutex.ReleaseMutex(); + } + } + } + + private void RunValidations() + { + var previousUILevel = (int)InstallUILevels.Basic; + var previousHwnd = IntPtr.Zero; + InstallUIHandler previousUIHandler = null; + + var baseCubePath = Path.Combine(Path.GetDirectoryName(typeof(WindowsInstallerValidator).Assembly.Location), CubesFolder); + var cubeFiles = this.CubeFiles.Select(s => Path.Combine(baseCubePath, s)).ToList(); + + try + { + using (var database = new Database(this.DatabasePath, OpenDatabase.Direct)) + { + var propertyTableExists = database.TableExists("Property"); + string productCode = null; + + // Remove the product code from the database before opening a session to prevent opening an installed product. + if (propertyTableExists) + { + using (var view = database.OpenExecuteView("SELECT `Value` FROM `Property` WHERE Property = 'ProductCode'")) + { + using (var record = view.Fetch()) + { + if (null != record) + { + productCode = record.GetString(1); + + using (var dropProductCodeView = database.OpenExecuteView("DELETE FROM `Property` WHERE `Property` = 'ProductCode'")) + { + } + } + } + } + } + + // Merge in the cube databases. + foreach (var cubeFile in cubeFiles) + { + try + { + using (var cubeDatabase = new Database(cubeFile, OpenDatabase.ReadOnly)) + { + try + { + database.Merge(cubeDatabase, "MergeConflicts"); + } + catch + { + // ignore merge errors since they are expected in the _Validation table + } + } + } + catch (Win32Exception e) + { + if (0x6E == e.NativeErrorCode) // ERROR_OPEN_FAILED + { + throw new WixException(ErrorMessages.CubeFileNotFound(cubeFile)); + } + + throw; + } + } + + // Commit the database before proceeding to ensure the streams don't get confused. + database.Commit(); + + // The property table may have been added to the database from a cub database without the proper validation rows. + if (!propertyTableExists) + { + using (var view = database.OpenExecuteView("DROP table `Property`")) + { + } + } + + // Get all the action names for ICEs which have not been suppressed. + var actions = new List(); + using (var view = database.OpenExecuteView("SELECT `Action` FROM `_ICESequence` ORDER BY `Sequence`")) + { + foreach (var record in view.Records) + { + var action = record.GetString(1); + + if (!this.SuppressedIces.Contains(action) && this.Ices.Contains(action)) + { + actions.Add(action); + } + } + } + + // Disable the internal UI handler and set an external UI handler. + previousUILevel = Installer.SetInternalUI((int)InstallUILevels.None, ref previousHwnd); + previousUIHandler = Installer.SetExternalUI(this.ValidationUIHandler, (int)InstallLogModes.Error | (int)InstallLogModes.Warning | (int)InstallLogModes.User, IntPtr.Zero); + + // Create a session for running the ICEs. + this.ValidationSessionInProgress = true; + + using (var session = new Session(database)) + { + // Add the product code back into the database. + if (null != productCode) + { + // Some CUBs erroneously have a ProductCode property, so delete it if we just picked one up. + using (var dropProductCodeView = database.OpenExecuteView("DELETE FROM `Property` WHERE `Property` = 'ProductCode'")) + { + } + + using (var view = database.OpenExecuteView($"INSERT INTO `Property` (`Property`, `Value`) VALUES ('ProductCode', '{productCode}')")) + { + } + } + + foreach (var action in actions) + { + this.CurrentIce = action; + + try + { + session.DoAction(action); + } + catch (Win32Exception e) + { + if (!this.Callback.EncounteredError) + { + throw e; + } + } + + this.CurrentIce = null; + } + + // Mark the validation session complete so we ignore any messages that MSI may fire + // during session clean-up. + this.ValidationSessionInProgress = false; + } + } + } + catch (Win32Exception e) + { + // Avoid displaying errors twice since one may have already occurred in the UI handler. + if (!this.Callback.EncounteredError) + { + if (0x6E == e.NativeErrorCode) // ERROR_OPEN_FAILED + { + // The database path is not passed to this exception since inside wix.exe + // this would be the temporary copy and there would be no final output becasue + // this error occured; and during standalone validation they should know the path + // passed in. + throw new WixException(ErrorMessages.ValidationFailedToOpenDatabase()); + } + else if (0x64D == e.NativeErrorCode) + { + throw new WixException(ErrorMessages.ValidationFailedDueToLowMsiEngine()); + } + else if (0x654 == e.NativeErrorCode) + { + throw new WixException(ErrorMessages.ValidationFailedDueToInvalidPackage()); + } + else if (0x658 == e.NativeErrorCode) + { + throw new WixException(ErrorMessages.ValidationFailedDueToMultilanguageMergeModule()); + } + else if (0x659 == e.NativeErrorCode) + { + throw new WixException(WarningMessages.ValidationFailedDueToSystemPolicy()); + } + else + { + var msg = String.IsNullOrEmpty(this.CurrentIce) ? e.Message : $"Action - '{this.CurrentIce}' {e.Message}"; + + throw new WixException(ErrorMessages.Win32Exception(e.NativeErrorCode, msg)); + } + } + } + finally + { + this.ValidationSessionInProgress = false; + + Installer.SetExternalUI(previousUIHandler, 0, IntPtr.Zero); + Installer.SetInternalUI(previousUILevel, ref previousHwnd); + } + } + + /// + /// The validation external UI handler. + /// + /// Pointer to an application context. + /// This parameter can be used for error checking. + /// Specifies a combination of one message box style, + /// one message box icon type, one default button, and one installation message type. + /// Specifies the message text. + /// -1 for an error, 0 if no action was taken, 1 if OK, 3 to abort. + private int ValidationUIHandler(IntPtr context, uint messageType, string message) + { + var continueValidation = true; + + // If we're getting messges during the validation session, log them. + // Otherwise, ignore the messages. + if (!this.ValidationSessionInProgress) + { + var parsedMessage = ParseValidationMessage(message, this.CurrentIce); + + continueValidation = this.Callback.ValidationMessage(parsedMessage); + } + + return continueValidation ? 1 : 3; + } + + /// + /// Parses a message from the Validator. + /// + /// A of tab-delmited tokens + /// in the validation message. + /// The name of the action to which the message + /// belongs. + /// The message cannot be null. + /// + /// The message does not contain four (4) + /// or more tab-delimited tokens. + /// + /// a tab-delimited set of tokens, + /// formatted according to Windows Installer guidelines for ICE + /// message. The following table lists what each token by index + /// should mean. + /// a name that represents the ICE + /// action that was executed (e.g. 'ICE08'). + /// + /// + /// Index + /// Description + /// + /// + /// 0 + /// Name of the ICE. + /// + /// + /// 1 + /// Message type. See the following list. + /// + /// + /// 2 + /// Detailed description. + /// + /// + /// 3 + /// Help URL or location. + /// + /// + /// 4 + /// Table name. + /// + /// + /// 5 + /// Column name. + /// + /// + /// 6 + /// This and remaining fields are primary keys + /// to identify a row. + /// + /// + /// The message types are one of the following value. + /// + /// + /// Value + /// Message Type + /// + /// + /// 0 + /// Failure message reporting the failure of the + /// ICE custom action. + /// + /// + /// 1 + /// Error message reporting database authoring that + /// case incorrect behavior. + /// + /// + /// 2 + /// Warning message reporting database authoring that + /// causes incorrect behavior in certain cases. Warnings can also + /// report unexpected side-effects of database authoring. + /// + /// + /// + /// 3 + /// Informational message. + /// + /// + /// + private static ValidationMessage ParseValidationMessage(string message, string currentIce) + { + if (message == null) + { + throw new ArgumentNullException(nameof(message)); + } + + var messageParts = message.Split('\t'); + if (messageParts.Length < 3) + { + if (null == currentIce) + { + throw new WixException(ErrorMessages.UnexpectedExternalUIMessage(message)); + } + else + { + throw new WixException(ErrorMessages.UnexpectedExternalUIMessage(message, currentIce)); + } + } + + var type = ParseValidationMessageType(messageParts[1]); + + return new ValidationMessage + { + IceName = messageParts[0], + Type = type, + Description = messageParts[2], + HelpUrl = messageParts.Length > 3 ? messageParts[3] : null, + Table = messageParts.Length > 4 ? messageParts[4] : null, + Column = messageParts.Length > 5 ? messageParts[4] : null, + PrimaryKeys = messageParts.Length > 6 ? messageParts.Skip(6).ToArray() : null + }; + } + + private static ValidationMessageType ParseValidationMessageType(string type) + { + switch (type) + { + case "0": + return ValidationMessageType.InternalFailure; + case "1": + return ValidationMessageType.Error; + case "2": + return ValidationMessageType.Warning; + case "3": + return ValidationMessageType.Info; + default: + throw new WixException(ErrorMessages.InvalidValidatorMessageType(type)); + } + } + } +} 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 @@ true + + + + + @@ -29,7 +34,7 @@ - + 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 @@ + + + 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 Binary files /dev/null and b/src/WixToolset.Core.Native/cubes/darice.cub 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 Binary files /dev/null and b/src/WixToolset.Core.Native/cubes/mergemod.cub differ -- cgit v1.2.3-55-g6feb