aboutsummaryrefslogtreecommitdiff
path: root/src/WixToolset.Core.Native/Msi/Database.cs
diff options
context:
space:
mode:
Diffstat (limited to 'src/WixToolset.Core.Native/Msi/Database.cs')
-rw-r--r--src/WixToolset.Core.Native/Msi/Database.cs257
1 files changed, 257 insertions, 0 deletions
diff --git a/src/WixToolset.Core.Native/Msi/Database.cs b/src/WixToolset.Core.Native/Msi/Database.cs
new file mode 100644
index 00000000..a44e8cf9
--- /dev/null
+++ b/src/WixToolset.Core.Native/Msi/Database.cs
@@ -0,0 +1,257 @@
1// 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.
2
3namespace WixToolset.Core.Native.Msi
4{
5 using System;
6 using System.Globalization;
7 using System.IO;
8 using System.Threading;
9
10 /// <summary>
11 /// Wrapper class for managing MSI API database handles.
12 /// </summary>
13 public sealed class Database : MsiHandle
14 {
15 private const int STG_E_LOCKVIOLATION = unchecked((int)0x80030021);
16
17 /// <summary>
18 /// Constructor that opens an MSI database.
19 /// </summary>
20 /// <param name="path">Path to the database to be opened.</param>
21 /// <param name="type">Persist mode to use when opening the database.</param>
22 public Database(string path, OpenDatabase type)
23 {
24 var error = MsiInterop.MsiOpenDatabase(path, new IntPtr((int)type), out var handle);
25 if (0 != error)
26 {
27 throw new MsiException(error);
28 }
29 this.Handle = handle;
30 }
31
32 /// <summary>
33 /// Maximum length of stream in an MSI database.
34 /// </summary>
35 public static int MsiMaxStreamNameLength => MsiInterop.MsiMaxStreamNameLength;
36
37 /// <summary>
38 /// Apply a transform to the MSI.
39 /// </summary>
40 /// <param name="transformFile">Path to transform to apply.</param>
41 public void ApplyTransform(string transformFile)
42 {
43 // get the curret validation bits
44 var conditions = TransformErrorConditions.None;
45 using (var summaryInfo = new SummaryInformation(transformFile))
46 {
47 var value = summaryInfo.GetProperty((int)SummaryInformation.Transform.ValidationFlags);
48 try
49 {
50 var validationFlags = Int32.Parse(value, CultureInfo.InvariantCulture);
51 conditions = (TransformErrorConditions)(validationFlags & 0xffff);
52 }
53 catch (FormatException)
54 {
55 // fallback to default of None
56 }
57 }
58
59 this.ApplyTransform(transformFile, conditions);
60 }
61
62 /// <summary>
63 /// Applies a transform to this database.
64 /// </summary>
65 /// <param name="transformFile">Path to the transform file being applied.</param>
66 /// <param name="errorConditions">Specifies the error conditions that are to be suppressed.</param>
67 public void ApplyTransform(string transformFile, TransformErrorConditions errorConditions)
68 {
69 var error = MsiInterop.MsiDatabaseApplyTransform(this.Handle, transformFile, errorConditions);
70 if (0 != error)
71 {
72 throw new MsiException(error);
73 }
74 }
75
76 /// <summary>
77 /// Commits changes made to the database.
78 /// </summary>
79 public void Commit()
80 {
81 // Retry this call 3 times to deal with an MSI internal locking problem.
82 const int retryWait = 300;
83 const int retryLimit = 3;
84 var error = 0;
85
86 for (var i = 1; i <= retryLimit; ++i)
87 {
88 error = MsiInterop.MsiDatabaseCommit(this.Handle);
89
90 if (0 == error)
91 {
92 return;
93 }
94 else
95 {
96 var exception = new MsiException(error);
97
98 // We need to see if the error code is contained in any of the strings in ErrorInfo.
99 // Join the array together and search for the error code to cover the string array.
100 if (!String.Join(", ", exception.ErrorInfo).Contains(STG_E_LOCKVIOLATION.ToString()))
101 {
102 break;
103 }
104
105 Console.Error.WriteLine(String.Format("Failed to create the database. Info: {0}. Retrying ({1} of {2})", String.Join(", ", exception.ErrorInfo), i, retryLimit));
106 Thread.Sleep(retryWait);
107 }
108 }
109
110 throw new MsiException(error);
111 }
112
113 /// <summary>
114 /// Creates and populates the summary information stream of an existing transform file.
115 /// </summary>
116 /// <param name="referenceDatabase">Required database that does not include the changes.</param>
117 /// <param name="transformFile">The name of the generated transform file.</param>
118 /// <param name="errorConditions">Required error conditions that should be suppressed when the transform is applied.</param>
119 /// <param name="validations">Required when the transform is applied to a database;
120 /// shows which properties should be validated to verify that this transform can be applied to the database.</param>
121 public void CreateTransformSummaryInfo(Database referenceDatabase, string transformFile, TransformErrorConditions errorConditions, TransformValidations validations)
122 {
123 var error = MsiInterop.MsiCreateTransformSummaryInfo(this.Handle, referenceDatabase.Handle, transformFile, errorConditions, validations);
124 if (0 != error)
125 {
126 throw new MsiException(error);
127 }
128 }
129
130 /// <summary>
131 /// Imports an installer text archive table (idt file) into an open database.
132 /// </summary>
133 /// <param name="idtPath">Specifies the path to the file to import.</param>
134 /// <exception cref="WixInvalidIdtException">Attempted to import an IDT file with an invalid format or unsupported data.</exception>
135 /// <exception cref="MsiException">Another error occured while importing the IDT file.</exception>
136 public void Import(string idtPath)
137 {
138 var folderPath = Path.GetFullPath(Path.GetDirectoryName(idtPath));
139 var fileName = Path.GetFileName(idtPath);
140
141 var error = MsiInterop.MsiDatabaseImport(this.Handle, folderPath, fileName);
142 if (1627 == error) // ERROR_FUNCTION_FAILED
143 {
144 throw new WixInvalidIdtException(idtPath);
145 }
146 else if (0 != error)
147 {
148 throw new MsiException(error);
149 }
150 }
151
152 /// <summary>
153 /// Exports an installer table from an open database to a text archive file (idt file).
154 /// </summary>
155 /// <param name="tableName">Specifies the name of the table to export.</param>
156 /// <param name="folderPath">Specifies the name of the folder that contains archive files. If null or empty string, uses current directory.</param>
157 /// <param name="fileName">Specifies the name of the exported table archive file.</param>
158 public void Export(string tableName, string folderPath, string fileName)
159 {
160 if (String.IsNullOrEmpty(folderPath))
161 {
162 folderPath = Environment.CurrentDirectory;
163 }
164
165 var error = MsiInterop.MsiDatabaseExport(this.Handle, tableName, folderPath, fileName);
166 if (0 != error)
167 {
168 throw new MsiException(error);
169 }
170 }
171
172 /// <summary>
173 /// Creates a transform that, when applied to the reference database, results in this database.
174 /// </summary>
175 /// <param name="referenceDatabase">Required database that does not include the changes.</param>
176 /// <param name="transformFile">The name of the generated transform file. This is optional.</param>
177 /// <returns>true if a transform is generated; false if a transform is not generated because
178 /// there are no differences between the two databases.</returns>
179 public bool GenerateTransform(Database referenceDatabase, string transformFile)
180 {
181 var error = MsiInterop.MsiDatabaseGenerateTransform(this.Handle, referenceDatabase.Handle, transformFile, 0, 0);
182 if (0 != error && 0xE8 != error) // ERROR_NO_DATA(0xE8) means no differences were found
183 {
184 throw new MsiException(error);
185 }
186
187 return (0xE8 != error);
188 }
189
190 /// <summary>
191 /// Merges two databases together.
192 /// </summary>
193 /// <param name="mergeDatabase">The database to merge into the base database.</param>
194 /// <param name="tableName">The name of the table to receive merge conflict information.</param>
195 public void Merge(Database mergeDatabase, string tableName)
196 {
197 var error = MsiInterop.MsiDatabaseMerge(this.Handle, mergeDatabase.Handle, tableName);
198 if (0 != error)
199 {
200 throw new MsiException(error);
201 }
202 }
203
204 /// <summary>
205 /// Prepares a database query and creates a <see cref="View">View</see> object.
206 /// </summary>
207 /// <param name="query">Specifies a SQL query string for querying the database.</param>
208 /// <returns>A view object is returned if the query was successful.</returns>
209 public View OpenView(string query)
210 {
211 return new View(this, query);
212 }
213
214 /// <summary>
215 /// Prepares and executes a database query and creates a <see cref="View">View</see> object.
216 /// </summary>
217 /// <param name="query">Specifies a SQL query string for querying the database.</param>
218 /// <returns>A view object is returned if the query was successful.</returns>
219 public View OpenExecuteView(string query)
220 {
221 var view = new View(this, query);
222
223 view.Execute();
224 return view;
225 }
226
227 /// <summary>
228 /// Verifies the existence or absence of a table.
229 /// </summary>
230 /// <param name="tableName">Table name to to verify the existence of.</param>
231 /// <returns>Returns true if the table exists, false if it does not.</returns>
232 public bool TableExists(string tableName)
233 {
234 var result = MsiInterop.MsiDatabaseIsTablePersistent(this.Handle, tableName);
235 return MsiInterop.MSICONDITIONTRUE == result;
236 }
237
238 /// <summary>
239 /// Returns a <see cref="Record">Record</see> containing the names of all the primary
240 /// key columns for a specified table.
241 /// </summary>
242 /// <param name="tableName">Specifies the name of the table from which to obtain
243 /// primary key names.</param>
244 /// <returns>Returns a <see cref="Record">Record</see> containing the names of all the
245 /// primary key columns for a specified table.</returns>
246 public Record PrimaryKeys(string tableName)
247 {
248 var error = MsiInterop.MsiDatabaseGetPrimaryKeys(this.Handle, tableName, out var recordHandle);
249 if (error != 0)
250 {
251 throw new MsiException(error);
252 }
253
254 return new Record(recordHandle);
255 }
256 }
257}