// 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.Core.WindowsInstaller.Databases { using System; using System.Collections.Generic; using System.Globalization; using System.IO; using System.Linq; using System.Runtime.InteropServices; using System.Threading; using WixToolset.Core.Bind; using WixToolset.Core.WindowsInstaller.Bind; using WixToolset.Data; using WixToolset.Data.Bind; using WixToolset.Data.Rows; using WixToolset.Extensibility; /// /// Creates cabinet files. /// internal class CreateCabinetsCommand { public const int DefaultMaximumUncompressedMediaSize = 200; // Default value is 200 MB public const int MaxValueOfMaxCabSizeForLargeFileSplitting = 2 * 1024; // 2048 MB (i.e. 2 GB) private List fileTransfers; private FileSplitCabNamesCallback newCabNamesCallBack; private Dictionary lastCabinetAddedToMediaTable; // Key is First Cabinet Name, Value is Last Cabinet Added in the Split Sequence public CreateCabinetsCommand() { this.fileTransfers = new List(); this.newCabNamesCallBack = this.NewCabNamesCallBack; } /// /// Sets the number of threads to use for cabinet creation. /// public int CabbingThreadCount { private get; set; } public string CabCachePath { private get; set; } public string TempFilesLocation { private get; set; } /// /// Sets the default compression level to use for cabinets /// that don't have their compression level explicitly set. /// public CompressionLevel DefaultCompressionLevel { private get; set; } public IEnumerable BackendExtensions { private get; set; } public Output Output { private get; set; } public string LayoutDirectory { private get; set; } public bool Compressed { private get; set; } public Dictionary> FileRowsByCabinet { private get; set; } public Func ResolveMedia { private get; set; } public TableDefinitionCollection TableDefinitions { private get; set; } public Table WixMediaTable { private get; set; } public IEnumerable FileTransfers => this.fileTransfers; /// Output to generate image for. /// Array of files to be transfered. /// The directory in which the image should be layed out. /// Flag if source image should be compressed. /// The uncompressed file rows. public void Execute() { RowDictionary wixMediaRows = new RowDictionary(this.WixMediaTable); this.lastCabinetAddedToMediaTable = new Dictionary(); this.SetCabbingThreadCount(); // Send Binder object to Facilitate NewCabNamesCallBack Callback CabinetBuilder cabinetBuilder = new CabinetBuilder(this.CabbingThreadCount, Marshal.GetFunctionPointerForDelegate(this.newCabNamesCallBack)); // Supply Compile MediaTemplate Attributes to Cabinet Builder int MaximumCabinetSizeForLargeFileSplitting; int MaximumUncompressedMediaSize; this.GetMediaTemplateAttributes(out MaximumCabinetSizeForLargeFileSplitting, out MaximumUncompressedMediaSize); cabinetBuilder.MaximumCabinetSizeForLargeFileSplitting = MaximumCabinetSizeForLargeFileSplitting; cabinetBuilder.MaximumUncompressedMediaSize = MaximumUncompressedMediaSize; foreach (var entry in this.FileRowsByCabinet) { MediaRow mediaRow = entry.Key; IEnumerable files = entry.Value; CompressionLevel compressionLevel = this.DefaultCompressionLevel; WixMediaRow wixMediaRow = null; string mediaLayoutFolder = null; if (wixMediaRows.TryGetValue(mediaRow.GetKey(), out wixMediaRow)) { mediaLayoutFolder = wixMediaRow.Layout; if (wixMediaRow.CompressionLevel.HasValue) { compressionLevel = wixMediaRow.CompressionLevel.Value; } } string cabinetDir = this.ResolveMedia(mediaRow, mediaLayoutFolder, this.LayoutDirectory); CabinetWorkItem cabinetWorkItem = this.CreateCabinetWorkItem(this.Output, cabinetDir, mediaRow, compressionLevel, files, this.fileTransfers); if (null != cabinetWorkItem) { cabinetBuilder.Enqueue(cabinetWorkItem); } } // stop processing if an error previously occurred if (Messaging.Instance.EncounteredError) { return; } // create queued cabinets with multiple threads cabinetBuilder.CreateQueuedCabinets(); if (Messaging.Instance.EncounteredError) { return; } } /// /// Sets the thead count to the number of processors if the current thread count is set to 0. /// /// The thread count value must be greater than 0 otherwise and exception will be thrown. private void SetCabbingThreadCount() { // default the number of cabbing threads to the number of processors if it wasn't specified if (0 == this.CabbingThreadCount) { string numberOfProcessors = System.Environment.GetEnvironmentVariable("NUMBER_OF_PROCESSORS"); try { if (null != numberOfProcessors) { this.CabbingThreadCount = Convert.ToInt32(numberOfProcessors, CultureInfo.InvariantCulture.NumberFormat); if (0 >= this.CabbingThreadCount) { throw new WixException(WixErrors.IllegalEnvironmentVariable("NUMBER_OF_PROCESSORS", numberOfProcessors)); } } else // default to 1 if the environment variable is not set { this.CabbingThreadCount = 1; } Messaging.Instance.OnMessage(WixVerboses.SetCabbingThreadCount(this.CabbingThreadCount.ToString())); } catch (ArgumentException) { throw new WixException(WixErrors.IllegalEnvironmentVariable("NUMBER_OF_PROCESSORS", numberOfProcessors)); } catch (FormatException) { throw new WixException(WixErrors.IllegalEnvironmentVariable("NUMBER_OF_PROCESSORS", numberOfProcessors)); } } } /// /// Creates a work item to create a cabinet. /// /// Output for the current database. /// Directory to create cabinet in. /// MediaRow containing information about the cabinet. /// Collection of files in this cabinet. /// Array of files to be transfered. /// created CabinetWorkItem object private CabinetWorkItem CreateCabinetWorkItem(Output output, string cabinetDir, MediaRow mediaRow, CompressionLevel compressionLevel, IEnumerable fileFacades, List fileTransfers) { CabinetWorkItem cabinetWorkItem = null; string tempCabinetFileX = Path.Combine(this.TempFilesLocation, mediaRow.Cabinet); // check for an empty cabinet if (!fileFacades.Any()) { string cabinetName = mediaRow.Cabinet; // remove the leading '#' from the embedded cabinet name to make the warning easier to understand if (cabinetName.StartsWith("#", StringComparison.Ordinal)) { cabinetName = cabinetName.Substring(1); } // If building a patch, remind them to run -p for torch. if (OutputType.Patch == output.Type) { Messaging.Instance.OnMessage(WixWarnings.EmptyCabinet(mediaRow.SourceLineNumbers, cabinetName, true)); } else { Messaging.Instance.OnMessage(WixWarnings.EmptyCabinet(mediaRow.SourceLineNumbers, cabinetName)); } } var cabinetResolver = new CabinetResolver(this.CabCachePath, this.BackendExtensions); ResolvedCabinet resolvedCabinet = cabinetResolver.ResolveCabinet(tempCabinetFileX, fileFacades); // create a cabinet work item if it's not being skipped if (CabinetBuildOption.BuildAndCopy == resolvedCabinet.BuildOption || CabinetBuildOption.BuildAndMove == resolvedCabinet.BuildOption) { int maxThreshold = 0; // default to the threshold for best smartcabbing (makes smallest cabinet). cabinetWorkItem = new CabinetWorkItem(fileFacades, resolvedCabinet.Path, maxThreshold, compressionLevel/*, this.FileManager*/); } else // reuse the cabinet from the cabinet cache. { Messaging.Instance.OnMessage(WixVerboses.ReusingCabCache(mediaRow.SourceLineNumbers, mediaRow.Cabinet, resolvedCabinet.Path)); try { // Ensure the cached cabinet timestamp is current to prevent perpetual incremental builds. The // problematic scenario goes like this. Imagine two cabinets in the cache. Update a file that // goes into one of the cabinets. One cabinet will get rebuilt, the other will be copied from // the cache. Now the file (an input) has a newer timestamp than the reused cabient (an output) // causing the project to look like it perpetually needs a rebuild until all of the reused // cabinets get newer timestamps. File.SetLastWriteTime(resolvedCabinet.Path, DateTime.Now); } catch (Exception e) { Messaging.Instance.OnMessage(WixWarnings.CannotUpdateCabCache(mediaRow.SourceLineNumbers, resolvedCabinet.Path, e.Message)); } } if (mediaRow.Cabinet.StartsWith("#", StringComparison.Ordinal)) { Table streamsTable = output.EnsureTable(this.TableDefinitions["_Streams"]); Row streamRow = streamsTable.CreateRow(mediaRow.SourceLineNumbers); streamRow[0] = mediaRow.Cabinet.Substring(1); streamRow[1] = resolvedCabinet.Path; } else { string destinationPath = Path.Combine(cabinetDir, mediaRow.Cabinet); FileTransfer transfer; if (FileTransfer.TryCreate(resolvedCabinet.Path, destinationPath, CabinetBuildOption.BuildAndMove == resolvedCabinet.BuildOption, "Cabinet", mediaRow.SourceLineNumbers, out transfer)) { transfer.Built = true; fileTransfers.Add(transfer); } } return cabinetWorkItem; } //private ResolvedCabinet ResolveCabinet(string cabinetPath, IEnumerable fileFacades) //{ // ResolvedCabinet resolved = null; // List filesWithPath = fileFacades.Select(f => new BindFileWithPath() { Id = f.File.File, Path = f.WixFile.Source }).ToList(); // foreach (var extension in this.BackendExtensions) // { // resolved = extension.ResolveCabinet(cabinetPath, filesWithPath); // if (null != resolved) // { // break; // } // } // return resolved; //} /// /// Delegate for Cabinet Split Callback /// [UnmanagedFunctionPointer(CallingConvention.StdCall)] internal delegate void FileSplitCabNamesCallback([MarshalAs(UnmanagedType.LPWStr)]string firstCabName, [MarshalAs(UnmanagedType.LPWStr)]string newCabName, [MarshalAs(UnmanagedType.LPWStr)]string fileToken); /// /// Call back to Add File Transfer for new Cab and add new Cab to Media table /// This callback can come from Multiple Cabinet Builder Threads and so should be thread safe /// This callback will not be called in case there is no File splitting. i.e. MaximumCabinetSizeForLargeFileSplitting was not authored /// /// The name of splitting cabinet without extention e.g. "cab1". /// The name of the new cabinet that would be formed by splitting e.g. "cab1b.cab" /// The file token of the first file present in the splitting cabinet internal void NewCabNamesCallBack([MarshalAs(UnmanagedType.LPWStr)]string firstCabName, [MarshalAs(UnmanagedType.LPWStr)]string newCabName, [MarshalAs(UnmanagedType.LPWStr)]string fileToken) { // Locking Mutex here as this callback can come from Multiple Cabinet Builder Threads Mutex mutex = new Mutex(false, "WixCabinetSplitBinderCallback"); try { if (!mutex.WaitOne(0, false)) // Check if you can get the lock { // Cound not get the Lock Messaging.Instance.OnMessage(WixVerboses.CabinetsSplitInParallel()); mutex.WaitOne(); // Wait on other thread } string firstCabinetName = firstCabName + ".cab"; string newCabinetName = newCabName; bool transferAdded = false; // Used for Error Handling // Create File Transfer for new Cabinet using transfer of Base Cabinet foreach (FileTransfer transfer in this.FileTransfers) { if (firstCabinetName.Equals(Path.GetFileName(transfer.Source), StringComparison.InvariantCultureIgnoreCase)) { string newCabSourcePath = Path.Combine(Path.GetDirectoryName(transfer.Source), newCabinetName); string newCabTargetPath = Path.Combine(Path.GetDirectoryName(transfer.Destination), newCabinetName); FileTransfer newTransfer; if (FileTransfer.TryCreate(newCabSourcePath, newCabTargetPath, transfer.Move, "Cabinet", transfer.SourceLineNumbers, out newTransfer)) { newTransfer.Built = true; this.fileTransfers.Add(newTransfer); transferAdded = true; break; } } } // Check if File Transfer was added if (!transferAdded) { throw new WixException(WixErrors.SplitCabinetCopyRegistrationFailed(newCabinetName, firstCabinetName)); } // Add the new Cabinets to media table using LastSequence of Base Cabinet Table mediaTable = this.Output.Tables["Media"]; Table wixFileTable = this.Output.Tables["WixFile"]; int diskIDForLastSplitCabAdded = 0; // The DiskID value for the first cab in this cabinet split chain int lastSequenceForLastSplitCabAdded = 0; // The LastSequence value for the first cab in this cabinet split chain bool lastSplitCabinetFound = false; // Used for Error Handling string lastCabinetOfThisSequence = String.Empty; // Get the Value of Last Cabinet Added in this split Sequence from Dictionary if (!this.lastCabinetAddedToMediaTable.TryGetValue(firstCabinetName, out lastCabinetOfThisSequence)) { // If there is no value for this sequence, then use first Cabinet is the last one of this split sequence lastCabinetOfThisSequence = firstCabinetName; } foreach (MediaRow mediaRow in mediaTable.Rows) { // Get details for the Last Cabinet Added in this Split Sequence if ((lastSequenceForLastSplitCabAdded == 0) && lastCabinetOfThisSequence.Equals(mediaRow.Cabinet, StringComparison.InvariantCultureIgnoreCase)) { lastSequenceForLastSplitCabAdded = mediaRow.LastSequence; diskIDForLastSplitCabAdded = mediaRow.DiskId; lastSplitCabinetFound = true; } // Check for Name Collision for the new Cabinet added if (newCabinetName.Equals(mediaRow.Cabinet, StringComparison.InvariantCultureIgnoreCase)) { // Name Collision of generated Split Cabinet Name and user Specified Cab name for current row throw new WixException(WixErrors.SplitCabinetNameCollision(newCabinetName, firstCabinetName)); } } // Check if the last Split Cabinet was found in the Media Table if (!lastSplitCabinetFound) { throw new WixException(WixErrors.SplitCabinetInsertionFailed(newCabinetName, firstCabinetName, lastCabinetOfThisSequence)); } // The new Row has to be inserted just after the last cab in this cabinet split chain according to DiskID Sort // This is because the FDI Extract requires DiskID of Split Cabinets to be continuous. It Fails otherwise with // Error 2350 (FDI Server Error) as next DiskID did not have the right split cabinet during extraction MediaRow newMediaRow = (MediaRow)mediaTable.CreateRow(null); newMediaRow.Cabinet = newCabinetName; newMediaRow.DiskId = diskIDForLastSplitCabAdded + 1; // When Sorted with DiskID, this new Cabinet Row is an Insertion newMediaRow.LastSequence = lastSequenceForLastSplitCabAdded; // Now increment the DiskID for all rows that come after the newly inserted row to Ensure that DiskId is unique foreach (MediaRow mediaRow in mediaTable.Rows) { // Check if this row comes after inserted row and it is not the new cabinet inserted row if (mediaRow.DiskId >= newMediaRow.DiskId && !newCabinetName.Equals(mediaRow.Cabinet, StringComparison.InvariantCultureIgnoreCase)) { mediaRow.DiskId++; // Increment DiskID } } // Now Increment DiskID for All files Rows so that they refer to the right Media Row foreach (WixFileRow wixFileRow in wixFileTable.Rows) { // Check if this row comes after inserted row and if this row is not the file that has to go into the current cabinet // This check will work as we have only one large file in every splitting cabinet // If we want to support splitting cabinet with more large files we need to update this code if (wixFileRow.DiskId >= newMediaRow.DiskId && !wixFileRow.File.Equals(fileToken, StringComparison.InvariantCultureIgnoreCase)) { wixFileRow.DiskId++; // Increment DiskID } } // Update the Last Cabinet Added in the Split Sequence in Dictionary for future callback this.lastCabinetAddedToMediaTable[firstCabinetName] = newCabinetName; mediaTable.ValidateRows(); // Valdiates DiskDIs, throws Exception as Wix Error if validation fails } finally { // Releasing the Mutex here mutex.ReleaseMutex(); } } /// /// Gets Compiler Values of MediaTemplate Attributes governing Maximum Cabinet Size after applying Environment Variable Overrides /// /// Output to generate image for. /// The indexed file rows. private void GetMediaTemplateAttributes(out int maxCabSizeForLargeFileSplitting, out int maxUncompressedMediaSize) { // Get Environment Variable Overrides for MediaTemplate Attributes governing Maximum Cabinet Size string mcslfsString = Environment.GetEnvironmentVariable("WIX_MCSLFS"); string mumsString = Environment.GetEnvironmentVariable("WIX_MUMS"); int maxCabSizeForLargeFileInMB = 0; int maxPreCompressedSizeInMB = 0; ulong testOverFlow = 0; // Supply Compile MediaTemplate Attributes to Cabinet Builder Table mediaTemplateTable = this.Output.Tables["WixMediaTemplate"]; if (mediaTemplateTable != null) { WixMediaTemplateRow mediaTemplateRow = (WixMediaTemplateRow)mediaTemplateTable.Rows[0]; // Get the Value for Max Cab Size for File Splitting try { // Override authored mcslfs value if environment variable is authored. if (!String.IsNullOrEmpty(mcslfsString)) { maxCabSizeForLargeFileInMB = Int32.Parse(mcslfsString); } else { maxCabSizeForLargeFileInMB = mediaTemplateRow.MaximumCabinetSizeForLargeFileSplitting; } testOverFlow = (ulong)maxCabSizeForLargeFileInMB * 1024 * 1024; } catch (FormatException) { throw new WixException(WixErrors.IllegalEnvironmentVariable("WIX_MCSLFS", mcslfsString)); } catch (OverflowException) { throw new WixException(WixErrors.MaximumCabinetSizeForLargeFileSplittingTooLarge(null, maxCabSizeForLargeFileInMB, MaxValueOfMaxCabSizeForLargeFileSplitting)); } try { // Override authored mums value if environment variable is authored. if (!String.IsNullOrEmpty(mumsString)) { maxPreCompressedSizeInMB = Int32.Parse(mumsString); } else { maxPreCompressedSizeInMB = mediaTemplateRow.MaximumUncompressedMediaSize; } testOverFlow = (ulong)maxPreCompressedSizeInMB * 1024 * 1024; } catch (FormatException) { throw new WixException(WixErrors.IllegalEnvironmentVariable("WIX_MUMS", mumsString)); } catch (OverflowException) { throw new WixException(WixErrors.MaximumUncompressedMediaSizeTooLarge(null, maxPreCompressedSizeInMB)); } maxCabSizeForLargeFileSplitting = maxCabSizeForLargeFileInMB; maxUncompressedMediaSize = maxPreCompressedSizeInMB; } else { maxCabSizeForLargeFileSplitting = 0; maxUncompressedMediaSize = DefaultMaximumUncompressedMediaSize; } } } }