aboutsummaryrefslogtreecommitdiff
path: root/src/WixToolset.Core.WindowsInstaller/Bind/CreateCabinetsCommand.cs
diff options
context:
space:
mode:
Diffstat (limited to 'src/WixToolset.Core.WindowsInstaller/Bind/CreateCabinetsCommand.cs')
-rw-r--r--src/WixToolset.Core.WindowsInstaller/Bind/CreateCabinetsCommand.cs499
1 files changed, 499 insertions, 0 deletions
diff --git a/src/WixToolset.Core.WindowsInstaller/Bind/CreateCabinetsCommand.cs b/src/WixToolset.Core.WindowsInstaller/Bind/CreateCabinetsCommand.cs
new file mode 100644
index 00000000..02015744
--- /dev/null
+++ b/src/WixToolset.Core.WindowsInstaller/Bind/CreateCabinetsCommand.cs
@@ -0,0 +1,499 @@
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.WindowsInstaller.Databases
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.Threading;
12 using WixToolset.Core.Bind;
13 using WixToolset.Core.WindowsInstaller.Bind;
14 using WixToolset.Data;
15 using WixToolset.Data.Bind;
16 using WixToolset.Data.Rows;
17 using WixToolset.Extensibility;
18
19 /// <summary>
20 /// Creates cabinet files.
21 /// </summary>
22 internal class CreateCabinetsCommand
23 {
24 public const int DefaultMaximumUncompressedMediaSize = 200; // Default value is 200 MB
25 public const int MaxValueOfMaxCabSizeForLargeFileSplitting = 2 * 1024; // 2048 MB (i.e. 2 GB)
26
27 private List<FileTransfer> fileTransfers;
28
29 private FileSplitCabNamesCallback newCabNamesCallBack;
30
31 private Dictionary<string, string> lastCabinetAddedToMediaTable; // Key is First Cabinet Name, Value is Last Cabinet Added in the Split Sequence
32
33 public CreateCabinetsCommand()
34 {
35 this.fileTransfers = new List<FileTransfer>();
36
37 this.newCabNamesCallBack = this.NewCabNamesCallBack;
38 }
39
40 /// <summary>
41 /// Sets the number of threads to use for cabinet creation.
42 /// </summary>
43 public int CabbingThreadCount { private get; set; }
44
45 public string CabCachePath { private get; set; }
46
47 public string TempFilesLocation { private get; set; }
48
49 /// <summary>
50 /// Sets the default compression level to use for cabinets
51 /// that don't have their compression level explicitly set.
52 /// </summary>
53 public CompressionLevel DefaultCompressionLevel { private get; set; }
54
55 public IEnumerable<IWindowsInstallerBackendExtension> BackendExtensions { private get; set; }
56
57 public Output Output { private get; set; }
58
59 public string LayoutDirectory { private get; set; }
60
61 public bool Compressed { private get; set; }
62
63 public Dictionary<MediaRow, IEnumerable<FileFacade>> FileRowsByCabinet { private get; set; }
64
65 public Func<MediaRow, string, string, string> ResolveMedia { private get; set; }
66
67 public TableDefinitionCollection TableDefinitions { private get; set; }
68
69 public Table WixMediaTable { private get; set; }
70
71 public IEnumerable<FileTransfer> FileTransfers => this.fileTransfers;
72
73 /// <param name="output">Output to generate image for.</param>
74 /// <param name="fileTransfers">Array of files to be transfered.</param>
75 /// <param name="layoutDirectory">The directory in which the image should be layed out.</param>
76 /// <param name="compressed">Flag if source image should be compressed.</param>
77 /// <returns>The uncompressed file rows.</returns>
78 public void Execute()
79 {
80 RowDictionary<WixMediaRow> wixMediaRows = new RowDictionary<WixMediaRow>(this.WixMediaTable);
81
82 this.lastCabinetAddedToMediaTable = new Dictionary<string, string>();
83
84 this.SetCabbingThreadCount();
85
86 // Send Binder object to Facilitate NewCabNamesCallBack Callback
87 CabinetBuilder cabinetBuilder = new CabinetBuilder(this.CabbingThreadCount, Marshal.GetFunctionPointerForDelegate(this.newCabNamesCallBack));
88
89 // Supply Compile MediaTemplate Attributes to Cabinet Builder
90 int MaximumCabinetSizeForLargeFileSplitting;
91 int MaximumUncompressedMediaSize;
92 this.GetMediaTemplateAttributes(out MaximumCabinetSizeForLargeFileSplitting, out MaximumUncompressedMediaSize);
93 cabinetBuilder.MaximumCabinetSizeForLargeFileSplitting = MaximumCabinetSizeForLargeFileSplitting;
94 cabinetBuilder.MaximumUncompressedMediaSize = MaximumUncompressedMediaSize;
95
96 foreach (var entry in this.FileRowsByCabinet)
97 {
98 MediaRow mediaRow = entry.Key;
99 IEnumerable<FileFacade> files = entry.Value;
100 CompressionLevel compressionLevel = this.DefaultCompressionLevel;
101
102 WixMediaRow wixMediaRow = null;
103 string mediaLayoutFolder = null;
104
105 if (wixMediaRows.TryGetValue(mediaRow.GetKey(), out wixMediaRow))
106 {
107 mediaLayoutFolder = wixMediaRow.Layout;
108
109 if (wixMediaRow.CompressionLevel.HasValue)
110 {
111 compressionLevel = wixMediaRow.CompressionLevel.Value;
112 }
113 }
114
115 string cabinetDir = this.ResolveMedia(mediaRow, mediaLayoutFolder, this.LayoutDirectory);
116
117 CabinetWorkItem cabinetWorkItem = this.CreateCabinetWorkItem(this.Output, cabinetDir, mediaRow, compressionLevel, files, this.fileTransfers);
118 if (null != cabinetWorkItem)
119 {
120 cabinetBuilder.Enqueue(cabinetWorkItem);
121 }
122 }
123
124 // stop processing if an error previously occurred
125 if (Messaging.Instance.EncounteredError)
126 {
127 return;
128 }
129
130 // create queued cabinets with multiple threads
131 cabinetBuilder.CreateQueuedCabinets();
132 if (Messaging.Instance.EncounteredError)
133 {
134 return;
135 }
136 }
137
138 /// <summary>
139 /// Sets the thead count to the number of processors if the current thread count is set to 0.
140 /// </summary>
141 /// <remarks>The thread count value must be greater than 0 otherwise and exception will be thrown.</remarks>
142 private void SetCabbingThreadCount()
143 {
144 // default the number of cabbing threads to the number of processors if it wasn't specified
145 if (0 == this.CabbingThreadCount)
146 {
147 string numberOfProcessors = System.Environment.GetEnvironmentVariable("NUMBER_OF_PROCESSORS");
148
149 try
150 {
151 if (null != numberOfProcessors)
152 {
153 this.CabbingThreadCount = Convert.ToInt32(numberOfProcessors, CultureInfo.InvariantCulture.NumberFormat);
154
155 if (0 >= this.CabbingThreadCount)
156 {
157 throw new WixException(WixErrors.IllegalEnvironmentVariable("NUMBER_OF_PROCESSORS", numberOfProcessors));
158 }
159 }
160 else // default to 1 if the environment variable is not set
161 {
162 this.CabbingThreadCount = 1;
163 }
164
165 Messaging.Instance.OnMessage(WixVerboses.SetCabbingThreadCount(this.CabbingThreadCount.ToString()));
166 }
167 catch (ArgumentException)
168 {
169 throw new WixException(WixErrors.IllegalEnvironmentVariable("NUMBER_OF_PROCESSORS", numberOfProcessors));
170 }
171 catch (FormatException)
172 {
173 throw new WixException(WixErrors.IllegalEnvironmentVariable("NUMBER_OF_PROCESSORS", numberOfProcessors));
174 }
175 }
176 }
177
178
179 /// <summary>
180 /// Creates a work item to create a cabinet.
181 /// </summary>
182 /// <param name="output">Output for the current database.</param>
183 /// <param name="cabinetDir">Directory to create cabinet in.</param>
184 /// <param name="mediaRow">MediaRow containing information about the cabinet.</param>
185 /// <param name="fileFacades">Collection of files in this cabinet.</param>
186 /// <param name="fileTransfers">Array of files to be transfered.</param>
187 /// <returns>created CabinetWorkItem object</returns>
188 private CabinetWorkItem CreateCabinetWorkItem(Output output, string cabinetDir, MediaRow mediaRow, CompressionLevel compressionLevel, IEnumerable<FileFacade> fileFacades, List<FileTransfer> fileTransfers)
189 {
190 CabinetWorkItem cabinetWorkItem = null;
191 string tempCabinetFileX = Path.Combine(this.TempFilesLocation, mediaRow.Cabinet);
192
193 // check for an empty cabinet
194 if (!fileFacades.Any())
195 {
196 string cabinetName = mediaRow.Cabinet;
197
198 // remove the leading '#' from the embedded cabinet name to make the warning easier to understand
199 if (cabinetName.StartsWith("#", StringComparison.Ordinal))
200 {
201 cabinetName = cabinetName.Substring(1);
202 }
203
204 // If building a patch, remind them to run -p for torch.
205 if (OutputType.Patch == output.Type)
206 {
207 Messaging.Instance.OnMessage(WixWarnings.EmptyCabinet(mediaRow.SourceLineNumbers, cabinetName, true));
208 }
209 else
210 {
211 Messaging.Instance.OnMessage(WixWarnings.EmptyCabinet(mediaRow.SourceLineNumbers, cabinetName));
212 }
213 }
214
215 var cabinetResolver = new CabinetResolver(this.CabCachePath, this.BackendExtensions);
216
217 ResolvedCabinet resolvedCabinet = cabinetResolver.ResolveCabinet(tempCabinetFileX, fileFacades);
218
219 // create a cabinet work item if it's not being skipped
220 if (CabinetBuildOption.BuildAndCopy == resolvedCabinet.BuildOption || CabinetBuildOption.BuildAndMove == resolvedCabinet.BuildOption)
221 {
222 int maxThreshold = 0; // default to the threshold for best smartcabbing (makes smallest cabinet).
223
224 cabinetWorkItem = new CabinetWorkItem(fileFacades, resolvedCabinet.Path, maxThreshold, compressionLevel/*, this.FileManager*/);
225 }
226 else // reuse the cabinet from the cabinet cache.
227 {
228 Messaging.Instance.OnMessage(WixVerboses.ReusingCabCache(mediaRow.SourceLineNumbers, mediaRow.Cabinet, resolvedCabinet.Path));
229
230 try
231 {
232 // Ensure the cached cabinet timestamp is current to prevent perpetual incremental builds. The
233 // problematic scenario goes like this. Imagine two cabinets in the cache. Update a file that
234 // goes into one of the cabinets. One cabinet will get rebuilt, the other will be copied from
235 // the cache. Now the file (an input) has a newer timestamp than the reused cabient (an output)
236 // causing the project to look like it perpetually needs a rebuild until all of the reused
237 // cabinets get newer timestamps.
238 File.SetLastWriteTime(resolvedCabinet.Path, DateTime.Now);
239 }
240 catch (Exception e)
241 {
242 Messaging.Instance.OnMessage(WixWarnings.CannotUpdateCabCache(mediaRow.SourceLineNumbers, resolvedCabinet.Path, e.Message));
243 }
244 }
245
246 if (mediaRow.Cabinet.StartsWith("#", StringComparison.Ordinal))
247 {
248 Table streamsTable = output.EnsureTable(this.TableDefinitions["_Streams"]);
249
250 Row streamRow = streamsTable.CreateRow(mediaRow.SourceLineNumbers);
251 streamRow[0] = mediaRow.Cabinet.Substring(1);
252 streamRow[1] = resolvedCabinet.Path;
253 }
254 else
255 {
256 string destinationPath = Path.Combine(cabinetDir, mediaRow.Cabinet);
257 FileTransfer transfer;
258 if (FileTransfer.TryCreate(resolvedCabinet.Path, destinationPath, CabinetBuildOption.BuildAndMove == resolvedCabinet.BuildOption, "Cabinet", mediaRow.SourceLineNumbers, out transfer))
259 {
260 transfer.Built = true;
261 fileTransfers.Add(transfer);
262 }
263 }
264
265 return cabinetWorkItem;
266 }
267
268 //private ResolvedCabinet ResolveCabinet(string cabinetPath, IEnumerable<FileFacade> fileFacades)
269 //{
270 // ResolvedCabinet resolved = null;
271
272 // List<BindFileWithPath> filesWithPath = fileFacades.Select(f => new BindFileWithPath() { Id = f.File.File, Path = f.WixFile.Source }).ToList();
273
274 // foreach (var extension in this.BackendExtensions)
275 // {
276 // resolved = extension.ResolveCabinet(cabinetPath, filesWithPath);
277 // if (null != resolved)
278 // {
279 // break;
280 // }
281 // }
282
283 // return resolved;
284 //}
285
286 /// <summary>
287 /// Delegate for Cabinet Split Callback
288 /// </summary>
289 [UnmanagedFunctionPointer(CallingConvention.StdCall)]
290 internal delegate void FileSplitCabNamesCallback([MarshalAs(UnmanagedType.LPWStr)]string firstCabName, [MarshalAs(UnmanagedType.LPWStr)]string newCabName, [MarshalAs(UnmanagedType.LPWStr)]string fileToken);
291
292 /// <summary>
293 /// Call back to Add File Transfer for new Cab and add new Cab to Media table
294 /// This callback can come from Multiple Cabinet Builder Threads and so should be thread safe
295 /// This callback will not be called in case there is no File splitting. i.e. MaximumCabinetSizeForLargeFileSplitting was not authored
296 /// </summary>
297 /// <param name="firstCabName">The name of splitting cabinet without extention e.g. "cab1".</param>
298 /// <param name="newCabName">The name of the new cabinet that would be formed by splitting e.g. "cab1b.cab"</param>
299 /// <param name="fileToken">The file token of the first file present in the splitting cabinet</param>
300 internal void NewCabNamesCallBack([MarshalAs(UnmanagedType.LPWStr)]string firstCabName, [MarshalAs(UnmanagedType.LPWStr)]string newCabName, [MarshalAs(UnmanagedType.LPWStr)]string fileToken)
301 {
302 // Locking Mutex here as this callback can come from Multiple Cabinet Builder Threads
303 Mutex mutex = new Mutex(false, "WixCabinetSplitBinderCallback");
304 try
305 {
306 if (!mutex.WaitOne(0, false)) // Check if you can get the lock
307 {
308 // Cound not get the Lock
309 Messaging.Instance.OnMessage(WixVerboses.CabinetsSplitInParallel());
310 mutex.WaitOne(); // Wait on other thread
311 }
312
313 string firstCabinetName = firstCabName + ".cab";
314 string newCabinetName = newCabName;
315 bool transferAdded = false; // Used for Error Handling
316
317 // Create File Transfer for new Cabinet using transfer of Base Cabinet
318 foreach (FileTransfer transfer in this.FileTransfers)
319 {
320 if (firstCabinetName.Equals(Path.GetFileName(transfer.Source), StringComparison.InvariantCultureIgnoreCase))
321 {
322 string newCabSourcePath = Path.Combine(Path.GetDirectoryName(transfer.Source), newCabinetName);
323 string newCabTargetPath = Path.Combine(Path.GetDirectoryName(transfer.Destination), newCabinetName);
324
325 FileTransfer newTransfer;
326 if (FileTransfer.TryCreate(newCabSourcePath, newCabTargetPath, transfer.Move, "Cabinet", transfer.SourceLineNumbers, out newTransfer))
327 {
328 newTransfer.Built = true;
329 this.fileTransfers.Add(newTransfer);
330 transferAdded = true;
331 break;
332 }
333 }
334 }
335
336 // Check if File Transfer was added
337 if (!transferAdded)
338 {
339 throw new WixException(WixErrors.SplitCabinetCopyRegistrationFailed(newCabinetName, firstCabinetName));
340 }
341
342 // Add the new Cabinets to media table using LastSequence of Base Cabinet
343 Table mediaTable = this.Output.Tables["Media"];
344 Table wixFileTable = this.Output.Tables["WixFile"];
345 int diskIDForLastSplitCabAdded = 0; // The DiskID value for the first cab in this cabinet split chain
346 int lastSequenceForLastSplitCabAdded = 0; // The LastSequence value for the first cab in this cabinet split chain
347 bool lastSplitCabinetFound = false; // Used for Error Handling
348
349 string lastCabinetOfThisSequence = String.Empty;
350 // Get the Value of Last Cabinet Added in this split Sequence from Dictionary
351 if (!this.lastCabinetAddedToMediaTable.TryGetValue(firstCabinetName, out lastCabinetOfThisSequence))
352 {
353 // If there is no value for this sequence, then use first Cabinet is the last one of this split sequence
354 lastCabinetOfThisSequence = firstCabinetName;
355 }
356
357 foreach (MediaRow mediaRow in mediaTable.Rows)
358 {
359 // Get details for the Last Cabinet Added in this Split Sequence
360 if ((lastSequenceForLastSplitCabAdded == 0) && lastCabinetOfThisSequence.Equals(mediaRow.Cabinet, StringComparison.InvariantCultureIgnoreCase))
361 {
362 lastSequenceForLastSplitCabAdded = mediaRow.LastSequence;
363 diskIDForLastSplitCabAdded = mediaRow.DiskId;
364 lastSplitCabinetFound = true;
365 }
366
367 // Check for Name Collision for the new Cabinet added
368 if (newCabinetName.Equals(mediaRow.Cabinet, StringComparison.InvariantCultureIgnoreCase))
369 {
370 // Name Collision of generated Split Cabinet Name and user Specified Cab name for current row
371 throw new WixException(WixErrors.SplitCabinetNameCollision(newCabinetName, firstCabinetName));
372 }
373 }
374
375 // Check if the last Split Cabinet was found in the Media Table
376 if (!lastSplitCabinetFound)
377 {
378 throw new WixException(WixErrors.SplitCabinetInsertionFailed(newCabinetName, firstCabinetName, lastCabinetOfThisSequence));
379 }
380
381 // The new Row has to be inserted just after the last cab in this cabinet split chain according to DiskID Sort
382 // This is because the FDI Extract requires DiskID of Split Cabinets to be continuous. It Fails otherwise with
383 // Error 2350 (FDI Server Error) as next DiskID did not have the right split cabinet during extraction
384 MediaRow newMediaRow = (MediaRow)mediaTable.CreateRow(null);
385 newMediaRow.Cabinet = newCabinetName;
386 newMediaRow.DiskId = diskIDForLastSplitCabAdded + 1; // When Sorted with DiskID, this new Cabinet Row is an Insertion
387 newMediaRow.LastSequence = lastSequenceForLastSplitCabAdded;
388
389 // Now increment the DiskID for all rows that come after the newly inserted row to Ensure that DiskId is unique
390 foreach (MediaRow mediaRow in mediaTable.Rows)
391 {
392 // Check if this row comes after inserted row and it is not the new cabinet inserted row
393 if (mediaRow.DiskId >= newMediaRow.DiskId && !newCabinetName.Equals(mediaRow.Cabinet, StringComparison.InvariantCultureIgnoreCase))
394 {
395 mediaRow.DiskId++; // Increment DiskID
396 }
397 }
398
399 // Now Increment DiskID for All files Rows so that they refer to the right Media Row
400 foreach (WixFileRow wixFileRow in wixFileTable.Rows)
401 {
402 // Check if this row comes after inserted row and if this row is not the file that has to go into the current cabinet
403 // This check will work as we have only one large file in every splitting cabinet
404 // If we want to support splitting cabinet with more large files we need to update this code
405 if (wixFileRow.DiskId >= newMediaRow.DiskId && !wixFileRow.File.Equals(fileToken, StringComparison.InvariantCultureIgnoreCase))
406 {
407 wixFileRow.DiskId++; // Increment DiskID
408 }
409 }
410
411 // Update the Last Cabinet Added in the Split Sequence in Dictionary for future callback
412 this.lastCabinetAddedToMediaTable[firstCabinetName] = newCabinetName;
413
414 mediaTable.ValidateRows(); // Valdiates DiskDIs, throws Exception as Wix Error if validation fails
415 }
416 finally
417 {
418 // Releasing the Mutex here
419 mutex.ReleaseMutex();
420 }
421 }
422
423
424 /// <summary>
425 /// Gets Compiler Values of MediaTemplate Attributes governing Maximum Cabinet Size after applying Environment Variable Overrides
426 /// </summary>
427 /// <param name="output">Output to generate image for.</param>
428 /// <param name="fileRows">The indexed file rows.</param>
429 private void GetMediaTemplateAttributes(out int maxCabSizeForLargeFileSplitting, out int maxUncompressedMediaSize)
430 {
431 // Get Environment Variable Overrides for MediaTemplate Attributes governing Maximum Cabinet Size
432 string mcslfsString = Environment.GetEnvironmentVariable("WIX_MCSLFS");
433 string mumsString = Environment.GetEnvironmentVariable("WIX_MUMS");
434 int maxCabSizeForLargeFileInMB = 0;
435 int maxPreCompressedSizeInMB = 0;
436 ulong testOverFlow = 0;
437
438 // Supply Compile MediaTemplate Attributes to Cabinet Builder
439 Table mediaTemplateTable = this.Output.Tables["WixMediaTemplate"];
440 if (mediaTemplateTable != null)
441 {
442 WixMediaTemplateRow mediaTemplateRow = (WixMediaTemplateRow)mediaTemplateTable.Rows[0];
443
444 // Get the Value for Max Cab Size for File Splitting
445 try
446 {
447 // Override authored mcslfs value if environment variable is authored.
448 if (!String.IsNullOrEmpty(mcslfsString))
449 {
450 maxCabSizeForLargeFileInMB = Int32.Parse(mcslfsString);
451 }
452 else
453 {
454 maxCabSizeForLargeFileInMB = mediaTemplateRow.MaximumCabinetSizeForLargeFileSplitting;
455 }
456 testOverFlow = (ulong)maxCabSizeForLargeFileInMB * 1024 * 1024;
457 }
458 catch (FormatException)
459 {
460 throw new WixException(WixErrors.IllegalEnvironmentVariable("WIX_MCSLFS", mcslfsString));
461 }
462 catch (OverflowException)
463 {
464 throw new WixException(WixErrors.MaximumCabinetSizeForLargeFileSplittingTooLarge(null, maxCabSizeForLargeFileInMB, MaxValueOfMaxCabSizeForLargeFileSplitting));
465 }
466
467 try
468 {
469 // Override authored mums value if environment variable is authored.
470 if (!String.IsNullOrEmpty(mumsString))
471 {
472 maxPreCompressedSizeInMB = Int32.Parse(mumsString);
473 }
474 else
475 {
476 maxPreCompressedSizeInMB = mediaTemplateRow.MaximumUncompressedMediaSize;
477 }
478 testOverFlow = (ulong)maxPreCompressedSizeInMB * 1024 * 1024;
479 }
480 catch (FormatException)
481 {
482 throw new WixException(WixErrors.IllegalEnvironmentVariable("WIX_MUMS", mumsString));
483 }
484 catch (OverflowException)
485 {
486 throw new WixException(WixErrors.MaximumUncompressedMediaSizeTooLarge(null, maxPreCompressedSizeInMB));
487 }
488
489 maxCabSizeForLargeFileSplitting = maxCabSizeForLargeFileInMB;
490 maxUncompressedMediaSize = maxPreCompressedSizeInMB;
491 }
492 else
493 {
494 maxCabSizeForLargeFileSplitting = 0;
495 maxUncompressedMediaSize = DefaultMaximumUncompressedMediaSize;
496 }
497 }
498 }
499}