main
cs 505 lines 20.6 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.Burn.CommandLine
4 {
5 using System;
6 using System.Collections.Generic;
7 using System.IO;
8 using System.Linq;
9 using System.Threading;
10 using System.Threading.Tasks;
11 using System.Xml.Linq;
12 using WixToolset.Core.Burn.Bundles;
13 using WixToolset.Core.Burn.Interfaces;
14 using WixToolset.Core.Native;
15 using WixToolset.Data;
16 using WixToolset.Data.Symbols;
17 using WixToolset.Extensibility;
18 using WixToolset.Extensibility.Data;
19 using WixToolset.Extensibility.Services;
20
21 internal class RemotePayloadSubcommand : BurnSubcommandBase
22 {
23 private static readonly XName BundlePackageName = "BundlePackage";
24 private static readonly XName ExePackageName = "ExePackage";
25 private static readonly XName MsuPackageName = "MsuPackage";
26 private static readonly XName BundlePackagePayloadName = "BundlePackagePayload";
27 private static readonly XName ExePackagePayloadName = "ExePackagePayload";
28 private static readonly XName MsuPackagePayloadName = "MsuPackagePayload";
29 private static readonly XName PayloadName = "Payload";
30 private static readonly XName PayloadGroupName = "PayloadGroup";
31 private static readonly XName RemoteBundleName = "RemoteBundle";
32 private static readonly XName RemoteRelatedBundleName = "RemoteRelatedBundle";
33
34 public RemotePayloadSubcommand(IServiceProvider serviceProvider)
35 {
36 this.ServiceProvider = serviceProvider;
37 this.Messaging = serviceProvider.GetService<IMessaging>();
38 this.PayloadHarvester = serviceProvider.GetService<IPayloadHarvester>();
39 var extensionManager = serviceProvider.GetService<IExtensionManager>();
40
41 this.BackendExtensions = extensionManager.GetServices<IBurnBackendBinderExtension>();
42 }
43
44 private IServiceProvider ServiceProvider { get; }
45
46 private IMessaging Messaging { get; }
47
48 private IPayloadHarvester PayloadHarvester { get; }
49
50 private IReadOnlyCollection<IBurnBackendBinderExtension> BackendExtensions { get; }
51
52 private List<string> BasePaths { get; } = new List<string>();
53
54 private string DownloadUrl { get; set; }
55
56 private List<string> InputPaths { get; } = new List<string>();
57
58 private string IntermediateFolder { get; set; }
59
60 private string OutputPath { get; set; }
61
62 private WixBundlePackageType? PackageType { get; set; }
63
64 private BundlePackagePayloadGenerationType BundlePayloadGeneration { get; set; } = BundlePackagePayloadGenerationType.ExternalWithoutDownloadUrl;
65
66 private bool Recurse { get; set; }
67
68 private bool UseCertificate { get; set; }
69
70 public override CommandLineHelp GetCommandLineHelp()
71 {
72 return new CommandLineHelp("Generate source code for a remote payload.", "burn remotepayload [options] payloadfile [payloadfile ...]", new[]
73 {
74 new CommandLineHelpSwitch("-basepath", "-bp", "Folder as base to make payloads relative."),
75 new CommandLineHelpSwitch("-bundlepayloadgeneration", "Sets the package payload generation option; available types are: none, externalwithoutdownloadurl, external, all."),
76 new CommandLineHelpSwitch("-downloadurl", "-du", "Set the DownloadUrl attribute on the generated payloads."),
77 new CommandLineHelpSwitch("-out", "-o", "Path to output the source code file."),
78 new CommandLineHelpSwitch("-recurse", "-r", "Generate source code for all payloads in directory recursively."),
79 new CommandLineHelpSwitch("-intermediatefolder", "Optional working folder. If not specified %TMP% folder will be created."),
80 new CommandLineHelpSwitch("-packagetype", "Explicitly set package type; available types are: _bundle_, _exe_, _msu_."),
81 new CommandLineHelpSwitch("-usecertificate", "Use certificate to validate signed payloads. This option is not recommended."),
82 });
83 }
84
85 public override Task<int> ExecuteAsync(CancellationToken cancellationToken)
86 {
87 var inputPaths = this.ExpandInputPaths();
88 if (inputPaths.Count == 0)
89 {
90 this.Messaging.Write(ErrorMessages.FilePathRequired("a remote payload"));
91 }
92 else
93 {
94 // Reverse sort to ensure longest paths are matched first.
95 this.BasePaths.Sort();
96 this.BasePaths.Reverse();
97
98 if (String.IsNullOrEmpty(this.IntermediateFolder))
99 {
100 this.IntermediateFolder = Path.GetTempPath();
101 }
102
103 var element = this.HarvestPackageElement(inputPaths);
104
105 if (!this.Messaging.EncounteredError)
106 {
107 if (!String.IsNullOrEmpty(this.OutputPath))
108 {
109 var outputFolder = Path.GetDirectoryName(this.OutputPath);
110 Directory.CreateDirectory(outputFolder);
111
112 File.WriteAllText(this.OutputPath, element.ToString());
113 }
114 else
115 {
116 Console.WriteLine(element.ToString());
117 }
118 }
119 }
120
121 return Task.FromResult(this.Messaging.LastErrorNumber);
122 }
123
124 public override bool TryParseArgument(ICommandLineParser parser, string argument)
125 {
126 if (parser.IsSwitch(argument))
127 {
128 var parameter = argument.Substring(1);
129 switch (parameter.ToLowerInvariant())
130 {
131 case "bp":
132 case "basepath":
133 this.BasePaths.Add(parser.GetNextArgumentAsDirectoryOrError(argument));
134 return true;
135
136 case "bundlepayloadgeneration":
137 var bundlePayloadGenerationValue = parser.GetNextArgumentOrError(argument);
138 if (Enum.TryParse(bundlePayloadGenerationValue, ignoreCase: true, out BundlePackagePayloadGenerationType bundlePayloadGeneration))
139 {
140 this.BundlePayloadGeneration = bundlePayloadGeneration;
141 }
142 else if (!String.IsNullOrEmpty(bundlePayloadGenerationValue))
143 {
144 parser.ReportErrorArgument(argument, ErrorMessages.IllegalCommandLineArgumentValue(argument, bundlePayloadGenerationValue, Enum.GetNames(typeof(BundlePackagePayloadGenerationType)).Select(s => s.ToLowerInvariant())));
145 }
146
147 return true;
148
149 case "du":
150 case "downloadurl":
151 this.DownloadUrl = parser.GetNextArgumentOrError(argument);
152 return true;
153
154 case "intermediatefolder":
155 this.IntermediateFolder = parser.GetNextArgumentAsDirectoryOrError(argument);
156 return true;
157
158 case "packagetype":
159 var packageTypeValue = parser.GetNextArgumentOrError(argument);
160 if (Enum.TryParse(packageTypeValue, ignoreCase: true, out WixBundlePackageType packageType))
161 {
162 this.PackageType = packageType;
163 }
164 else if (!String.IsNullOrEmpty(packageTypeValue))
165 {
166 parser.ReportErrorArgument(argument, ErrorMessages.IllegalCommandLineArgumentValue(argument, packageTypeValue, Enum.GetNames(typeof(WixBundlePackageType)).Select(s => s.ToLowerInvariant())));
167 }
168
169 return true;
170
171 case "o":
172 case "out":
173 this.OutputPath = parser.GetNextArgumentAsFilePathOrError(argument, "output file");
174 return true;
175
176 case "r":
177 case "recurse":
178 this.Recurse = true;
179 return true;
180
181 case "usecertificate":
182 this.UseCertificate = true;
183 return true;
184 }
185 }
186 else
187 {
188 this.InputPaths.Add(argument);
189 return true;
190 }
191
192 return false;
193 }
194
195 private IReadOnlyCollection<string> ExpandInputPaths()
196 {
197 var result = new List<string>();
198
199 foreach (var inputPath in this.InputPaths)
200 {
201 var filename = Path.GetFileName(inputPath);
202 var folder = Path.GetDirectoryName(inputPath);
203
204 if (String.IsNullOrEmpty(folder))
205 {
206 folder = ".";
207 }
208
209 foreach (var path in Directory.EnumerateFiles(folder, filename, this.Recurse ? SearchOption.AllDirectories : SearchOption.TopDirectoryOnly))
210 {
211 result.Add(path);
212 }
213 }
214
215 return result.Distinct(StringComparer.OrdinalIgnoreCase).ToList();
216 }
217
218 private XElement HarvestPackageElement(IEnumerable<string> paths)
219 {
220 var harvestedFiles = this.HarvestRemotePayloads(paths).ToList();
221 var firstFile = harvestedFiles.FirstOrDefault();
222
223 if (firstFile == null)
224 {
225 return null;
226 }
227
228 var containerElement = firstFile.PackageElement;
229
230 if (containerElement == null)
231 {
232 containerElement = new XElement(PayloadGroupName);
233 }
234 else
235 {
236 var cacheId = CacheIdGenerator.GenerateRemoteCacheId(firstFile.HarvestedPackageSymbol, firstFile.PayloadSymbol);
237 if (cacheId != null)
238 {
239 containerElement.Add(new XAttribute("CacheId", cacheId));
240 }
241 }
242
243 containerElement.Add(harvestedFiles.Select(h => h.Element));
244
245 return containerElement;
246 }
247
248 private IEnumerable<HarvestedFile> HarvestRemotePayloads(IEnumerable<string> paths)
249 {
250 var first = true;
251 var hashes = this.GetCertificateHashes(paths);
252
253 foreach (var path in paths)
254 {
255 var harvestedFile = this.HarvestFile(path, first, hashes);
256 first = false;
257
258 if (harvestedFile == null)
259 {
260 continue;
261 }
262
263 yield return harvestedFile;
264
265 if (harvestedFile.PackagePayloads.Any())
266 {
267 var packageCertificateHashes = this.GetCertificateHashes(harvestedFile.PackagePayloads.Select(x => x.SourceFile.Path));
268
269 foreach (var payloadSymbol in harvestedFile.PackagePayloads)
270 {
271 var harvestedPackageFile = this.HarvestFile(payloadSymbol.SourceFile.Path, false, packageCertificateHashes);
272 yield return harvestedPackageFile;
273 }
274 }
275 }
276 }
277
278 private Dictionary<string, CertificateHashes> GetCertificateHashes(IEnumerable<string> paths)
279 {
280 var hashes = new Dictionary<string, CertificateHashes>();
281
282 if (this.UseCertificate)
283 {
284 hashes = CertificateHashes.Read(paths)
285 .Where(c => !String.IsNullOrEmpty(c.PublicKey) && !String.IsNullOrEmpty(c.Thumbprint) && c.Exception is null)
286 .ToDictionary(c => c.Path);
287 }
288
289 return hashes;
290 }
291
292 private HarvestedFile HarvestFile(string path, bool isPackage, Dictionary<string, CertificateHashes> certificateHashes)
293 {
294 XElement element;
295 WixBundlePackageType? packageType = null;
296
297 if (isPackage)
298 {
299 var extension = this.PackageType.HasValue ? this.PackageType.ToString() : Path.GetExtension(path);
300
301 switch (extension.ToUpperInvariant())
302 {
303 case "BUNDLE":
304 packageType = WixBundlePackageType.Bundle;
305 element = new XElement(BundlePackagePayloadName);
306 break;
307
308 case "EXE":
309 case ".EXE":
310 packageType = WixBundlePackageType.Exe;
311 element = new XElement(ExePackagePayloadName);
312 break;
313
314 case "MSU":
315 case ".MSU":
316 packageType = WixBundlePackageType.Msu;
317 element = new XElement(MsuPackagePayloadName);
318 break;
319
320 default:
321 this.Messaging.Write(BurnBackendErrors.UnsupportedRemotePackagePayload(extension, path));
322 return null;
323 }
324 }
325 else
326 {
327 element = new XElement(PayloadName);
328 }
329
330 var payloadSymbol = new WixBundlePayloadSymbol(null, new Identifier(AccessModifier.Section, "id"))
331 {
332 SourceFile = new IntermediateFieldPathValue { Path = path },
333 Name = this.GetRelativeFileName(path),
334 };
335
336 this.PayloadHarvester.HarvestStandardInformation(payloadSymbol);
337
338 element.Add(new XAttribute("Name", payloadSymbol.Name));
339
340 if (!String.IsNullOrEmpty(payloadSymbol.DisplayName))
341 {
342 element.Add(new XAttribute("ProductName", payloadSymbol.DisplayName));
343 }
344
345 if (!String.IsNullOrEmpty(payloadSymbol.Description))
346 {
347 element.Add(new XAttribute("Description", payloadSymbol.Description));
348 }
349
350 if (!String.IsNullOrEmpty(this.DownloadUrl))
351 {
352 element.Add(new XAttribute("DownloadUrl", this.DownloadUrl));
353 }
354
355 if (certificateHashes.TryGetValue(path, out var certificateHashForPath))
356 {
357 payloadSymbol.CertificatePublicKey = certificateHashForPath.PublicKey;
358 payloadSymbol.CertificateThumbprint = certificateHashForPath.Thumbprint;
359
360 element.Add(new XAttribute("CertificatePublicKey", payloadSymbol.CertificatePublicKey));
361 element.Add(new XAttribute("CertificateThumbprint", payloadSymbol.CertificateThumbprint));
362 }
363 else if (!String.IsNullOrEmpty(payloadSymbol.Hash))
364 {
365 element.Add(new XAttribute("Hash", payloadSymbol.Hash));
366 }
367
368 if (payloadSymbol.FileSize.HasValue)
369 {
370 element.Add(new XAttribute("Size", payloadSymbol.FileSize.Value));
371 }
372
373 if (!String.IsNullOrEmpty(payloadSymbol.Version))
374 {
375 element.Add(new XAttribute("Version", payloadSymbol.Version));
376 }
377
378 var harvestedFile = new HarvestedFile
379 {
380 Element = element,
381 PayloadSymbol = payloadSymbol,
382 };
383
384 switch (packageType)
385 {
386 case WixBundlePackageType.Bundle:
387 this.HarvestBundlePackage(harvestedFile);
388 break;
389 case WixBundlePackageType.Exe:
390 this.HarvestExePackage(harvestedFile);
391 break;
392 case WixBundlePackageType.Msu:
393 this.HarvestMsuPackage(harvestedFile);
394 break;
395 }
396
397 return harvestedFile;
398 }
399
400 private string GetRelativeFileName(string path)
401 {
402 foreach (var basePath in this.BasePaths)
403 {
404 if (path.StartsWith(basePath, StringComparison.OrdinalIgnoreCase))
405 {
406 return path.Substring(basePath.Length).TrimStart(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar);
407 }
408 }
409
410 return Path.GetFileName(path);
411 }
412
413 private void HarvestBundlePackage(HarvestedFile harvestedFile)
414 {
415 var packagePayloadSymbol = new WixBundleBundlePackagePayloadSymbol(null, new Identifier(AccessModifier.Section, harvestedFile.PayloadSymbol.Id.Id))
416 {
417 PayloadGeneration = this.BundlePayloadGeneration,
418 };
419
420 var command = new HarvestBundlePackageCommand(this.ServiceProvider, this.BackendExtensions, this.IntermediateFolder, harvestedFile.PayloadSymbol, packagePayloadSymbol, new Dictionary<string, WixBundlePayloadSymbol>());
421 command.Execute();
422
423 if (!this.Messaging.EncounteredError)
424 {
425 var bundleElement = new XElement(RemoteBundleName);
426
427 bundleElement.Add(new XAttribute("BundleId", command.HarvestedBundlePackage.BundleId));
428
429 if (!String.IsNullOrEmpty(command.HarvestedBundlePackage.DisplayName))
430 {
431 bundleElement.Add(new XAttribute("DisplayName", command.HarvestedBundlePackage.DisplayName));
432 }
433
434 if (!String.IsNullOrEmpty(command.HarvestedBundlePackage.EngineVersion))
435 {
436 bundleElement.Add(new XAttribute("EngineVersion", command.HarvestedBundlePackage.EngineVersion));
437 }
438
439 bundleElement.Add(new XAttribute("InstallSize", command.HarvestedBundlePackage.InstallSize));
440 bundleElement.Add(new XAttribute("ManifestNamespace", command.HarvestedBundlePackage.ManifestNamespace));
441 bundleElement.Add(new XAttribute("PerMachine", command.HarvestedBundlePackage.PerMachine ? "yes" : "no"));
442 bundleElement.Add(new XAttribute("ProviderKey", command.HarvestedDependencyProvider.ProviderKey));
443 bundleElement.Add(new XAttribute("ProtocolVersion", command.HarvestedBundlePackage.ProtocolVersion));
444
445 if (!String.IsNullOrEmpty(command.HarvestedBundlePackage.Version))
446 {
447 bundleElement.Add(new XAttribute("Version", command.HarvestedBundlePackage.Version));
448 }
449
450 bundleElement.Add(new XAttribute("Win64", command.HarvestedBundlePackage.Win64 ? "yes" : "no"));
451
452 var setUpgradeCode = false;
453 foreach (var relatedBundle in command.RelatedBundles)
454 {
455 if (!setUpgradeCode && relatedBundle.Action == RelatedBundleActionType.Upgrade)
456 {
457 setUpgradeCode = true;
458 bundleElement.Add(new XAttribute("UpgradeCode", relatedBundle.BundleId));
459 continue;
460 }
461
462 var relatedBundleElement = new XElement(RemoteRelatedBundleName);
463
464 relatedBundleElement.Add(new XAttribute("Id", relatedBundle.BundleId));
465 relatedBundleElement.Add(new XAttribute("Action", relatedBundle.Action.ToString()));
466
467 bundleElement.Add(relatedBundleElement);
468 }
469
470 harvestedFile.PackagePayloads.AddRange(command.Payloads);
471 harvestedFile.HarvestedPackageSymbol = command.HarvestedBundlePackage;
472 harvestedFile.Element.Add(bundleElement);
473
474 harvestedFile.PackageElement = new XElement(BundlePackageName);
475 if (BurnCommon.BurnV3Namespace == command.HarvestedBundlePackage.ManifestNamespace)
476 {
477 harvestedFile.PackageElement.Add(new XAttribute("Visible", "yes"));
478 }
479 }
480 }
481
482 private void HarvestExePackage(HarvestedFile harvestedFile)
483 {
484 harvestedFile.PackageElement = new XElement(ExePackageName);
485 }
486
487 private void HarvestMsuPackage(HarvestedFile harvestedFile)
488 {
489 harvestedFile.PackageElement = new XElement(MsuPackageName);
490 }
491
492 private class HarvestedFile
493 {
494 public XElement Element { get; set; }
495
496 public XElement PackageElement { get; set; }
497
498 public IntermediateSymbol HarvestedPackageSymbol { get; set; }
499
500 public WixBundlePayloadSymbol PayloadSymbol { get; set; }
501
502 public List<WixBundlePayloadSymbol> PackagePayloads { get; } = new List<WixBundlePayloadSymbol>();
503 }
504 }
505 }