aboutsummaryrefslogtreecommitdiff
path: root/src/WixToolset.Core/Msi/Database.cs
blob: 7c3a97bb910fae742307ff7119368a50d0032b52 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
// 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;

    /// <summary>
    /// Wrapper class for managing MSI API database handles.
    /// </summary>
    internal sealed class Database : MsiHandle
    {
        private const int STG_E_LOCKVIOLATION = unchecked((int)0x80030021);

        /// <summary>
        /// Constructor that opens an MSI database.
        /// </summary>
        /// <param name="path">Path to the database to be opened.</param>
        /// <param name="type">Persist mode to use when opening the database.</param>
        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);
        }

        /// <summary>
        /// Applies a transform to this database.
        /// </summary>
        /// <param name="transformFile">Path to the transform file being applied.</param>
        /// <param name="errorConditions">Specifies the error conditions that are to be suppressed.</param>
        public void ApplyTransform(string transformFile, TransformErrorConditions errorConditions)
        {
            int error = MsiInterop.MsiDatabaseApplyTransform(this.Handle, transformFile, errorConditions);
            if (0 != error)
            {
                throw new MsiException(error);
            }
        }

        /// <summary>
        /// Commits changes made to the database.
        /// </summary>
        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);
        }

        /// <summary>
        /// Creates and populates the summary information stream of an existing transform file.
        /// </summary>
        /// <param name="referenceDatabase">Required database that does not include the changes.</param>
        /// <param name="transformFile">The name of the generated transform file.</param>
        /// <param name="errorConditions">Required error conditions that should be suppressed when the transform is applied.</param>
        /// <param name="validations">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.</param>
        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);
            }
        }

        /// <summary>
        /// Imports an installer text archive table (idt file) into an open database.
        /// </summary>
        /// <param name="idtPath">Specifies the path to the file to import.</param>
        /// <exception cref="WixInvalidIdtException">Attempted to import an IDT file with an invalid format or unsupported data.</exception>
        /// <exception cref="MsiException">Another error occured while importing the IDT file.</exception>
        public void Import(string idtPath)
        {
            string folderPath = 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);
            }
        }

        /// <summary>
        /// Exports an installer table from an open database to a text archive file (idt file).
        /// </summary>
        /// <param name="tableName">Specifies the name of the table to export.</param>
        /// <param name="folderPath">Specifies the name of the folder that contains archive files. If null or empty string, uses current directory.</param>
        /// <param name="fileName">Specifies the name of the exported table archive file.</param>
        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);
            }
        }

        /// <summary>
        /// Creates a transform that, when applied to the reference database, results in this database.
        /// </summary>
        /// <param name="referenceDatabase">Required database that does not include the changes.</param>
        /// <param name="transformFile">The name of the generated transform file. This is optional.</param>
        /// <returns>true if a transform is generated; false if a transform is not generated because
        /// there are no differences between the two databases.</returns>
        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);
        }

        /// <summary>
        /// Merges two databases together.
        /// </summary>
        /// <param name="mergeDatabase">The database to merge into the base database.</param>
        /// <param name="tableName">The name of the table to receive merge conflict information.</param>
        public void Merge(Database mergeDatabase, string tableName)
        {
            int error = MsiInterop.MsiDatabaseMerge(this.Handle, mergeDatabase.Handle, tableName);
            if (0 != error)
            {
                throw new MsiException(error);
            }
        }

        /// <summary>
        /// Prepares a database query and creates a <see cref="View">View</see> object.
        /// </summary>
        /// <param name="query">Specifies a SQL query string for querying the database.</param>
        /// <returns>A view object is returned if the query was successful.</returns>
        public View OpenView(string query)
        {
            return new View(this, query);
        }

        /// <summary>
        /// Prepares and executes a database query and creates a <see cref="View">View</see> object.
        /// </summary>
        /// <param name="query">Specifies a SQL query string for querying the database.</param>
        /// <returns>A view object is returned if the query was successful.</returns>
        public View OpenExecuteView(string query)
        {
            View view = new View(this, query);

            view.Execute();
            return view;
        }

        /// <summary>
        /// Verifies the existence or absence of a table.
        /// </summary>
        /// <param name="tableName">Table name to to verify the existence of.</param>
        /// <returns>Returns true if the table exists, false if it does not.</returns>
        public bool TableExists(string tableName)
        {
            int result = MsiInterop.MsiDatabaseIsTablePersistent(this.Handle, tableName);
            return MsiInterop.MSICONDITIONTRUE == result;
        }

        /// <summary>
        /// Returns a <see cref="Record">Record</see> containing the names of all the primary 
        /// key columns for a specified table.
        /// </summary>
        /// <param name="tableName">Specifies the name of the table from which to obtain 
        /// primary key names.</param>
        /// <returns>Returns a <see cref="Record">Record</see> containing the names of all the 
        /// primary key columns for a specified table.</returns>
        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);
        }

        /// <summary>
        /// Imports a table into the database.
        /// </summary>
        /// <param name="codepage">Codepage of the database to import table to.</param>
        /// <param name="table">Table to import into database.</param>
        /// <param name="baseDirectory">The base directory where intermediate files are created.</param>
        /// <param name="keepAddedColumns">Whether to keep columns added in a transform.</param>
        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);
            }
        }
    }
}