// 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.Dtf.WindowsInstaller { using System; using System.IO; using System.Text; using System.Collections.Generic; using System.Globalization; using System.Runtime.Serialization; using System.Diagnostics.CodeAnalysis; /// /// Base class for Windows Installer exceptions. /// [Serializable] public class InstallerException : SystemException { private int errorCode; private object[] errorData; /// /// Creates a new InstallerException with a specified error message and a reference to the /// inner exception that is the cause of this exception. /// /// The message that describes the error. /// The exception that is the cause of the current exception. If the /// innerException parameter is not a null reference (Nothing in Visual Basic), the current exception /// is raised in a catch block that handles the inner exception. public InstallerException(string msg, Exception innerException) : this(0, msg, innerException) { } /// /// Creates a new InstallerException with a specified error message. /// /// The message that describes the error. public InstallerException(string msg) : this(0, msg) { } /// /// Creates a new InstallerException. /// public InstallerException() : this(0, null) { } internal InstallerException(int errorCode, string msg, Exception innerException) : base(msg, innerException) { this.errorCode = errorCode; this.SaveErrorRecord(); } internal InstallerException(int errorCode, string msg) : this(errorCode, msg, null) { } /// /// Initializes a new instance of the InstallerException class with serialized data. /// /// The SerializationInfo that holds the serialized object data about the exception being thrown. /// The StreamingContext that contains contextual information about the source or destination. protected InstallerException(SerializationInfo info, StreamingContext context) : base(info, context) { if (info == null) { throw new ArgumentNullException("info"); } this.errorCode = info.GetInt32("msiErrorCode"); } /// /// Gets the system error code that resulted in this exception, or 0 if not applicable. /// public int ErrorCode { get { return this.errorCode; } } /// /// Gets a message that describes the exception. This message may contain detailed /// formatted error data if it was available. /// public override String Message { get { string msg = base.Message; using (Record errorRec = this.GetErrorRecord()) { if (errorRec != null) { string errorMsg = Installer.GetErrorMessage(errorRec, CultureInfo.InvariantCulture); msg = Combine(msg, errorMsg); } } return msg; } } /// /// Sets the SerializationInfo with information about the exception. /// /// The SerializationInfo that holds the serialized object data about the exception being thrown. /// The StreamingContext that contains contextual information about the source or destination. public override void GetObjectData(SerializationInfo info, StreamingContext context) { if (info == null) { throw new ArgumentNullException("info"); } info.AddValue("msiErrorCode", this.errorCode); base.GetObjectData(info, context); } /// /// Gets extended information about the error, or null if no further information /// is available. /// /// A Record object. Field 1 of the Record contains the installer /// message code. Other fields contain data specific to the particular error. ///

/// If the record is passed to , it is formatted /// by looking up the string in the current database. If there is no installation /// session, the formatted error message may be obtained by a query on the Error table using /// the error code, followed by a call to . /// Alternatively, the standard MSI message can by retrieved by calling the /// method. ///

/// The following methods and properties may report extended error data: /// /// (constructor) /// . /// . /// . /// . /// . /// . /// . /// . /// . /// . /// . /// . /// . /// . /// . /// . /// . /// . /// . /// . /// . /// . /// . /// . /// . /// . /// . /// . /// . /// . /// (constructor) /// . /// . /// . /// . /// . /// . /// . /// . /// . /// ///

/// The Record object should be d after use. /// It is best that the handle be closed manually as soon as it is no longer /// needed, as leaving lots of unused handles open can degrade performance. ///

/// Win32 MSI API: /// MsiGetLastErrorRecord ///

[SuppressMessage("Microsoft.Design", "CA1024:UsePropertiesWhereAppropriate")] public Record GetErrorRecord() { return this.errorData != null ? new Record(this.errorData) : null; } internal static Exception ExceptionFromReturnCode(uint errorCode) { return ExceptionFromReturnCode(errorCode, null); } internal static Exception ExceptionFromReturnCode(uint errorCode, string msg) { msg = Combine(GetSystemMessage(errorCode), msg); switch (errorCode) { case (uint) NativeMethods.Error.FILE_NOT_FOUND: case (uint) NativeMethods.Error.PATH_NOT_FOUND: return new FileNotFoundException(msg); case (uint) NativeMethods.Error.INVALID_PARAMETER: case (uint) NativeMethods.Error.DIRECTORY: case (uint) NativeMethods.Error.UNKNOWN_PROPERTY: case (uint) NativeMethods.Error.UNKNOWN_PRODUCT: case (uint) NativeMethods.Error.UNKNOWN_FEATURE: case (uint) NativeMethods.Error.UNKNOWN_COMPONENT: return new ArgumentException(msg); case (uint) NativeMethods.Error.BAD_QUERY_SYNTAX: return new BadQuerySyntaxException(msg); case (uint) NativeMethods.Error.INVALID_HANDLE_STATE: case (uint) NativeMethods.Error.INVALID_HANDLE: InvalidHandleException ihex = new InvalidHandleException(msg); ihex.errorCode = (int) errorCode; return ihex; case (uint) NativeMethods.Error.INSTALL_USEREXIT: return new InstallCanceledException(msg); case (uint) NativeMethods.Error.CALL_NOT_IMPLEMENTED: return new NotImplementedException(msg); default: return new InstallerException((int) errorCode, msg); } } internal static string GetSystemMessage(uint errorCode) { const uint FORMAT_MESSAGE_IGNORE_INSERTS = 0x00000200; const uint FORMAT_MESSAGE_FROM_SYSTEM = 0x00001000; StringBuilder buf = new StringBuilder(1024); uint formatCount = NativeMethods.FormatMessage( FORMAT_MESSAGE_FROM_SYSTEM | FORMAT_MESSAGE_IGNORE_INSERTS, IntPtr.Zero, (uint) errorCode, 0, buf, (uint) buf.Capacity, IntPtr.Zero); if (formatCount != 0) { return buf.ToString().Trim(); } else { return null; } } internal void SaveErrorRecord() { // TODO: pass an affinity handle here? int recordHandle = RemotableNativeMethods.MsiGetLastErrorRecord(0); if (recordHandle != 0) { using (Record errorRec = new Record((IntPtr) recordHandle, true, null)) { this.errorData = new object[errorRec.FieldCount]; for (int i = 0; i < this.errorData.Length; i++) { this.errorData[i] = errorRec[i + 1]; } } } else { this.errorData = null; } } private static string Combine(string msg1, string msg2) { if (msg1 == null) return msg2; if (msg2 == null) return msg1; return msg1 + " " + msg2; } } /// /// User Canceled the installation. /// [Serializable] public class InstallCanceledException : InstallerException { /// /// Creates a new InstallCanceledException with a specified error message and a reference to the /// inner exception that is the cause of this exception. /// /// The message that describes the error. /// The exception that is the cause of the current exception. If the /// innerException parameter is not a null reference (Nothing in Visual Basic), the current exception /// is raised in a catch block that handles the inner exception. public InstallCanceledException(string msg, Exception innerException) : base((int) NativeMethods.Error.INSTALL_USEREXIT, msg, innerException) { } /// /// Creates a new InstallCanceledException with a specified error message. /// /// The message that describes the error. public InstallCanceledException(string msg) : this(msg, null) { } /// /// Creates a new InstallCanceledException. /// public InstallCanceledException() : this(null, null) { } /// /// Initializes a new instance of the InstallCanceledException class with serialized data. /// /// The SerializationInfo that holds the serialized object data about the exception being thrown. /// The StreamingContext that contains contextual information about the source or destination. protected InstallCanceledException(SerializationInfo info, StreamingContext context) : base(info, context) { } } /// /// A bad SQL query string was passed to or . /// [Serializable] public class BadQuerySyntaxException : InstallerException { /// /// Creates a new BadQuerySyntaxException with a specified error message and a reference to the /// inner exception that is the cause of this exception. /// /// The message that describes the error. /// The exception that is the cause of the current exception. If the /// innerException parameter is not a null reference (Nothing in Visual Basic), the current exception /// is raised in a catch block that handles the inner exception. public BadQuerySyntaxException(string msg, Exception innerException) : base((int) NativeMethods.Error.BAD_QUERY_SYNTAX, msg, innerException) { } /// /// Creates a new BadQuerySyntaxException with a specified error message. /// /// The message that describes the error. public BadQuerySyntaxException(string msg) : this(msg, null) { } /// /// Creates a new BadQuerySyntaxException. /// public BadQuerySyntaxException() : this(null, null) { } /// /// Initializes a new instance of the BadQuerySyntaxException class with serialized data. /// /// The SerializationInfo that holds the serialized object data about the exception being thrown. /// The StreamingContext that contains contextual information about the source or destination. protected BadQuerySyntaxException(SerializationInfo info, StreamingContext context) : base(info, context) { } } /// /// A method was called on an invalid installer handle. The handle may have been already closed. /// [Serializable] public class InvalidHandleException : InstallerException { /// /// Creates a new InvalidHandleException with a specified error message and a reference to the /// inner exception that is the cause of this exception. /// /// The message that describes the error. /// The exception that is the cause of the current exception. If the /// innerException parameter is not a null reference (Nothing in Visual Basic), the current exception /// is raised in a catch block that handles the inner exception. public InvalidHandleException(string msg, Exception innerException) : base((int) NativeMethods.Error.INVALID_HANDLE, msg, innerException) { } /// /// Creates a new InvalidHandleException with a specified error message. /// /// The message that describes the error. public InvalidHandleException(string msg) : this(msg, null) { } /// /// Creates a new InvalidHandleException. /// public InvalidHandleException() : this(null, null) { } /// /// Initializes a new instance of the InvalidHandleException class with serialized data. /// /// The SerializationInfo that holds the serialized object data about the exception being thrown. /// The StreamingContext that contains contextual information about the source or destination. protected InvalidHandleException(SerializationInfo info, StreamingContext context) : base(info, context) { } } /// /// A failure occurred when executing . The exception may contain /// details about the merge conflict. /// [Serializable] public class MergeException : InstallerException { private IList conflictTables; private IList conflictCounts; /// /// Creates a new MergeException with a specified error message and a reference to the /// inner exception that is the cause of this exception. /// /// The message that describes the error. /// The exception that is the cause of the current exception. If the /// innerException parameter is not a null reference (Nothing in Visual Basic), the current exception /// is raised in a catch block that handles the inner exception. public MergeException(string msg, Exception innerException) : base(msg, innerException) { } /// /// Creates a new MergeException with a specified error message. /// /// The message that describes the error. public MergeException(string msg) : base(msg) { } /// /// Creates a new MergeException. /// public MergeException() : base() { } internal MergeException(Database db, string conflictsTableName) : base("Merge failed.") { if (conflictsTableName != null) { IList conflictTableList = new List(); IList conflictCountList = new List(); using (View view = db.OpenView("SELECT `Table`, `NumRowMergeConflicts` FROM `" + conflictsTableName + "`")) { view.Execute(); foreach (Record rec in view) using (rec) { conflictTableList.Add(rec.GetString(1)); conflictCountList.Add((int) rec.GetInteger(2)); } } this.conflictTables = conflictTableList; this.conflictCounts = conflictCountList; } } /// /// Initializes a new instance of the MergeException class with serialized data. /// /// The SerializationInfo that holds the serialized object data about the exception being thrown. /// The StreamingContext that contains contextual information about the source or destination. protected MergeException(SerializationInfo info, StreamingContext context) : base(info, context) { if (info == null) { throw new ArgumentNullException("info"); } this.conflictTables = (string[]) info.GetValue("mergeConflictTables", typeof(string[])); this.conflictCounts = (int[]) info.GetValue("mergeConflictCounts", typeof(int[])); } /// /// Gets the number of merge conflicts in each table, corresponding to the tables returned by /// . /// public IList ConflictCounts { get { return new List(this.conflictCounts); } } /// /// Gets the list of tables containing merge conflicts. /// public IList ConflictTables { get { return new List(this.conflictTables); } } /// /// Gets a message that describes the merge conflits. /// public override String Message { get { StringBuilder msg = new StringBuilder(base.Message); if (this.conflictTables != null) { for (int i = 0; i < this.conflictTables.Count; i++) { msg.Append(i == 0 ? " Conflicts: " : ", "); msg.Append(this.conflictTables[i]); msg.Append('('); msg.Append(this.conflictCounts[i]); msg.Append(')'); } } return msg.ToString(); } } /// /// Sets the SerializationInfo with information about the exception. /// /// The SerializationInfo that holds the serialized object data about the exception being thrown. /// The StreamingContext that contains contextual information about the source or destination. public override void GetObjectData(SerializationInfo info, StreamingContext context) { if (info == null) { throw new ArgumentNullException("info"); } info.AddValue("mergeConflictTables", this.conflictTables); info.AddValue("mergeConflictCounts", this.conflictCounts); base.GetObjectData(info, context); } } }