// 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.Bind
{
using System;
using System.Collections;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading;
using WixToolset.Core.Bind;
using WixToolset.Core.Native;
using WixToolset.Data;
///
/// Builds cabinets using multiple threads. This implements a thread pool that generates cabinets with multiple
/// threads. Unlike System.Threading.ThreadPool, it waits until all threads are finished.
///
internal sealed class CabinetBuilder
{
private Queue cabinetWorkItems;
private object lockObject;
private int threadCount;
// Address of Binder's callback function for Cabinet Splitting
private IntPtr newCabNamesCallBackAddress;
public int MaximumCabinetSizeForLargeFileSplitting { get; set; }
public int MaximumUncompressedMediaSize { get; set; }
///
/// Instantiate a new CabinetBuilder.
///
/// number of threads to use
/// Address of Binder's callback function for Cabinet Splitting
public CabinetBuilder(int threadCount, IntPtr newCabNamesCallBackAddress)
{
if (0 >= threadCount)
{
throw new ArgumentOutOfRangeException("threadCount");
}
this.cabinetWorkItems = new Queue();
this.lockObject = new object();
this.threadCount = threadCount;
// Set Address of Binder's callback function for Cabinet Splitting
this.newCabNamesCallBackAddress = newCabNamesCallBackAddress;
}
///
/// Enqueues a CabinetWorkItem to the queue.
///
/// cabinet work item
public void Enqueue(CabinetWorkItem cabinetWorkItem)
{
this.cabinetWorkItems.Enqueue(cabinetWorkItem);
}
///
/// Create the queued cabinets.
///
/// error message number (zero if no error)
public void CreateQueuedCabinets()
{
// don't create more threads than the number of cabinets to build
if (this.cabinetWorkItems.Count < this.threadCount)
{
this.threadCount = this.cabinetWorkItems.Count;
}
if (0 < this.threadCount)
{
Thread[] threads = new Thread[this.threadCount];
for (int i = 0; i < threads.Length; i++)
{
threads[i] = new Thread(new ThreadStart(this.ProcessWorkItems));
threads[i].Start();
}
// wait for all threads to finish
foreach (Thread thread in threads)
{
thread.Join();
}
}
}
///
/// This function gets called by multiple threads to do actual work.
/// It takes one work item at a time and calls this.CreateCabinet().
/// It does not return until cabinetWorkItems queue is empty
///
private void ProcessWorkItems()
{
try
{
while (true)
{
CabinetWorkItem cabinetWorkItem;
lock (this.cabinetWorkItems)
{
// check if there are any more cabinets to create
if (0 == this.cabinetWorkItems.Count)
{
break;
}
cabinetWorkItem = (CabinetWorkItem)this.cabinetWorkItems.Dequeue();
}
// create a cabinet
this.CreateCabinet(cabinetWorkItem);
}
}
catch (WixException we)
{
Messaging.Instance.OnMessage(we.Error);
}
catch (Exception e)
{
Messaging.Instance.OnMessage(WixErrors.UnexpectedException(e.Message, e.GetType().ToString(), e.StackTrace));
}
}
///
/// Creates a cabinet using the wixcab.dll interop layer.
///
/// CabinetWorkItem containing information about the cabinet to create.
private void CreateCabinet(CabinetWorkItem cabinetWorkItem)
{
Messaging.Instance.OnMessage(WixVerboses.CreateCabinet(cabinetWorkItem.CabinetFile));
int maxCabinetSize = 0; // The value of 0 corresponds to default of 2GB which means no cabinet splitting
ulong maxPreCompressedSizeInBytes = 0;
if (this.MaximumCabinetSizeForLargeFileSplitting != 0)
{
// User Specified Max Cab Size for File Splitting, So Check if this cabinet has a single file larger than MaximumUncompressedFileSize
// If a file is larger than MaximumUncompressedFileSize, then the cabinet containing it will have only this file
if (1 == cabinetWorkItem.FileFacades.Count())
{
// Cabinet has Single File, Check if this is Large File than needs Splitting into Multiple cabs
// Get the Value for Max Uncompressed Media Size
maxPreCompressedSizeInBytes = (ulong)MaximumUncompressedMediaSize * 1024 * 1024;
foreach (FileFacade facade in cabinetWorkItem.FileFacades) // No other easy way than looping to get the only row
{
if ((ulong)facade.File.FileSize >= maxPreCompressedSizeInBytes)
{
// If file is larger than MaximumUncompressedFileSize set Maximum Cabinet Size for Cabinet Splitting
maxCabinetSize = this.MaximumCabinetSizeForLargeFileSplitting;
}
}
}
}
// create the cabinet file
var cabinetPath = Path.GetFullPath(cabinetWorkItem.CabinetFile);
string cabinetFileName = Path.GetFileName(cabinetWorkItem.CabinetFile);
string cabinetDirectory = Path.GetDirectoryName(cabinetWorkItem.CabinetFile);
var files = cabinetWorkItem.FileFacades
.Select(facade => facade.Hash == null ?
new CabinetCompressFile(facade.WixFile.Source.Path, facade.File.File) :
new CabinetCompressFile(facade.WixFile.Source.Path, facade.File.File, facade.Hash.HashPart1, facade.Hash.HashPart2, facade.Hash.HashPart3, facade.Hash.HashPart4))
.ToList();
var cabinetCompressionLevel = (CabinetCompressionLevel)cabinetWorkItem.CompressionLevel;
var cab = new Cabinet(cabinetPath);
cab.Compress(files, cabinetCompressionLevel, maxCabinetSize, cabinetWorkItem.MaxThreshold);
// TODO: Handle newCabNamesCallBackAddress from compression.
}
}
}