From dbde9e7104b907bbbaea17e21247d8cafc8b3a4c Mon Sep 17 00:00:00 2001 From: Rob Mensching Date: Sat, 14 Oct 2017 16:12:07 -0700 Subject: Massive refactoring to introduce the concept of IBackend --- src/WixToolset.Core.WindowsInstaller/Validator.cs | 385 ++++++++++++++++++++++ 1 file changed, 385 insertions(+) create mode 100644 src/WixToolset.Core.WindowsInstaller/Validator.cs (limited to 'src/WixToolset.Core.WindowsInstaller/Validator.cs') diff --git a/src/WixToolset.Core.WindowsInstaller/Validator.cs b/src/WixToolset.Core.WindowsInstaller/Validator.cs new file mode 100644 index 00000000..db66f600 --- /dev/null +++ b/src/WixToolset.Core.WindowsInstaller/Validator.cs @@ -0,0 +1,385 @@ +// 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.WindowsInstaller +{ + using System; + using System.Collections.Generic; + using System.Collections.Specialized; + using System.ComponentModel; + using System.Diagnostics.CodeAnalysis; + using System.Globalization; + using System.IO; + using System.Threading; + using WixToolset.Data; + using WixToolset.Extensibility; + using WixToolset.Core.Native; + using WixToolset.Msi; + using System.Linq; + using System.Reflection; + + /// + /// Runs internal consistency evaluators (ICEs) from cub files against a database. + /// + public sealed class Validator : IMessageHandler + { + private string actionName; + private StringCollection cubeFiles; + private ValidatorExtension extension; + private Output output; + private InstallUIHandler validationUIHandler; + private bool validationSessionComplete; + + /// + /// Instantiate a new Validator. + /// + public Validator() + { + this.cubeFiles = new StringCollection(); + this.extension = new ValidatorExtension(); + this.validationUIHandler = new InstallUIHandler(this.ValidationUIHandler); + } + + /// + /// Gets or sets a that directs messages from the validator. + /// + /// A that directs messages from the validator. + public ValidatorExtension Extension + { + get { return this.extension; } + set { this.extension = value; } + } + + /// + /// Gets or sets the list of ICEs to run. + /// + /// The list of ICEs. + [SuppressMessage("Microsoft.Performance", "CA1819:PropertiesShouldNotReturnArrays")] + public ISet ICEs { get; set; } + + /// + /// Gets or sets the output used for finding source line information. + /// + /// The output used for finding source line information. + public Output Output + { + // cache Output object until validation for changes in extension + get { return this.output; } + set { this.output = value; } + } + + /// + /// Gets or sets the suppressed ICEs. + /// + /// The suppressed ICEs. + [SuppressMessage("Microsoft.Performance", "CA1819:PropertiesShouldNotReturnArrays")] + public ISet SuppressedICEs { get; set; } + + /// + /// Sets the temporary path for the Binder. + /// + public string IntermediateFolder { private get; set; } + + /// + /// Add a cube file to the validation run. + /// + /// A cube file. + public void AddCubeFile(string cubeFile) + { + this.cubeFiles.Add(cubeFile); + } + + /// + /// Validate a database. + /// + /// The database to validate. + /// true if validation succeeded; false otherwise. + public void Validate(string databaseFile) + { + int previousUILevel = (int)InstallUILevels.Basic; + IntPtr previousHwnd = IntPtr.Zero; + InstallUIHandler previousUIHandler = null; + + if (null == databaseFile) + { + throw new ArgumentNullException("databaseFile"); + } + + // initialize the validator extension + this.extension.DatabaseFile = databaseFile; + this.extension.Output = this.output; + this.extension.InitializeValidator(); + + // Ensure the temporary files can be created. + Directory.CreateDirectory(this.IntermediateFolder); + + // copy the database to a temporary location so it can be manipulated + string tempDatabaseFile = Path.Combine(this.IntermediateFolder, Path.GetFileName(databaseFile)); + File.Copy(databaseFile, tempDatabaseFile); + + // remove the read-only property from the temporary database + FileAttributes attributes = File.GetAttributes(tempDatabaseFile); + File.SetAttributes(tempDatabaseFile, attributes & ~FileAttributes.ReadOnly); + + Mutex mutex = new Mutex(false, "WixValidator"); + try + { + if (!mutex.WaitOne(0, false)) + { + this.OnMessage(WixVerboses.ValidationSerialized()); + mutex.WaitOne(); + } + + using (Database database = new Database(tempDatabaseFile, OpenDatabase.Direct)) + { + bool 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 (View view = database.OpenExecuteView("SELECT `Value` FROM `Property` WHERE Property = 'ProductCode'")) + { + using (Record record = view.Fetch()) + { + if (null != record) + { + productCode = record.GetString(1); + + using (View dropProductCodeView = database.OpenExecuteView("DELETE FROM `Property` WHERE `Property` = 'ProductCode'")) + { + } + } + } + } + } + + // merge in the cube databases + foreach (string cubeFile in this.cubeFiles) + { + try + { + using (Database 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(WixErrors.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 (View view = database.OpenExecuteView("DROP table `Property`")) + { + } + } + + // get all the action names for ICEs which have not been suppressed + List actions = new List(); + using (View view = database.OpenExecuteView("SELECT `Action` FROM `_ICESequence` ORDER BY `Sequence`")) + { + while (true) + { + using (Record record = view.Fetch()) + { + if (null == record) + { + break; + } + + string action = record.GetString(1); + + if ((this.SuppressedICEs == null || !this.SuppressedICEs.Contains(action)) && (this.ICEs == null || 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.validationSessionComplete = false; + using (Session 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 (View dropProductCodeView = database.OpenExecuteView("DELETE FROM `Property` WHERE `Property` = 'ProductCode'")) + { + } + + using (View view = database.OpenExecuteView(String.Format(CultureInfo.InvariantCulture, "INSERT INTO `Property` (`Property`, `Value`) VALUES ('ProductCode', '{0}')", productCode))) + { + } + } + + foreach (string action in actions) + { + this.actionName = action; + try + { + session.DoAction(action); + } + catch (Win32Exception e) + { + if (!Messaging.Instance.EncounteredError) + { + throw e; + } + // TODO: Review why this was clearing the error state when an exception had happened but an error was already encountered. That's weird. + //else + //{ + // this.encounteredError = false; + //} + } + this.actionName = null; + } + + // Mark the validation session complete so we ignore any messages that MSI may fire + // during session clean-up. + this.validationSessionComplete = true; + } + } + } + catch (Win32Exception e) + { + // avoid displaying errors twice since one may have already occurred in the UI handler + if (!Messaging.Instance.EncounteredError) + { + if (0x6E == e.NativeErrorCode) // ERROR_OPEN_FAILED + { + // databaseFile is not passed since during light + // this would be the temporary copy and there would be + // no final output since the error occured; during smoke + // they should know the path passed into smoke + this.OnMessage(WixErrors.ValidationFailedToOpenDatabase()); + } + else if (0x64D == e.NativeErrorCode) + { + this.OnMessage(WixErrors.ValidationFailedDueToLowMsiEngine()); + } + else if (0x654 == e.NativeErrorCode) + { + this.OnMessage(WixErrors.ValidationFailedDueToInvalidPackage()); + } + else if (0x658 == e.NativeErrorCode) + { + this.OnMessage(WixErrors.ValidationFailedDueToMultilanguageMergeModule()); + } + else if (0x659 == e.NativeErrorCode) + { + this.OnMessage(WixWarnings.ValidationFailedDueToSystemPolicy()); + } + else + { + string msgTemp = e.Message; + + if (null != this.actionName) + { + msgTemp = String.Concat("Action - '", this.actionName, "' ", e.Message); + } + + this.OnMessage(WixErrors.Win32Exception(e.NativeErrorCode, msgTemp)); + } + } + } + finally + { + Installer.SetExternalUI(previousUIHandler, 0, IntPtr.Zero); + Installer.SetInternalUI(previousUILevel, ref previousHwnd); + + this.validationSessionComplete = false; // no validation session at this point, so reset the completion flag. + + mutex.ReleaseMutex(); + this.cubeFiles.Clear(); + this.extension.FinalizeValidator(); + } + } + + /// + /// Sends a message to the message delegate if there is one. + /// + /// Message event arguments. + public void OnMessage(MessageEventArgs e) + { + Messaging.Instance.OnMessage(e); + this.extension.OnMessage(e); + } + + public static Validator CreateFromContext(IBindContext context, string cubeFilename) + { + Validator validator = null; + + // Tell the binder about the validator if validation isn't suppressed + if (!context.SuppressValidation) + { + validator = new Validator(); + validator.IntermediateFolder = Path.Combine(context.IntermediateFolder, "validate"); + + // set the default cube file + string thisPath = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location); + validator.AddCubeFile(Path.Combine(thisPath, cubeFilename)); + + // Set the ICEs + validator.ICEs = new SortedSet(context.Ices); + + // Set the suppressed ICEs and disable ICEs that have equivalent-or-better checks in WiX. + validator.SuppressedICEs = new SortedSet(context.SuppressIces.Union(new[] { "ICE08", "ICE33", "ICE47", "ICE66" })); + } + + return validator; + } + + /// + /// 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) + { + try + { + // If we're getting messges during the validation session, send them to + // the extension. Otherwise, ignore the messages. + if (!this.validationSessionComplete) + { + this.extension.Log(message, this.actionName); + } + } + catch (WixException ex) + { + this.OnMessage(ex.Error); + } + + return 1; + } + } +} -- cgit v1.2.3-55-g6feb