main
cs 354 lines 16.4 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.WindowsInstaller.Bind
4 {
5 using System;
6 using System.Collections.Generic;
7 using System.Globalization;
8 using System.IO;
9 using System.Linq;
10 using System.Runtime.InteropServices;
11 using System.Text;
12 using System.Threading;
13 using WixToolset.Core.Native.Msi;
14 using WixToolset.Core.Native.Msm;
15 using WixToolset.Data;
16 using WixToolset.Data.Symbols;
17 using WixToolset.Data.WindowsInstaller;
18 using WixToolset.Extensibility.Data;
19 using WixToolset.Extensibility.Services;
20
21 /// <summary>
22 /// Merge modules into the database at output path.
23 /// </summary>
24 internal class MergeModulesCommand
25 {
26 public MergeModulesCommand(IMessaging messaging, IBackendHelper backendHelper, IEnumerable<IFileFacade> fileFacadesFromModule, IntermediateSection section, IEnumerable<string> suppressedTableNames, string outputPath, string intermediateFolder)
27 {
28 this.Messaging = messaging;
29 this.BackendHelper = backendHelper;
30 this.FileFacadesFromModule = fileFacadesFromModule;
31 this.Section = section;
32 this.SuppressedTableNames = suppressedTableNames ?? Array.Empty<string>();
33 this.OutputPath = outputPath;
34 this.IntermediateFolder = intermediateFolder;
35 }
36
37 private IMessaging Messaging { get; }
38
39 private IBackendHelper BackendHelper { get; }
40
41 private IEnumerable<IFileFacade> FileFacadesFromModule { get; }
42
43 private IntermediateSection Section { get; }
44
45 private IEnumerable<string> SuppressedTableNames { get; }
46
47 private string OutputPath { get; }
48
49 private string IntermediateFolder { get; }
50
51 public IReadOnlyList<ITrackedFile> TrackedFiles { get; private set; }
52
53 public void Execute()
54 {
55 var trackedFiles = new List<ITrackedFile>();
56
57 var wixMergeSymbols = this.Section.Symbols.OfType<WixMergeSymbol>().ToList();
58 if (!wixMergeSymbols.Any())
59 {
60 return;
61 }
62
63 IMsmMerge2 merge = null;
64 var commit = true;
65 var logOpen = false;
66 var databaseOpen = false;
67 var logPath = Path.Combine(this.IntermediateFolder, "merge.log");
68
69 try
70 {
71 merge = MsmInterop.GetMsmMerge();
72
73 ActionWithRetries(() => merge.OpenLog(logPath));
74 logOpen = true;
75
76 ActionWithRetries(() => merge.OpenDatabase(this.OutputPath));
77 databaseOpen = true;
78
79 var featureModulesByMergeId = this.Section.Symbols.OfType<WixFeatureModulesSymbol>().GroupBy(t => t.WixMergeRef).ToDictionary(g => g.Key);
80
81 // process all the merge rows
82 foreach (var wixMergeRow in wixMergeSymbols)
83 {
84 var moduleOpen = false;
85
86 try
87 {
88 short mergeLanguage;
89
90 try
91 {
92 mergeLanguage = Convert.ToInt16(wixMergeRow.Language, CultureInfo.InvariantCulture);
93 }
94 catch (FormatException)
95 {
96 this.Messaging.Write(ErrorMessages.InvalidMergeLanguage(wixMergeRow.SourceLineNumbers, wixMergeRow.Id.Id, wixMergeRow.Language.ToString()));
97 continue;
98 }
99
100 this.Messaging.Write(VerboseMessages.OpeningMergeModule(wixMergeRow.SourceFile, mergeLanguage));
101 ActionWithRetries(() => merge.OpenModule(wixMergeRow.SourceFile, mergeLanguage));
102 moduleOpen = true;
103
104 trackedFiles.Add(this.BackendHelper.TrackFile(wixMergeRow.SourceFile, TrackedFileType.Input, wixMergeRow.SourceLineNumbers));
105
106 // If there is merge configuration data, create a callback object to contain it all.
107 ConfigurationCallback callback = null;
108 if (!String.IsNullOrEmpty(wixMergeRow.ConfigurationData))
109 {
110 callback = new ConfigurationCallback(wixMergeRow.ConfigurationData);
111 }
112
113 // Merge the module into the database that's being built.
114 this.Messaging.Write(VerboseMessages.MergingMergeModule(wixMergeRow.SourceFile));
115 merge.MergeEx(wixMergeRow.FeatureRef, wixMergeRow.DirectoryRef, callback);
116
117 // Connect any non-primary features.
118 if (featureModulesByMergeId.TryGetValue(wixMergeRow.Id.Id, out var featureModules))
119 {
120 foreach (var featureModule in featureModules)
121 {
122 this.Messaging.Write(VerboseMessages.ConnectingMergeModule(wixMergeRow.SourceFile, featureModule.FeatureRef));
123 merge.Connect(featureModule.FeatureRef);
124 }
125 }
126 }
127 catch (COMException)
128 {
129 commit = false;
130 }
131 finally
132 {
133 var mergeErrors = merge.Errors;
134
135 // display all the errors encountered during the merge operations for this module
136 for (var i = 1; i <= mergeErrors.Count; i++)
137 {
138 var mergeError = mergeErrors[i];
139 var databaseKeys = new StringBuilder();
140 var moduleKeys = new StringBuilder();
141
142 // build a string of the database keys
143 for (var j = 1; j <= mergeError.DatabaseKeys.Count; j++)
144 {
145 if (1 != j)
146 {
147 databaseKeys.Append(';');
148 }
149 databaseKeys.Append(mergeError.DatabaseKeys[j]);
150 }
151
152 // build a string of the module keys
153 for (var j = 1; j <= mergeError.ModuleKeys.Count; j++)
154 {
155 if (1 != j)
156 {
157 moduleKeys.Append(';');
158 }
159 moduleKeys.Append(mergeError.ModuleKeys[j]);
160 }
161
162 // display the merge error based on the msm error type
163 switch (mergeError.Type)
164 {
165 case MsmErrorType.msmErrorExclusion:
166 this.Messaging.Write(ErrorMessages.MergeExcludedModule(wixMergeRow.SourceLineNumbers, wixMergeRow.Id.Id, moduleKeys.ToString()));
167 break;
168 case MsmErrorType.msmErrorFeatureRequired:
169 this.Messaging.Write(ErrorMessages.MergeFeatureRequired(wixMergeRow.SourceLineNumbers, mergeError.ModuleTable, moduleKeys.ToString(), wixMergeRow.SourceFile, wixMergeRow.Id.Id));
170 break;
171 case MsmErrorType.msmErrorLanguageFailed:
172 this.Messaging.Write(ErrorMessages.MergeLanguageFailed(wixMergeRow.SourceLineNumbers, mergeError.Language, wixMergeRow.SourceFile));
173 break;
174 case MsmErrorType.msmErrorLanguageUnsupported:
175 this.Messaging.Write(ErrorMessages.MergeLanguageUnsupported(wixMergeRow.SourceLineNumbers, mergeError.Language, wixMergeRow.SourceFile));
176 break;
177 case MsmErrorType.msmErrorResequenceMerge:
178 this.Messaging.Write(WarningMessages.MergeRescheduledAction(wixMergeRow.SourceLineNumbers, mergeError.DatabaseTable, databaseKeys.ToString(), wixMergeRow.SourceFile));
179 break;
180 case MsmErrorType.msmErrorTableMerge:
181 if ("_Validation" != mergeError.DatabaseTable) // ignore merge errors in the _Validation table
182 {
183 this.Messaging.Write(WarningMessages.MergeTableFailed(wixMergeRow.SourceLineNumbers, mergeError.DatabaseTable, databaseKeys.ToString(), wixMergeRow.SourceFile));
184 }
185 break;
186 case MsmErrorType.msmErrorPlatformMismatch:
187 this.Messaging.Write(ErrorMessages.MergePlatformMismatch(wixMergeRow.SourceLineNumbers, wixMergeRow.SourceFile));
188 break;
189 default:
190 this.Messaging.Write(ErrorMessages.UnexpectedException(String.Format(CultureInfo.CurrentUICulture, "Encountered an unexpected merge error of type '{0}' for which there is currently no error message to display. More information about the merge and the failure can be found in the merge log: '{1}'", Enum.GetName(typeof(MsmErrorType), mergeError.Type), logPath), "InvalidOperationException", Environment.StackTrace));
191 break;
192 }
193 }
194
195 if (0 >= mergeErrors.Count && !commit)
196 {
197 this.Messaging.Write(ErrorMessages.UnexpectedException(String.Format(CultureInfo.CurrentUICulture, "Encountered an unexpected error while merging '{0}'. More information about the merge and the failure can be found in the merge log: '{1}'", wixMergeRow.SourceFile, logPath), "InvalidOperationException", Environment.StackTrace));
198 }
199
200 if (moduleOpen)
201 {
202 merge.CloseModule();
203 }
204 }
205 }
206 }
207 finally
208 {
209 if (databaseOpen)
210 {
211 merge.CloseDatabase(commit);
212 }
213
214 if (logOpen)
215 {
216 merge.CloseLog();
217 }
218 }
219
220 // stop processing if an error previously occurred
221 if (this.Messaging.EncounteredError)
222 {
223 return;
224 }
225
226 using (var db = new Database(this.OutputPath, OpenDatabase.Direct))
227 {
228 // Suppress individual actions.
229 foreach (var suppressAction in this.Section.Symbols.OfType<WixSuppressActionSymbol>())
230 {
231 var tableName = suppressAction.SequenceTable.WindowsInstallerTableName();
232 if (db.TableExists(tableName))
233 {
234 var query = $"SELECT * FROM {tableName} WHERE `Action` = '{suppressAction.Action}'";
235
236 using (var view = db.OpenExecuteView(query))
237 using (var record = view.Fetch())
238 {
239 if (null != record)
240 {
241 this.Messaging.Write(WarningMessages.SuppressMergedAction(suppressAction.Action, tableName));
242 view.Modify(ModifyView.Delete, record);
243 }
244 }
245 }
246 }
247
248 // Query for merge module actions in suppressed sequences and drop them.
249 foreach (var tableName in this.SuppressedTableNames)
250 {
251 if (!db.TableExists(tableName))
252 {
253 continue;
254 }
255
256 using (var view = db.OpenExecuteView(String.Concat("SELECT `Action` FROM ", tableName)))
257 {
258 foreach (var resultRecord in view.Records)
259 {
260 this.Messaging.Write(WarningMessages.SuppressMergedAction(resultRecord.GetString(1), tableName));
261 }
262 }
263
264 // drop suppressed sequences
265 using (var view = db.OpenExecuteView(String.Concat("DROP TABLE ", tableName)))
266 {
267 }
268
269 // delete the validation rows
270 using (var view = db.OpenView(String.Concat("DELETE FROM _Validation WHERE `Table` = ?")))
271 using (var record = new Record(1))
272 {
273 record.SetString(1, tableName);
274 view.Execute(record);
275 }
276 }
277
278 // now update the Attributes column for the files from the Merge Modules
279 this.Messaging.Write(VerboseMessages.ResequencingMergeModuleFiles());
280 using (var view = db.OpenView("SELECT `Sequence`, `Attributes` FROM `File` WHERE `File`=?"))
281 {
282 foreach (var file in this.FileFacadesFromModule)
283 {
284 using (var record = new Record(1))
285 {
286 record.SetString(1, file.Id);
287 view.Execute(record);
288 }
289
290 using (var recordUpdate = view.Fetch())
291 {
292 if (null == recordUpdate)
293 {
294 throw new InvalidOperationException("Failed to fetch a File row from the database that was merged in from a module.");
295 }
296
297 recordUpdate.SetInteger(1, file.Sequence);
298
299 // Update the file attributes to match the compression specified
300 // on the Merge element or on the Package element.
301 var attributes = 0;
302
303 // Get the current value if its not null.
304 if (!recordUpdate.IsNull(2))
305 {
306 attributes = recordUpdate.GetInteger(2);
307 }
308
309 if (file.Compressed)
310 {
311 attributes |= WindowsInstallerConstants.MsidbFileAttributesCompressed;
312 attributes &= ~WindowsInstallerConstants.MsidbFileAttributesNoncompressed;
313 }
314 else if (file.Uncompressed)
315 {
316 attributes |= WindowsInstallerConstants.MsidbFileAttributesNoncompressed;
317 attributes &= ~WindowsInstallerConstants.MsidbFileAttributesCompressed;
318 }
319 else // clear all compression bits.
320 {
321 attributes &= ~WindowsInstallerConstants.MsidbFileAttributesCompressed;
322 attributes &= ~WindowsInstallerConstants.MsidbFileAttributesNoncompressed;
323 }
324
325 recordUpdate.SetInteger(2, attributes);
326
327 view.Modify(ModifyView.Update, recordUpdate);
328 }
329 }
330 }
331
332 db.Commit();
333 }
334
335 this.TrackedFiles = trackedFiles;
336 }
337
338 internal static void ActionWithRetries(Action action, int maxRetries = 3)
339 {
340 for (var attempt = 1; attempt <= maxRetries; ++attempt)
341 {
342 try
343 {
344 action();
345 break;
346 }
347 catch when (attempt < maxRetries)
348 {
349 Thread.Sleep(250);
350 }
351 }
352 }
353 }
354 }