// 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.Text; using System.Collections.Generic; using System.Globalization; using System.Runtime.InteropServices; using System.Diagnostics.CodeAnalysis; /// /// The Session object controls the installation process. It opens the /// install database, which contains the installation tables and data. /// ///

/// This object is associated with a standard set of action functions, /// each performing particular operations on data from one or more tables. Additional /// custom actions may be added for particular product installations. The basic engine /// function is a sequencer that fetches sequential records from a designated sequence /// table, evaluates any specified condition expression, and executes the designated /// action. Actions not recognized by the engine are deferred to the UI handler object /// for processing, usually dialog box sequences. ///

/// Note that only one Session object can be opened by a single process. ///

public sealed class Session : InstallerHandle, IFormatProvider { private Database database; private CustomActionData customActionData; private bool sessionAccessValidated = false; internal Session(IntPtr handle, bool ownsHandle) : base(handle, ownsHandle) { } /// /// Gets the Database for the install session. /// /// the Session handle is invalid /// the Database cannot be accessed ///

/// Normally there is no need to close this Database object. The same object can be /// used throughout the lifetime of the Session, and it will be closed when the Session /// is closed. ///

/// Win32 MSI API: /// MsiGetActiveDatabase ///

[SuppressMessage("Microsoft.Design", "CA1065:DoNotRaiseExceptionsInUnexpectedLocations")] public Database Database { get { if (this.database == null || this.database.IsClosed) { lock (this.Sync) { if (this.database == null || this.database.IsClosed) { this.ValidateSessionAccess(); int hDb = RemotableNativeMethods.MsiGetActiveDatabase((int) this.Handle); if (hDb == 0) { throw new InstallerException(); } this.database = new Database((IntPtr) hDb, true, "", DatabaseOpenMode.ReadOnly); } } } return this.database; } } /// /// Gets the numeric language ID used by the current install session. /// ///

/// Win32 MSI API: /// MsiGetLanguage ///

public int Language { get { return (int) RemotableNativeMethods.MsiGetLanguage((int) this.Handle); } } /// /// Gets or sets the string value of a named installer property, as maintained by the /// Session object in the in-memory Property table, or, if it is prefixed with a percent /// sign (%), the value of a system environment variable for the current process. /// /// the Session handle is invalid ///

/// Win32 MSI APIs: /// MsiGetProperty, /// MsiSetProperty ///

public string this[string property] { get { if (String.IsNullOrEmpty(property)) { throw new ArgumentNullException("property"); } if (!this.sessionAccessValidated && !Session.NonImmediatePropertyNames.Contains(property)) { this.ValidateSessionAccess(); } StringBuilder buf = new StringBuilder(); uint bufSize = 0; uint ret = RemotableNativeMethods.MsiGetProperty((int) this.Handle, property, buf, ref bufSize); if (ret == (uint) NativeMethods.Error.MORE_DATA) { buf.Capacity = (int) ++bufSize; ret = RemotableNativeMethods.MsiGetProperty((int) this.Handle, property, buf, ref bufSize); } if (ret != 0) { throw InstallerException.ExceptionFromReturnCode(ret); } return buf.ToString(); } set { if (String.IsNullOrEmpty(property)) { throw new ArgumentNullException("property"); } this.ValidateSessionAccess(); if (value == null) { value = String.Empty; } uint ret = RemotableNativeMethods.MsiSetProperty((int) this.Handle, property, value); if (ret != 0) { throw InstallerException.ExceptionFromReturnCode(ret); } } } /// /// Creates a new Session object from an integer session handle. /// /// Integer session handle /// true to close the handle when this object is disposed or finalized ///

/// This method is only provided for interop purposes. A Session object /// should normally be obtained by calling /// or . ///

public static Session FromHandle(IntPtr handle, bool ownsHandle) { return new Session(handle, ownsHandle); } /// /// Performs any enabled logging operations and defers execution to the UI handler /// object associated with the engine. /// /// Type of message to be processed /// Contains message-specific fields /// A message-dependent return value /// the Session or Record handle is invalid /// an invalid message kind is specified /// the user exited the installation /// the message-handler failed for an unknown reason ///

/// Logging may be selectively enabled for the various message types. /// See the method. ///

/// If record field 0 contains a formatting string, it is used to format the data in /// the other fields. Else if the message is an error, warning, or user message, an attempt /// is made to find a message template in the Error table for the current database using the /// error number found in field 1 of the record for message types and return values. ///

/// The parameter may also include message-box flags from /// the following enumerations: System.Windows.Forms.MessageBoxButtons, /// System.Windows.Forms.MessageBoxDefaultButton, System.Windows.Forms.MessageBoxIcon. These /// flags can be combined with the InstallMessage with a bitwise OR. ///

/// Note, this method never returns Cancel or Error values. Instead, appropriate /// exceptions are thrown in those cases. ///

/// Win32 MSI API: /// MsiProcessMessage ///

[SuppressMessage("Microsoft.Design", "CA1011:ConsiderPassingBaseTypesAsParameters")] public MessageResult Message(InstallMessage messageType, Record record) { if (record == null) { throw new ArgumentNullException("record"); } int ret = RemotableNativeMethods.MsiProcessMessage((int) this.Handle, (uint) messageType, (int) record.Handle); if (ret < 0) { throw new InstallerException(); } else if (ret == (int) MessageResult.Cancel) { throw new InstallCanceledException(); } return (MessageResult) ret; } /// /// Writes a message to the log, if logging is enabled. /// /// The line to be written to the log ///

/// Win32 MSI API: /// MsiProcessMessage ///

public void Log(string msg) { if (msg == null) { throw new ArgumentNullException("msg"); } using (Record rec = new Record(0)) { rec.FormatString = msg; this.Message(InstallMessage.Info, rec); } } /// /// Writes a formatted message to the log, if logging is enabled. /// /// The line to be written to the log, containing 0 or more format specifiers /// An array containing 0 or more objects to be formatted ///

/// Win32 MSI API: /// MsiProcessMessage ///

public void Log(string format, params object[] args) { this.Log(String.Format(CultureInfo.InvariantCulture, format, args)); } /// /// Evaluates a logical expression containing symbols and values. /// /// conditional expression /// The result of the condition evaluation /// the Session handle is invalid /// the condition is null or empty /// the conditional expression is invalid ///

/// Win32 MSI API: /// MsiEvaluateCondition ///

public bool EvaluateCondition(string condition) { if (String.IsNullOrEmpty(condition)) { throw new ArgumentNullException("condition"); } uint value = RemotableNativeMethods.MsiEvaluateCondition((int) this.Handle, condition); if (value == 0) { return false; } else if (value == 1) { return true; } else { throw new InvalidOperationException(); } } /// /// Evaluates a logical expression containing symbols and values, specifying a default /// value to be returned in case the condition is empty. /// /// conditional expression /// value to return if the condition is empty /// The result of the condition evaluation /// the Session handle is invalid /// the conditional expression is invalid ///

/// Win32 MSI API: /// MsiEvaluateCondition ///

public bool EvaluateCondition(string condition, bool defaultValue) { if (condition == null) { throw new ArgumentNullException("condition"); } else if (condition.Length == 0) { return defaultValue; } else { this.ValidateSessionAccess(); return this.EvaluateCondition(condition); } } /// /// Formats a string containing installer properties. /// /// A format string containing property tokens /// A formatted string containing property data /// the Record handle is invalid ///

/// Win32 MSI API: /// MsiFormatRecord ///

[SuppressMessage("Microsoft.Naming", "CA1719:ParameterNamesShouldNotMatchMemberNames")] public string Format(string format) { if (format == null) { throw new ArgumentNullException("format"); } using (Record formatRec = new Record(0)) { formatRec.FormatString = format; return formatRec.ToString(this); } } /// /// Returns a formatted string from record data. /// /// Record object containing a template and data to be formatted. /// The template string must be set in field 0 followed by any referenced data parameters. /// A formatted string containing the record data /// the Record handle is invalid ///

/// Win32 MSI API: /// MsiFormatRecord ///

public string FormatRecord(Record record) { if (record == null) { throw new ArgumentNullException("record"); } return record.ToString(this); } /// /// Returns a formatted string from record data using a specified format. /// /// Record object containing a template and data to be formatted /// Format string to be used instead of field 0 of the Record /// A formatted string containing the record data /// the Record handle is invalid ///

/// Win32 MSI API: /// MsiFormatRecord ///

[Obsolete("This method is obsolete because it has undesirable side-effects. As an alternative, set the Record's " + "FormatString property separately before calling the FormatRecord() override that takes only the Record parameter.")] public string FormatRecord(Record record, string format) { if (record == null) { throw new ArgumentNullException("record"); } return record.ToString(format, this); } /// /// Retrieves product properties (not session properties) from the product database. /// /// Value of the property, or an empty string if the property is not set. ///

/// Note this is not the correct method for getting ordinary session properties. For that, /// see the indexer on the Session class. ///

/// Win32 MSI API: /// MsiGetProductProperty ///

public string GetProductProperty(string property) { if (String.IsNullOrEmpty(property)) { throw new ArgumentNullException("property"); } this.ValidateSessionAccess(); StringBuilder buf = new StringBuilder(); uint bufSize = (uint) buf.Capacity; uint ret = NativeMethods.MsiGetProductProperty((int) this.Handle, property, buf, ref bufSize); if (ret == (uint) NativeMethods.Error.MORE_DATA) { buf.Capacity = (int) ++bufSize; ret = NativeMethods.MsiGetProductProperty((int) this.Handle, property, buf, ref bufSize); } if (ret != 0) { throw InstallerException.ExceptionFromReturnCode(ret); } return buf.ToString(); } /// /// Gets an accessor for components in the current session. /// public ComponentInfoCollection Components { get { this.ValidateSessionAccess(); return new ComponentInfoCollection(this); } } /// /// Gets an accessor for features in the current session. /// public FeatureInfoCollection Features { get { this.ValidateSessionAccess(); return new FeatureInfoCollection(this); } } /// /// Checks to see if sufficient disk space is present for the current installation. /// /// True if there is sufficient disk space; false otherwise. ///

/// Win32 MSI API: /// MsiVerifyDiskSpace ///

public bool VerifyDiskSpace() { this.ValidateSessionAccess(); uint ret = RemotableNativeMethods.MsiVerifyDiskSpace((int)this.Handle); if (ret == (uint) NativeMethods.Error.DISK_FULL) { return false; } else if (ret != 0) { throw InstallerException.ExceptionFromReturnCode(ret); } return true; } /// /// Gets the total disk space per drive required for the installation. /// /// A list of InstallCost structures, specifying the cost for each drive ///

/// Win32 MSI API: /// MsiEnumComponentCosts ///

[SuppressMessage("Microsoft.Design", "CA1024:UsePropertiesWhereAppropriate")] public IList GetTotalCost() { this.ValidateSessionAccess(); IList costs = new List(); StringBuilder driveBuf = new StringBuilder(20); for (uint i = 0; true; i++) { int cost, tempCost; uint driveBufSize = (uint) driveBuf.Capacity; uint ret = RemotableNativeMethods.MsiEnumComponentCosts( (int) this.Handle, null, i, (int) InstallState.Default, driveBuf, ref driveBufSize, out cost, out tempCost); if (ret == (uint) NativeMethods.Error.NO_MORE_ITEMS) break; if (ret == (uint) NativeMethods.Error.MORE_DATA) { driveBuf.Capacity = (int) ++driveBufSize; ret = RemotableNativeMethods.MsiEnumComponentCosts( (int) this.Handle, null, i, (int) InstallState.Default, driveBuf, ref driveBufSize, out cost, out tempCost); } if (ret != 0) { throw InstallerException.ExceptionFromReturnCode(ret); } costs.Add(new InstallCost(driveBuf.ToString(), cost * 512L, tempCost * 512L)); } return costs; } /// /// Gets the designated mode flag for the current install session. /// /// The type of mode to be checked. /// The value of the designated mode flag. /// the Session handle is invalid /// an invalid mode flag was specified ///

/// Note that only the following run modes are available to read from /// a deferred custom action: /// /// /// /// ///

/// Win32 MSI API: /// MsiGetMode ///

public bool GetMode(InstallRunMode mode) { return RemotableNativeMethods.MsiGetMode((int) this.Handle, (uint) mode); } /// /// Sets the designated mode flag for the current install session. /// /// The type of mode to be set. /// The desired value of the mode. /// the Session handle is invalid /// an invalid mode flag was specified /// the mode cannot not be set ///

/// Win32 MSI API: /// MsiSetMode ///

public void SetMode(InstallRunMode mode, bool value) { this.ValidateSessionAccess(); uint ret = RemotableNativeMethods.MsiSetMode((int) this.Handle, (uint) mode, value); if (ret != 0) { if (ret == (uint) NativeMethods.Error.ACCESS_DENIED) { throw new InvalidOperationException(); } else { throw InstallerException.ExceptionFromReturnCode(ret); } } } /// /// Gets the full path to the designated folder on the source media or server image. /// /// the folder was not found in the Directory table /// the Session handle is invalid ///

/// Win32 MSI API: /// MsiGetSourcePath ///

public string GetSourcePath(string directory) { if (String.IsNullOrEmpty(directory)) { throw new ArgumentNullException("directory"); } this.ValidateSessionAccess(); StringBuilder buf = new StringBuilder(); uint bufSize = 0; uint ret = RemotableNativeMethods.MsiGetSourcePath((int) this.Handle, directory, buf, ref bufSize); if (ret == (uint) NativeMethods.Error.MORE_DATA) { buf.Capacity = (int) ++bufSize; ret = ret = RemotableNativeMethods.MsiGetSourcePath((int) this.Handle, directory, buf, ref bufSize); } if (ret != 0) { if (ret == (uint) NativeMethods.Error.DIRECTORY) { throw InstallerException.ExceptionFromReturnCode(ret, directory); } else { throw InstallerException.ExceptionFromReturnCode(ret); } } return buf.ToString(); } /// /// Gets the full path to the designated folder on the installation target drive. /// /// the folder was not found in the Directory table /// the Session handle is invalid ///

/// Win32 MSI API: /// MsiGetTargetPath ///

public string GetTargetPath(string directory) { if (String.IsNullOrEmpty(directory)) { throw new ArgumentNullException("directory"); } this.ValidateSessionAccess(); StringBuilder buf = new StringBuilder(); uint bufSize = 0; uint ret = RemotableNativeMethods.MsiGetTargetPath((int) this.Handle, directory, buf, ref bufSize); if (ret == (uint) NativeMethods.Error.MORE_DATA) { buf.Capacity = (int) ++bufSize; ret = ret = RemotableNativeMethods.MsiGetTargetPath((int) this.Handle, directory, buf, ref bufSize); } if (ret != 0) { if (ret == (uint) NativeMethods.Error.DIRECTORY) { throw InstallerException.ExceptionFromReturnCode(ret, directory); } else { throw InstallerException.ExceptionFromReturnCode(ret); } } return buf.ToString(); } /// /// Sets the full path to the designated folder on the installation target drive. /// /// the folder was not found in the Directory table /// the Session handle is invalid ///

/// Setting the target path of a directory changes the path specification for the directory /// in the in-memory Directory table. Also, the path specifications of all other path objects /// in the table that are either subordinate or equivalent to the changed path are updated /// to reflect the change. The properties for each affected path are also updated. ///

/// If an error occurs in this function, all updated paths and properties revert to /// their previous values. Therefore, it is safe to treat errors returned by this function /// as non-fatal. ///

/// Do not attempt to configure the target path if the components using those paths /// are already installed for the current user or for a different user. Check the /// ProductState property before setting the target path to determine if the product /// containing this component is installed. ///

/// Win32 MSI API: /// MsiSetTargetPath ///

public void SetTargetPath(string directory, string value) { if (String.IsNullOrEmpty(directory)) { throw new ArgumentNullException("directory"); } if (value == null) { throw new ArgumentNullException("value"); } this.ValidateSessionAccess(); uint ret = RemotableNativeMethods.MsiSetTargetPath((int) this.Handle, directory, value); if (ret != 0) { if (ret == (uint) NativeMethods.Error.DIRECTORY) { throw InstallerException.ExceptionFromReturnCode(ret, directory); } else { throw InstallerException.ExceptionFromReturnCode(ret); } } } /// /// Sets the install level for the current installation to a specified value and /// recalculates the Select and Installed states for all features in the Feature /// table. Also sets the Action state of each component in the Component table based /// on the new level. /// /// New install level /// the Session handle is invalid ///

/// The SetInstallLevel method sets the following: /// The installation level for the current installation to a specified value /// The Select and Installed states for all features in the Feature table /// The Action state of each component in the Component table, based on the new level /// /// If 0 or a negative number is passed in the ilnstallLevel parameter, /// the current installation level does not change, but all features are still /// updated based on the current installation level. ///

/// Win32 MSI API: /// MsiSetInstallLevel ///

public void SetInstallLevel(int installLevel) { this.ValidateSessionAccess(); uint ret = RemotableNativeMethods.MsiSetInstallLevel((int) this.Handle, installLevel); if (ret != 0) { throw InstallerException.ExceptionFromReturnCode(ret); } } /// /// Executes a built-in action, custom action, or user-interface wizard action. /// /// Name of the action to execute. Case-sensitive. /// the Session handle is invalid /// the user exited the installation ///

/// The DoAction method executes the action that corresponds to the name supplied. If the /// name is not recognized by the installer as a built-in action or as a custom action in /// the CustomAction table, the name is passed to the user-interface handler object, which /// can invoke a function or a dialog box. If a null action name is supplied, the installer /// uses the upper-case value of the ACTION property as the action to perform. If no property /// value is defined, the default action is performed, defined as "INSTALL". ///

/// Actions that update the system, such as the InstallFiles and WriteRegistryValues /// actions, cannot be run by calling MsiDoAction. The exception to this rule is if DoAction /// is called from a custom action that is scheduled in the InstallExecuteSequence table /// between the InstallInitialize and InstallFinalize actions. Actions that do not update the /// system, such as AppSearch or CostInitialize, can be called. ///

/// Win32 MSI API: /// MsiDoAction ///

public void DoAction(string action) { this.DoAction(action, null); } /// /// Executes a built-in action, custom action, or user-interface wizard action. /// /// Name of the action to execute. Case-sensitive. /// Optional data to be passed to a deferred custom action. /// the Session handle is invalid /// the user exited the installation ///

/// The DoAction method executes the action that corresponds to the name supplied. If the /// name is not recognized by the installer as a built-in action or as a custom action in /// the CustomAction table, the name is passed to the user-interface handler object, which /// can invoke a function or a dialog box. If a null action name is supplied, the installer /// uses the upper-case value of the ACTION property as the action to perform. If no property /// value is defined, the default action is performed, defined as "INSTALL". ///

/// Actions that update the system, such as the InstallFiles and WriteRegistryValues /// actions, cannot be run by calling MsiDoAction. The exception to this rule is if DoAction /// is called from a custom action that is scheduled in the InstallExecuteSequence table /// between the InstallInitialize and InstallFinalize actions. Actions that do not update the /// system, such as AppSearch or CostInitialize, can be called. ///

/// If the called action is a deferred, rollback, or commit custom action, then the supplied /// will be available via the /// property of that custom action's session. ///

/// Win32 MSI API: /// MsiDoAction ///

public void DoAction(string action, CustomActionData actionData) { if (String.IsNullOrEmpty(action)) { throw new ArgumentNullException("action"); } this.ValidateSessionAccess(); if (actionData != null) { this[action] = actionData.ToString(); } uint ret = RemotableNativeMethods.MsiDoAction((int) this.Handle, action); if (ret != 0) { throw InstallerException.ExceptionFromReturnCode(ret); } } /// /// Executes an action sequence described in the specified table. /// /// Name of the table containing the action sequence. /// the Session handle is invalid /// the user exited the installation ///

/// This method queries the specified table, ordering the actions by the numbers in the Sequence column. /// For each row retrieved, an action is executed, provided that any supplied condition expression does /// not evaluate to FALSE. ///

/// An action sequence containing any actions that update the system, such as the InstallFiles and /// WriteRegistryValues actions, cannot be run by calling DoActionSequence. The exception to this rule is if /// DoActionSequence is called from a custom action that is scheduled in the InstallExecuteSequence table /// between the InstallInitialize and InstallFinalize actions. Actions that do not update the system, such /// as AppSearch or CostInitialize, can be called. ///

/// Win32 MSI API: /// MsiSequence ///

public void DoActionSequence(string sequenceTable) { if (String.IsNullOrEmpty(sequenceTable)) { throw new ArgumentNullException("sequenceTable"); } this.ValidateSessionAccess(); uint ret = RemotableNativeMethods.MsiSequence((int) this.Handle, sequenceTable, 0); if (ret != 0) { throw InstallerException.ExceptionFromReturnCode(ret); } } /// /// Gets custom action data for the session that was supplied by the caller. /// /// public CustomActionData CustomActionData { get { if (this.customActionData == null) { this.customActionData = new CustomActionData(this[CustomActionData.PropertyName]); } return this.customActionData; } } /// /// Implements formatting for data. /// /// Type of format object to get. /// The the current instance, if is the same type /// as the current instance; otherwise, null. object IFormatProvider.GetFormat(Type formatType) { return formatType == typeof(Session) ? this : null; } /// /// Closes the session handle. Also closes the active database handle, if it is open. /// After closing a handle, further method calls may throw an . /// /// If true, the method has been called directly /// or indirectly by a user's code, so managed and unmanaged resources will /// be disposed. If false, only unmanaged resources will be disposed. protected override void Dispose(bool disposing) { try { if (disposing) { if (this.database != null) { this.database.Dispose(); this.database = null; } } } finally { base.Dispose(disposing); } } /// /// Gets the (short) list of properties that are available from non-immediate custom actions. /// private static IList NonImmediatePropertyNames { get { return new string[] { CustomActionData.PropertyName, "ProductCode", "UserSID" }; } } /// /// Throws an exception if the custom action is not able to access immedate session details. /// private void ValidateSessionAccess() { if (!this.sessionAccessValidated) { if (this.GetMode(InstallRunMode.Scheduled) || this.GetMode(InstallRunMode.Rollback) || this.GetMode(InstallRunMode.Commit)) { throw new InstallerException("Cannot access session details from a non-immediate custom action"); } this.sessionAccessValidated = true; } } } }