// 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
{
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.Msi;
using WixToolset.Core.Native;
///
/// 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 string[] ices;
private Output output;
private string[] suppressedICEs;
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 string[] ICEs
{
get { return this.ices; }
set { this.ices = value; }
}
///
/// 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 string[] SuppressedICEs
{
get { return this.suppressedICEs; }
set { this.suppressedICEs = value; }
}
///
/// Sets the temporary path for the Binder.
///
public string TempFilesLocation { 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)
{
Dictionary indexedICEs = new Dictionary();
Dictionary indexedSuppressedICEs = new Dictionary();
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.TempFilesLocation);
// index the ICEs
if (null != this.ices)
{
foreach (string ice in this.ices)
{
indexedICEs[ice] = null;
}
}
// index the suppressed ICEs
if (null != this.suppressedICEs)
{
foreach (string suppressedICE in this.suppressedICEs)
{
indexedSuppressedICEs[suppressedICE] = null;
}
}
// copy the database to a temporary location so it can be manipulated
string tempDatabaseFile = Path.Combine(this.TempFilesLocation, 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 (!indexedSuppressedICEs.ContainsKey(action))
{
actions.Add(action);
}
}
}
}
if (0 != indexedICEs.Count)
{
// Walk backwards and remove those that arent in the list
for (int i = actions.Count - 1; 0 <= i; i--)
{
if (!indexedICEs.ContainsKey(actions[i]))
{
actions.RemoveAt(i);
}
}
}
// 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);
}
///
/// 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;
}
}
}