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