// 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.Msi
{
using System;
using System.ComponentModel;
using System.Globalization;
using System.IO;
using System.Text;
using System.Threading;
using WixToolset.Data;
using WixToolset.Core.Native;
///
/// Wrapper class for managing MSI API database handles.
///
internal sealed class Database : MsiHandle
{
private const int STG_E_LOCKVIOLATION = unchecked((int)0x80030021);
///
/// Constructor that opens an MSI database.
///
/// Path to the database to be opened.
/// Persist mode to use when opening the database.
public Database(string path, OpenDatabase type)
{
uint handle = 0;
int error = MsiInterop.MsiOpenDatabase(path, new IntPtr((int)type), out handle);
if (0 != error)
{
throw new MsiException(error);
}
this.Handle = handle;
}
public void ApplyTransform(string transformFile)
{
// get the curret validation bits
TransformErrorConditions conditions = TransformErrorConditions.None;
using (SummaryInformation summaryInfo = new SummaryInformation(transformFile))
{
string value = summaryInfo.GetProperty((int)SummaryInformation.Transform.ValidationFlags);
try
{
int validationFlags = Int32.Parse(value, CultureInfo.InvariantCulture);
conditions = (TransformErrorConditions)(validationFlags & 0xffff);
}
catch (FormatException)
{
// fallback to default of None
}
}
this.ApplyTransform(transformFile, conditions);
}
///
/// Applies a transform to this database.
///
/// Path to the transform file being applied.
/// Specifies the error conditions that are to be suppressed.
public void ApplyTransform(string transformFile, TransformErrorConditions errorConditions)
{
int error = MsiInterop.MsiDatabaseApplyTransform(this.Handle, transformFile, errorConditions);
if (0 != error)
{
throw new MsiException(error);
}
}
///
/// Commits changes made to the database.
///
public void Commit()
{
// Retry this call 3 times to deal with an MSI internal locking problem.
const int retryWait = 300;
const int retryLimit = 3;
int error = 0;
for (int i = 1; i <= retryLimit; ++i)
{
error = MsiInterop.MsiDatabaseCommit(this.Handle);
if (0 == error)
{
return;
}
else
{
MsiException exception = new MsiException(error);
// We need to see if the error code is contained in any of the strings in ErrorInfo.
// Join the array together and search for the error code to cover the string array.
if (!String.Join(", ", exception.ErrorInfo).Contains(STG_E_LOCKVIOLATION.ToString()))
{
break;
}
Console.Error.WriteLine(String.Format("Failed to create the database. Info: {0}. Retrying ({1} of {2})", String.Join(", ", exception.ErrorInfo), i, retryLimit));
Thread.Sleep(retryWait);
}
}
throw new MsiException(error);
}
///
/// Creates and populates the summary information stream of an existing transform file.
///
/// Required database that does not include the changes.
/// The name of the generated transform file.
/// Required error conditions that should be suppressed when the transform is applied.
/// Required when the transform is applied to a database;
/// shows which properties should be validated to verify that this transform can be applied to the database.
public void CreateTransformSummaryInfo(Database referenceDatabase, string transformFile, TransformErrorConditions errorConditions, TransformValidations validations)
{
int error = MsiInterop.MsiCreateTransformSummaryInfo(this.Handle, referenceDatabase.Handle, transformFile, errorConditions, validations);
if (0 != error)
{
throw new MsiException(error);
}
}
///
/// Imports an installer text archive table (idt file) into an open database.
///
/// Specifies the path to the file to import.
/// Attempted to import an IDT file with an invalid format or unsupported data.
/// Another error occured while importing the IDT file.
public void Import(string idtPath)
{
string folderPath = Path.GetFullPath(Path.GetDirectoryName(idtPath));
string fileName = Path.GetFileName(idtPath);
int error = MsiInterop.MsiDatabaseImport(this.Handle, folderPath, fileName);
if (1627 == error) // ERROR_FUNCTION_FAILED
{
throw new WixInvalidIdtException(idtPath);
}
else if (0 != error)
{
throw new MsiException(error);
}
}
///
/// Exports an installer table from an open database to a text archive file (idt file).
///
/// Specifies the name of the table to export.
/// Specifies the name of the folder that contains archive files. If null or empty string, uses current directory.
/// Specifies the name of the exported table archive file.
public void Export(string tableName, string folderPath, string fileName)
{
if (null == folderPath || 0 == folderPath.Length)
{
folderPath = System.Environment.CurrentDirectory;
}
int error = MsiInterop.MsiDatabaseExport(this.Handle, tableName, folderPath, fileName);
if (0 != error)
{
throw new MsiException(error);
}
}
///
/// Creates a transform that, when applied to the reference database, results in this database.
///
/// Required database that does not include the changes.
/// The name of the generated transform file. This is optional.
/// true if a transform is generated; false if a transform is not generated because
/// there are no differences between the two databases.
public bool GenerateTransform(Database referenceDatabase, string transformFile)
{
int error = MsiInterop.MsiDatabaseGenerateTransform(this.Handle, referenceDatabase.Handle, transformFile, 0, 0);
if (0 != error && 0xE8 != error) // ERROR_NO_DATA(0xE8) means no differences were found
{
throw new MsiException(error);
}
return (0xE8 != error);
}
///
/// Merges two databases together.
///
/// The database to merge into the base database.
/// The name of the table to receive merge conflict information.
public void Merge(Database mergeDatabase, string tableName)
{
int error = MsiInterop.MsiDatabaseMerge(this.Handle, mergeDatabase.Handle, tableName);
if (0 != error)
{
throw new MsiException(error);
}
}
///
/// Prepares a database query and creates a View object.
///
/// Specifies a SQL query string for querying the database.
/// A view object is returned if the query was successful.
public View OpenView(string query)
{
return new View(this, query);
}
///
/// Prepares and executes a database query and creates a View object.
///
/// Specifies a SQL query string for querying the database.
/// A view object is returned if the query was successful.
public View OpenExecuteView(string query)
{
View view = new View(this, query);
view.Execute();
return view;
}
///
/// Verifies the existence or absence of a table.
///
/// Table name to to verify the existence of.
/// Returns true if the table exists, false if it does not.
public bool TableExists(string tableName)
{
int result = MsiInterop.MsiDatabaseIsTablePersistent(this.Handle, tableName);
return MsiInterop.MSICONDITIONTRUE == result;
}
///
/// Returns a Record containing the names of all the primary
/// key columns for a specified table.
///
/// Specifies the name of the table from which to obtain
/// primary key names.
/// Returns a Record containing the names of all the
/// primary key columns for a specified table.
public Record PrimaryKeys(string tableName)
{
uint recordHandle;
int error = MsiInterop.MsiDatabaseGetPrimaryKeys(this.Handle, tableName, out recordHandle);
if (0 != error)
{
throw new MsiException(error);
}
return new Record(recordHandle);
}
///
/// Imports a table into the database.
///
/// Codepage of the database to import table to.
/// Table to import into database.
/// The base directory where intermediate files are created.
/// Whether to keep columns added in a transform.
public void ImportTable(int codepage, Table table, string baseDirectory, bool keepAddedColumns)
{
// write out the table to an IDT file
string idtPath = Path.Combine(baseDirectory, String.Concat(table.Name, ".idt"));
Encoding encoding;
// If UTF8 encoding, use the UTF8-specific constructor to avoid writing
// the byte order mark at the beginning of the file
if (Encoding.UTF8.CodePage == codepage)
{
encoding = new UTF8Encoding(false, true);
}
else
{
if (0 == codepage)
{
codepage = Encoding.ASCII.CodePage;
}
encoding = Encoding.GetEncoding(codepage, new EncoderExceptionFallback(), new DecoderExceptionFallback());
}
using (StreamWriter idtWriter = new StreamWriter(idtPath, false, encoding))
{
table.ToIdtDefinition(idtWriter, keepAddedColumns);
}
// try to import the table into the MSI
try
{
this.Import(idtPath);
}
catch (WixInvalidIdtException)
{
table.ValidateRows();
// If ValidateRows finds anything it doesn't like, it throws. Otherwise, we'll
// throw WixInvalidIdtException here which is caught in light and turns off tidy.
throw new WixInvalidIdtException(idtPath, table.Name);
}
}
}
}