// 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.Linq; using System.Reflection; using System.Threading; using WixToolset.Core.Native; using WixToolset.Data; using WixToolset.Data.WindowsInstaller; using WixToolset.Extensibility; using WixToolset.Extensibility.Services; using WixToolset.Msi; /// /// Runs internal consistency evaluators (ICEs) from cub files against a database. /// public sealed class Validator { private string actionName; private StringCollection cubeFiles; private ValidatorExtension extension; private Output output; private InstallUIHandler validationUIHandler; private bool validationSessionComplete; private readonly IMessaging messaging; /// /// Instantiate a new Validator. /// public Validator(IMessaging messaging) { this.cubeFiles = new StringCollection(); this.extension = new ValidatorExtension(messaging); this.validationUIHandler = new InstallUIHandler(this.ValidationUIHandler); this.messaging = messaging; } /// /// 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.messaging.Write(VerboseMessages.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(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 (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 (!this.messaging.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 (!this.messaging.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.messaging.Write(ErrorMessages.ValidationFailedToOpenDatabase()); } else if (0x64D == e.NativeErrorCode) { this.messaging.Write(ErrorMessages.ValidationFailedDueToLowMsiEngine()); } else if (0x654 == e.NativeErrorCode) { this.messaging.Write(ErrorMessages.ValidationFailedDueToInvalidPackage()); } else if (0x658 == e.NativeErrorCode) { this.messaging.Write(ErrorMessages.ValidationFailedDueToMultilanguageMergeModule()); } else if (0x659 == e.NativeErrorCode) { this.messaging.Write(WarningMessages.ValidationFailedDueToSystemPolicy()); } else { string msgTemp = e.Message; if (null != this.actionName) { msgTemp = String.Concat("Action - '", this.actionName, "' ", e.Message); } this.messaging.Write(ErrorMessages.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(); } } 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(context.Messaging); 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.messaging.Write(ex.Error); } return 1; } } }