main
cs 462 lines 22 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.Concurrent;
7 using System.Collections.Generic;
8 using System.ComponentModel;
9 using System.Globalization;
10 using System.IO;
11 using System.Linq;
12 using System.Threading;
13 using System.Threading.Tasks;
14 using WixToolset.Core.Native.Msi;
15 using WixToolset.Data;
16 using WixToolset.Data.Symbols;
17 using WixToolset.Extensibility.Data;
18 using WixToolset.Extensibility.Services;
19
20 /// <summary>
21 /// Update file information.
22 /// </summary>
23 internal class UpdateFileFacadesCommand
24 {
25 public UpdateFileFacadesCommand(IMessaging messaging, IFileSystem fileSystem, IntermediateSection section, IEnumerable<IFileFacade> allFileFacades, IEnumerable<IFileFacade> updateFileFacades, IDictionary<string, string> variableCache, bool overwriteHash, CancellationToken cancellationToken, int threadCount)
26 {
27 this.Messaging = messaging;
28 this.FileSystem = fileSystem;
29 this.Section = section;
30 this.AllFileFacades = allFileFacades;
31 this.UpdateFileFacades = updateFileFacades;
32 this.VariableCache = variableCache;
33 this.OverwriteHash = overwriteHash;
34 this.CancellationToken = cancellationToken;
35 this.ThreadCount = threadCount;
36 }
37
38 private IMessaging Messaging { get; }
39
40 private IFileSystem FileSystem { get; }
41
42 private IntermediateSection Section { get; }
43
44 private IEnumerable<IFileFacade> AllFileFacades { get; }
45
46 private IEnumerable<IFileFacade> UpdateFileFacades { get; }
47
48 private bool OverwriteHash { get; }
49
50 private IDictionary<string, string> VariableCache { get; }
51
52 private CancellationToken CancellationToken { get; }
53
54 private int ThreadCount { get; }
55
56 public void Execute()
57 {
58 try
59 {
60 this.UpdateFileFacadesInParallel(this.UpdateFileFacades.Where(f => f.SourcePath != null));
61 }
62 catch (AggregateException ae)
63 {
64 foreach (var ex in ae.Flatten().InnerExceptions)
65 {
66 throw ex;
67 }
68 }
69 }
70
71 private void UpdateFileFacadesInParallel(IEnumerable<IFileFacade> facades)
72 {
73 var mut = new Mutex();
74 var exceptions = new ConcurrentQueue<Exception>();
75
76 var assemblySymbols = this.Section.Symbols.OfType<AssemblySymbol>().ToDictionary(t => t.Id.Id);
77
78 Parallel.ForEach(facades,
79 new ParallelOptions{
80 CancellationToken = this.CancellationToken,
81 MaxDegreeOfParallelism = this.ThreadCount
82 },
83 () =>
84 {
85 return new LocalData(
86 this.Messaging,
87 this.FileSystem,
88 this.AllFileFacades,
89 this.OverwriteHash,
90 this.Section.Symbols.OfType<MsiAssemblyNameSymbol>().ToDictionary(t => t.Id.Id),
91 useVariableCache: null != this.VariableCache
92 );
93 },
94 (file, loopstate, local) =>
95 {
96 try
97 {
98 local.UpdateFileFacade(file, assemblySymbols);
99 }
100 catch (Exception ex)
101 {
102 exceptions.Enqueue(ex);
103 loopstate.Stop();
104 }
105
106 return local;
107 },
108 (local) =>
109 {
110 // Merge local variable cache back into common variable cache
111 if (null != this.VariableCache)
112 {
113 mut.WaitOne();
114 try
115 {
116 local.MergeVariableCacheWith(this.VariableCache);
117 }
118 finally
119 {
120 mut.ReleaseMutex();
121 }
122 }
123 }
124 );
125
126 if (!exceptions.IsEmpty)
127 {
128 throw new AggregateException(exceptions);
129 }
130 }
131
132
133 internal class LocalData
134 {
135 public LocalData(IMessaging messaging, IFileSystem fileSystem, IEnumerable<IFileFacade> allFileFacades, bool overwriteHash, Dictionary<string, MsiAssemblyNameSymbol> assemblyNameSymbols, bool useVariableCache)
136 {
137 this.Messaging = messaging;
138 this.FileSystem = fileSystem;
139 this.AllFileFacades = allFileFacades;
140 this.OverwriteHash = overwriteHash;
141 this.AssemblyNameSymbols = assemblyNameSymbols;
142 this.VariableCache = useVariableCache ? new Dictionary<string, string>() : null;
143 }
144
145 private IMessaging Messaging { get; }
146
147 private IFileSystem FileSystem { get; }
148
149 private IEnumerable<IFileFacade> AllFileFacades { get; }
150
151 private bool OverwriteHash { get; }
152
153 private Dictionary<string, MsiAssemblyNameSymbol> AssemblyNameSymbols { get; }
154
155 private Dictionary<string, string> VariableCache { get; }
156
157 public void MergeVariableCacheWith(IDictionary<string, string> variableCache)
158 {
159 if (null != variableCache && null != this.VariableCache)
160 {
161 foreach (var v in this.VariableCache)
162 {
163 variableCache[v.Key] = v.Value;
164 }
165 }
166 }
167
168 public void UpdateFileFacade(IFileFacade facade, Dictionary<string, AssemblySymbol> assemblySymbols)
169 {
170 FileInfo fileInfo = null;
171 try
172 {
173 fileInfo = new FileInfo(facade.SourcePath);
174 }
175 catch (ArgumentException)
176 {
177 this.Messaging.Write(ErrorMessages.InvalidFileName(facade.SourceLineNumber, facade.SourcePath));
178 return;
179 }
180 catch (PathTooLongException)
181 {
182 this.Messaging.Write(ErrorMessages.InvalidFileName(facade.SourceLineNumber, facade.SourcePath));
183 return;
184 }
185 catch (NotSupportedException)
186 {
187 this.Messaging.Write(ErrorMessages.InvalidFileName(facade.SourceLineNumber, facade.SourcePath));
188 return;
189 }
190
191 if (!fileInfo.Exists)
192 {
193 this.Messaging.Write(ErrorMessages.CannotFindFile(facade.SourceLineNumber, facade.Id, facade.FileName, facade.SourcePath));
194 return;
195 }
196
197 using (var fileStream = this.FileSystem.OpenFile(facade.SourceLineNumber, fileInfo.FullName, FileMode.Open, FileAccess.Read, FileShare.Read))
198 {
199 if (Int32.MaxValue < fileStream.Length)
200 {
201 throw new WixException(ErrorMessages.FileTooLarge(facade.SourceLineNumber, facade.SourcePath));
202 }
203
204 facade.FileSize = Convert.ToInt32(fileStream.Length, CultureInfo.InvariantCulture);
205 }
206
207 string version = null;
208 string language = null;
209 try
210 {
211 Installer.GetFileVersion(fileInfo.FullName, out version, out language);
212 }
213 catch (Win32Exception e)
214 {
215 if (0x2 == e.NativeErrorCode) // ERROR_FILE_NOT_FOUND
216 {
217 throw new WixException(ErrorMessages.FileNotFound(facade.SourceLineNumber, fileInfo.FullName));
218 }
219 else
220 {
221 throw new WixException(ErrorMessages.Win32Exception(e.NativeErrorCode, e.Message));
222 }
223 }
224
225 // If there is no version, it is assumed there is no language because it won't matter in the versioning of the install.
226 if (String.IsNullOrEmpty(version)) // unversioned files have their hashes added to the MsiFileHash table
227 {
228 if (!this.OverwriteHash)
229 {
230 // not overwriting hash, so don't do the rest of these options.
231 }
232 else if (null != facade.Version)
233 {
234 // Search all of the file rows available to see if the specified version is actually a companion file. Yes, this looks
235 // very expensive and you're probably thinking it would be better to create an index of some sort to do an O(1) look up.
236 // That's a reasonable thought but companion file usage is usually pretty rare so we'd be doing something expensive (indexing
237 // all the file rows) for a relatively uncommon situation. Let's not do that.
238 //
239 // Also, if we do not find a matching file identifier then the user provided a default version and is providing a version
240 // for unversioned file. That's allowed but generally a dangerous thing to do so let's point that out to the user.
241 if (!this.AllFileFacades.Any(r => facade.Version.Equals(r.Id, StringComparison.Ordinal)))
242 {
243 this.Messaging.Write(WarningMessages.DefaultVersionUsedForUnversionedFile(facade.SourceLineNumber, facade.Version, facade.Id));
244 }
245 }
246 else
247 {
248 if (null != facade.Language)
249 {
250 this.Messaging.Write(WarningMessages.DefaultLanguageUsedForUnversionedFile(facade.SourceLineNumber, facade.Language, facade.Id));
251 }
252
253 int[] hash;
254 try
255 {
256 Installer.GetFileHash(fileInfo.FullName, 0, out hash);
257 }
258 catch (Win32Exception e)
259 {
260 if (0x2 == e.NativeErrorCode) // ERROR_FILE_NOT_FOUND
261 {
262 throw new WixException(ErrorMessages.FileNotFound(facade.SourceLineNumber, fileInfo.FullName));
263 }
264 else
265 {
266 throw new WixException(ErrorMessages.Win32Exception(e.NativeErrorCode, fileInfo.FullName, e.Message));
267 }
268 }
269
270 // Remember the hash symbol for use later.
271 facade.MsiFileHashSymbol = new MsiFileHashSymbol(facade.SourceLineNumber, facade.Identifier)
272 {
273 Options = 0,
274 HashPart1 = hash[0],
275 HashPart2 = hash[1],
276 HashPart3 = hash[2],
277 HashPart4 = hash[3],
278 };
279 }
280 }
281 else // update the file row with the version and language information.
282 {
283 // If no version was provided by the user, use the version from the file itself.
284 // This is the most common case.
285 if (String.IsNullOrEmpty(facade.Version))
286 {
287 facade.Version = version;
288 }
289 else if (!this.AllFileFacades.Any(r => facade.Version.Equals(r.Id, StringComparison.Ordinal))) // this looks expensive, but see explanation below.
290 {
291 // The user provided a default version for the file row so we looked for a companion file (a file row with Id matching
292 // the version value). We didn't find it so, we will override the default version they provided with the actual
293 // version from the file itself. Now, I know it looks expensive to search through all the file rows trying to match
294 // on the Id. However, the alternative is to build a big index of all file rows to do look ups. Since this case
295 // where the file version is already present is rare (companion files are pretty uncommon), we'll do the more
296 // CPU intensive search to save on the memory intensive index that wouldn't be used much.
297 //
298 // Also note this case can occur when the file is being updated using the WixBindUpdatedFiles extension mechanism.
299 // That's typically even more rare than companion files so again, no index, just search.
300 facade.Version = version;
301 }
302
303 if (!String.IsNullOrEmpty(facade.Language) && String.IsNullOrEmpty(language))
304 {
305 this.Messaging.Write(WarningMessages.DefaultLanguageUsedForVersionedFile(facade.SourceLineNumber, facade.Language, facade.Id));
306 }
307 else // override the default provided by the user (usually nothing) with the actual language from the file itself.
308 {
309 facade.Language = language;
310 }
311 }
312
313 // Populate the binder variables for this file information if requested.
314 if (null != this.VariableCache)
315 {
316 this.VariableCache[$"fileversion.{facade.Id}"] = facade.Version ?? String.Empty;
317 this.VariableCache[$"filelanguage.{facade.Id}"] = facade.Language ?? String.Empty;
318 }
319
320 // If there is an assembly for this file.
321 if (assemblySymbols.TryGetValue(facade.Id, out var assemblySymbol))
322 {
323 // If this is a CLR assembly, load the assembly and get the assembly name information
324 if (AssemblyType.DotNetAssembly == assemblySymbol.Type)
325 {
326 try
327 {
328 var assemblyName = AssemblyNameReader.ReadAssembly(this.FileSystem, facade.SourceLineNumber, fileInfo.FullName, version);
329
330 this.SetMsiAssemblyName(facade, assemblySymbol, "name", assemblyName.Name);
331 this.SetMsiAssemblyName(facade, assemblySymbol, "culture", assemblyName.Culture);
332 this.SetMsiAssemblyName(facade, assemblySymbol, "version", assemblyName.Version);
333
334 if (!String.IsNullOrEmpty(assemblyName.Architecture))
335 {
336 this.SetMsiAssemblyName(facade, assemblySymbol, "processorArchitecture", assemblyName.Architecture);
337 }
338 // TODO: WiX v3 seemed to do this but not clear it should actually be done.
339 //else if (!String.IsNullOrEmpty(file.WixFile.ProcessorArchitecture))
340 //{
341 // this.SetMsiAssemblyName(assemblyNameSymbols, file, "processorArchitecture", file.WixFile.ProcessorArchitecture);
342 //}
343
344 if (assemblyName.StrongNamedSigned)
345 {
346 this.SetMsiAssemblyName(facade, assemblySymbol, "publicKeyToken", assemblyName.PublicKeyToken);
347 }
348 else if (assemblySymbol.ApplicationFileRef == null)
349 {
350 throw new WixException(ErrorMessages.GacAssemblyNoStrongName(facade.SourceLineNumber, fileInfo.FullName, facade.ComponentRef));
351 }
352
353 if (!String.IsNullOrEmpty(assemblyName.FileVersion))
354 {
355 this.SetMsiAssemblyName(facade, assemblySymbol, "fileVersion", assemblyName.FileVersion);
356 }
357
358 // add the assembly name to the information cache
359 if (null != this.VariableCache)
360 {
361 this.VariableCache[$"assemblyfullname.{facade.Id}"] = assemblyName.GetFullName();
362 }
363 }
364 catch (WixException e)
365 {
366 this.Messaging.Write(e.Error);
367 }
368 }
369 else if (AssemblyType.Win32Assembly == assemblySymbol.Type)
370 {
371 // TODO: Consider passing in the this.AllFileFacades as an indexed collection instead of searching through
372 // all files like this. Even though this is a rare case it looks like we might be able to index the
373 // file earlier.
374 var fileManifest = this.AllFileFacades.FirstOrDefault(r => r.Id.Equals(assemblySymbol.ManifestFileRef, StringComparison.Ordinal));
375 if (null == fileManifest)
376 {
377 this.Messaging.Write(ErrorMessages.MissingManifestForWin32Assembly(facade.SourceLineNumber, facade.Id, assemblySymbol.ManifestFileRef));
378 }
379
380 try
381 {
382 var assemblyName = AssemblyNameReader.ReadAssemblyManifest(facade.SourceLineNumber, fileManifest.SourcePath);
383
384 if (!String.IsNullOrEmpty(assemblyName.Name))
385 {
386 this.SetMsiAssemblyName(facade, assemblySymbol, "name", assemblyName.Name);
387 }
388
389 if (!String.IsNullOrEmpty(assemblyName.Version))
390 {
391 this.SetMsiAssemblyName(facade, assemblySymbol, "version", assemblyName.Version);
392 }
393
394 if (!String.IsNullOrEmpty(assemblyName.Type))
395 {
396 this.SetMsiAssemblyName(facade, assemblySymbol, "type", assemblyName.Type);
397 }
398
399 if (!String.IsNullOrEmpty(assemblyName.Architecture))
400 {
401 this.SetMsiAssemblyName(facade, assemblySymbol, "processorArchitecture", assemblyName.Architecture);
402 }
403
404 if (!String.IsNullOrEmpty(assemblyName.PublicKeyToken))
405 {
406 this.SetMsiAssemblyName(facade, assemblySymbol, "publicKeyToken", assemblyName.PublicKeyToken);
407 }
408 }
409 catch (WixException e)
410 {
411 this.Messaging.Write(e.Error);
412 }
413 }
414 }
415 }
416
417 private void SetMsiAssemblyName(IFileFacade facade, AssemblySymbol assemblySymbol, string name, string value)
418 {
419 // check for null value (this can occur when grabbing the file version from an assembly without one)
420 if (String.IsNullOrEmpty(value))
421 {
422 this.Messaging.Write(WarningMessages.NullMsiAssemblyNameValue(facade.SourceLineNumber, facade.ComponentRef, name));
423 }
424 else
425 {
426 // if the assembly will be GAC'd and the name in the file table doesn't match the name in the MsiAssemblyName table, error because the install will fail.
427 if ("name" == name && AssemblyType.DotNetAssembly == assemblySymbol.Type &&
428 String.IsNullOrEmpty(assemblySymbol.ApplicationFileRef) &&
429 !String.Equals(Path.GetFileNameWithoutExtension(facade.FileName), value, StringComparison.OrdinalIgnoreCase))
430 {
431 this.Messaging.Write(ErrorMessages.GACAssemblyIdentityWarning(facade.SourceLineNumber, Path.GetFileNameWithoutExtension(facade.FileName), value));
432 }
433
434 // Override directly authored value, otherwise remember the gathered information on the facade for use later.
435 var lookup = String.Concat(facade.ComponentRef, "/", name);
436 if (this.AssemblyNameSymbols.TryGetValue(lookup, out var assemblyNameSymbol))
437 {
438 assemblyNameSymbol.Value = value;
439 }
440 else
441 {
442 assemblyNameSymbol = new MsiAssemblyNameSymbol(assemblySymbol.SourceLineNumbers, new Identifier(AccessModifier.Section, facade.ComponentRef, name))
443 {
444 ComponentRef = facade.ComponentRef,
445 Name = name,
446 Value = value,
447 };
448
449 facade.AssemblyNameSymbols.Add(assemblyNameSymbol);
450 this.AssemblyNameSymbols.Add(assemblyNameSymbol.Id.Id, assemblyNameSymbol);
451 }
452
453 if (this.VariableCache != null)
454 {
455 var key = String.Format(CultureInfo.InvariantCulture, "assembly{0}.{1}", name, facade.Id).ToLowerInvariant();
456 this.VariableCache[key] = value;
457 }
458 }
459 }
460 }
461 }
462 }