main
cs 229 lines 9.02 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.ComponentModel;
8 using System.IO;
9 using System.Linq;
10 using System.Threading;
11 using WixToolset.Core.Native;
12 using WixToolset.Data;
13 using WixToolset.Extensibility.Services;
14
15 /// <summary>
16 /// Builds cabinets using multiple threads. This implements a thread pool that generates cabinets with multiple
17 /// threads. Unlike System.Threading.ThreadPool, it waits until all threads are finished.
18 /// </summary>
19 internal sealed class CabinetBuilder
20 {
21 private readonly Queue<CabinetWorkItem> cabinetWorkItems;
22 private readonly List<CompletedCabinetWorkItem> completedCabinets;
23
24 public CabinetBuilder(IMessaging messaging, int threadCount, int maximumCabinetSizeForLargeFileSplitting, int maximumUncompressedMediaSize)
25 {
26 if (0 >= threadCount)
27 {
28 throw new ArgumentOutOfRangeException(nameof(threadCount));
29 }
30
31 this.cabinetWorkItems = new Queue<CabinetWorkItem>();
32 this.completedCabinets = new List<CompletedCabinetWorkItem>();
33
34 this.Messaging = messaging;
35 this.ThreadCount = threadCount;
36 this.MaximumCabinetSizeForLargeFileSplitting = maximumCabinetSizeForLargeFileSplitting;
37 this.MaximumUncompressedMediaSize = maximumUncompressedMediaSize;
38 }
39
40 private IMessaging Messaging { get; }
41
42 private int ThreadCount { get; }
43
44 private int MaximumCabinetSizeForLargeFileSplitting { get; }
45
46 private int MaximumUncompressedMediaSize { get; }
47
48 public IReadOnlyCollection<CompletedCabinetWorkItem> CompletedCabinets => this.completedCabinets;
49
50 /// <summary>
51 /// Enqueues a CabinetWorkItem to the queue.
52 /// </summary>
53 /// <param name="cabinetWorkItem">cabinet work item</param>
54 public void Enqueue(CabinetWorkItem cabinetWorkItem)
55 {
56 this.cabinetWorkItems.Enqueue(cabinetWorkItem);
57 }
58
59 /// <summary>
60 /// Create the queued cabinets.
61 /// </summary>
62 /// <returns>error message number (zero if no error)</returns>
63 public void CreateQueuedCabinets()
64 {
65 if (this.cabinetWorkItems.Count == 0)
66 {
67 return;
68 }
69
70 var cabinetFolders = this.cabinetWorkItems.Select(c => Path.GetDirectoryName(c.CabinetFile)).Distinct(StringComparer.OrdinalIgnoreCase);
71
72 foreach (var folder in cabinetFolders)
73 {
74 Directory.CreateDirectory(folder);
75 }
76
77 // don't create more threads than the number of cabinets to build
78 var numberOfThreads = Math.Min(this.ThreadCount, this.cabinetWorkItems.Count);
79
80 if (0 < numberOfThreads)
81 {
82 var threads = new Thread[numberOfThreads];
83
84 for (var i = 0; i < threads.Length; i++)
85 {
86 threads[i] = new Thread(new ThreadStart(this.ProcessWorkItems));
87 threads[i].Start();
88 }
89
90 // wait for all threads to finish
91 foreach (var thread in threads)
92 {
93 thread.Join();
94 }
95 }
96 }
97
98 /// <summary>
99 /// This function gets called by multiple threads to do actual work.
100 /// It takes one work item at a time and calls this.CreateCabinet().
101 /// It does not return until cabinetWorkItems queue is empty
102 /// </summary>
103 private void ProcessWorkItems()
104 {
105 try
106 {
107 while (true)
108 {
109 CabinetWorkItem cabinetWorkItem;
110
111 lock (this.cabinetWorkItems)
112 {
113 // check if there are any more cabinets to create
114 if (0 == this.cabinetWorkItems.Count)
115 {
116 break;
117 }
118
119 cabinetWorkItem = this.cabinetWorkItems.Dequeue();
120 }
121
122 // Create a cabinet.
123 var created = this.CreateCabinet(cabinetWorkItem);
124
125 // Update the cabinet work item to report back what cabinets were created.
126 if (created?.Any() == true)
127 {
128 lock (this.completedCabinets)
129 {
130 this.completedCabinets.Add(new CompletedCabinetWorkItem(cabinetWorkItem.DiskId, created));
131 }
132 }
133 }
134 }
135 catch (WixException we)
136 {
137 this.Messaging.Write(we.Error);
138 }
139 catch (Exception e)
140 {
141 this.Messaging.Write(ErrorMessages.UnexpectedException(e));
142 }
143 }
144
145 /// <summary>
146 /// Creates a cabinet using the wixcab.dll interop layer.
147 /// </summary>
148 /// <param name="cabinetWorkItem">CabinetWorkItem containing information about the cabinet to create.</param>
149 private IReadOnlyCollection<CabinetCreated> CreateCabinet(CabinetWorkItem cabinetWorkItem)
150 {
151 this.Messaging.Write(VerboseMessages.CreateCabinet(cabinetWorkItem.CabinetFile));
152
153 var maxCabinetSize = 0; // The value of 0 corresponds to default of 2GB which means no cabinet splitting
154 ulong maxPreCompressedSizeInBytes = 0;
155
156 if (this.MaximumCabinetSizeForLargeFileSplitting != 0)
157 {
158 // User Specified Max Cab Size for File Splitting, So Check if this cabinet has a single file larger than MaximumUncompressedFileSize
159 // If a file is larger than MaximumUncompressedFileSize, then the cabinet containing it will have only this file
160 if (1 == cabinetWorkItem.FileFacades.Count())
161 {
162 // Cabinet has Single File, Check if this is Large File than needs Splitting into Multiple cabs
163 // Get the Value for Max Uncompressed Media Size
164 maxPreCompressedSizeInBytes = (ulong)this.MaximumUncompressedMediaSize * 1024 * 1024;
165
166 var facade = cabinetWorkItem.FileFacades.First();
167
168 // If the file is larger than MaximumUncompressedFileSize set Maximum Cabinet Size for Cabinet Splitting
169 if ((ulong)facade.FileSize >= maxPreCompressedSizeInBytes)
170 {
171 maxCabinetSize = this.MaximumCabinetSizeForLargeFileSplitting;
172 }
173 }
174 }
175
176 // Calculate the files to be compressed into the cabinet.
177 var compressFiles = new List<CabinetCompressFile>();
178
179 foreach (var facade in cabinetWorkItem.FileFacades.OrderBy(f => f.Sequence))
180 {
181 var modularizedId = facade.Id + cabinetWorkItem.ModularizationSuffix;
182
183 var compressFile = cabinetWorkItem.HashesByFileId.TryGetValue(facade.Id, out var hash) ?
184 new CabinetCompressFile(facade.SourcePath, modularizedId, hash.HashPart1, hash.HashPart2, hash.HashPart3, hash.HashPart4) :
185 new CabinetCompressFile(facade.SourcePath, modularizedId);
186
187 compressFiles.Add(compressFile);
188 }
189
190 // create the cabinet file
191 var cabinetPath = Path.GetFullPath(cabinetWorkItem.CabinetFile);
192 var cab = new Cabinet(cabinetPath);
193
194 try
195 {
196 var created = cab.Compress(compressFiles, cabinetWorkItem.CompressionLevel, maxCabinetSize, cabinetWorkItem.MaxThreshold);
197
198 // Best effort check to see if the cabinet is too large for the Windows Installer.
199 try
200 {
201 var fi = new FileInfo(cabinetPath);
202 if (fi.Length > Int32.MaxValue)
203 {
204 this.Messaging.Write(WarningMessages.WindowsInstallerFileTooLarge(cabinetWorkItem.SourceLineNumber, cabinetPath, "cabinet"));
205 }
206 }
207 catch
208 {
209 }
210
211 return created;
212 }
213 catch (Exception e) when (e.InnerException is Win32Exception win32Exception)
214 {
215 switch (win32Exception.NativeErrorCode)
216 {
217 case 0x4005:
218 this.Messaging.Write(ErrorMessages.CreateCabAddFileFailed());
219 return null;
220 case 0x0070:
221 this.Messaging.Write(ErrorMessages.CreateCabInsufficientDiskSpace());
222 return null;
223 default:
224 throw;
225 }
226 }
227 }
228 }
229 }