main
cs 698 lines 25.5 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.WixBA
4 {
5 using System;
6 using System.Collections.Generic;
7 using System.ComponentModel;
8 using System.Linq;
9 using System.Reflection;
10 using System.Windows;
11 using System.Windows.Input;
12 using IO = System.IO;
13 using WixToolset.BootstrapperApplicationApi;
14
15 /// <summary>
16 /// The states of detection.
17 /// </summary>
18 public enum DetectionState
19 {
20 Absent,
21 Present,
22 }
23
24 /// <summary>
25 /// The states of upgrade detection.
26 /// </summary>
27 public enum UpgradeDetectionState
28 {
29 // There are no Upgrade related bundles installed.
30 None,
31 // All Upgrade related bundles that are installed are older than or the same version as this bundle.
32 Older,
33 // At least one Upgrade related bundle is installed that is newer than this bundle.
34 Newer,
35 }
36
37 /// <summary>
38 /// The states of installation.
39 /// </summary>
40 public enum InstallationState
41 {
42 Initializing,
43 Detecting,
44 Waiting,
45 Planning,
46 Applying,
47 Applied,
48 Failed,
49 }
50
51 /// <summary>
52 /// The model of the installation view in WixBA.
53 /// </summary>
54 public class InstallationViewModel : PropertyNotifyBase
55 {
56 private readonly RootViewModel root;
57
58 private readonly Dictionary<string, int> downloadRetries;
59 private bool downgrade;
60 private string downgradeMessage;
61
62 private ICommand licenseCommand;
63 private ICommand launchHomePageCommand;
64 private ICommand launchNewsCommand;
65 private ICommand launchVSExtensionPageCommand;
66 private ICommand installCommand;
67 private ICommand repairCommand;
68 private ICommand uninstallCommand;
69 private ICommand openLogCommand;
70 private ICommand openLogFolderCommand;
71 private ICommand tryAgainCommand;
72
73 private string message;
74 private DateTime cachePackageStart;
75 private DateTime executePackageStart;
76
77 /// <summary>
78 /// Creates a new model of the installation view.
79 /// </summary>
80 public InstallationViewModel(RootViewModel root)
81 {
82 this.root = root;
83 this.downloadRetries = new Dictionary<string, int>();
84
85 this.root.PropertyChanged += new System.ComponentModel.PropertyChangedEventHandler(this.RootPropertyChanged);
86
87 WixBA.Model.Bootstrapper.DetectBegin += this.DetectBegin;
88 WixBA.Model.Bootstrapper.DetectRelatedBundle += this.DetectedRelatedBundle;
89 WixBA.Model.Bootstrapper.DetectComplete += this.DetectComplete;
90 WixBA.Model.Bootstrapper.PlanPackageBegin += this.PlanPackageBegin;
91 WixBA.Model.Bootstrapper.PlanComplete += this.PlanComplete;
92 WixBA.Model.Bootstrapper.ApplyBegin += this.ApplyBegin;
93 WixBA.Model.Bootstrapper.CacheAcquireBegin += this.CacheAcquireBegin;
94 WixBA.Model.Bootstrapper.CacheAcquireResolving += this.CacheAcquireResolving;
95 WixBA.Model.Bootstrapper.CacheAcquireComplete += this.CacheAcquireComplete;
96 WixBA.Model.Bootstrapper.ExecutePackageBegin += this.ExecutePackageBegin;
97 WixBA.Model.Bootstrapper.ExecutePackageComplete += this.ExecutePackageComplete;
98 WixBA.Model.Bootstrapper.Error += this.ExecuteError;
99 WixBA.Model.Bootstrapper.ApplyComplete += this.ApplyComplete;
100 }
101
102 void RootPropertyChanged(object sender, PropertyChangedEventArgs e)
103 {
104 if (("DetectState" == e.PropertyName) || ("UpgradeDetectState" == e.PropertyName) || ("InstallState" == e.PropertyName))
105 {
106 base.OnPropertyChanged("RepairEnabled");
107 base.OnPropertyChanged("InstallEnabled");
108 base.OnPropertyChanged("IsComplete");
109 base.OnPropertyChanged("IsSuccessfulCompletion");
110 base.OnPropertyChanged("IsFailedCompletion");
111 base.OnPropertyChanged("StatusText");
112 base.OnPropertyChanged("UninstallEnabled");
113 }
114 }
115
116 /// <summary>
117 /// Gets the version for the application.
118 /// </summary>
119 public string Version
120 {
121 get { return String.Concat("v", WixBA.Model.Version.ToString()); }
122 }
123
124 /// <summary>
125 /// The Publisher of this bundle.
126 /// </summary>
127 public string Publisher
128 {
129 get
130 {
131 string company = "[AssemblyCompany]";
132 return WixDistribution.ReplacePlaceholders(company, typeof(WixBA).Assembly);
133 }
134 }
135
136 /// <summary>
137 /// The Publisher of this bundle.
138 /// </summary>
139 public string SupportUrl
140 {
141 get
142 {
143 return WixDistribution.SupportUrl;
144 }
145 }
146 public string VSExtensionUrl
147 {
148 get
149 {
150 return WixDistribution.VSExtensionsLandingUrl;
151 }
152 }
153
154 public string Message
155 {
156 get
157 {
158 return this.message;
159 }
160
161 set
162 {
163 if (this.message != value)
164 {
165 this.message = value;
166 base.OnPropertyChanged("Message");
167 }
168 }
169 }
170
171 /// <summary>
172 /// Gets and sets whether the view model considers this install to be a downgrade.
173 /// </summary>
174 public bool Downgrade
175 {
176 get
177 {
178 return this.downgrade;
179 }
180
181 set
182 {
183 if (this.downgrade != value)
184 {
185 this.downgrade = value;
186 base.OnPropertyChanged("Downgrade");
187 }
188 }
189 }
190
191 public string DowngradeMessage
192 {
193 get
194 {
195 return this.downgradeMessage;
196 }
197 set
198 {
199 if (this.downgradeMessage != value)
200 {
201 this.downgradeMessage = value;
202 base.OnPropertyChanged("DowngradeMessage");
203 }
204 }
205 }
206
207 public ICommand LaunchHomePageCommand
208 {
209 get
210 {
211 if (this.launchHomePageCommand == null)
212 {
213 this.launchHomePageCommand = new RelayCommand(param => WixBA.LaunchUrl(this.SupportUrl), param => true);
214 }
215
216 return this.launchHomePageCommand;
217 }
218 }
219
220 public ICommand LaunchNewsCommand
221 {
222 get
223 {
224 if (this.launchNewsCommand == null)
225 {
226 this.launchNewsCommand = new RelayCommand(param => WixBA.LaunchUrl(WixDistribution.NewsUrl), param => true);
227 }
228
229 return this.launchNewsCommand;
230 }
231 }
232
233 public ICommand LaunchVSExtensionPageCommand
234 {
235 get
236 {
237 if (this.launchVSExtensionPageCommand == null)
238 {
239 this.launchVSExtensionPageCommand = new RelayCommand(param => WixBA.LaunchUrl(WixDistribution.VSExtensionsLandingUrl), param => true);
240 }
241
242 return this.launchVSExtensionPageCommand;
243 }
244 }
245
246 public ICommand LicenseCommand
247 {
248 get
249 {
250 if (this.licenseCommand == null)
251 {
252 this.licenseCommand = new RelayCommand(param => this.LaunchLicense(), param => true);
253 }
254
255 return this.licenseCommand;
256 }
257 }
258
259 public bool LicenseEnabled
260 {
261 get { return this.LicenseCommand.CanExecute(this); }
262 }
263
264 public ICommand CloseCommand
265 {
266 get { return this.root.CloseCommand; }
267 }
268
269 public bool IsComplete
270 {
271 get { return this.IsSuccessfulCompletion || this.IsFailedCompletion; }
272 }
273
274 public bool IsSuccessfulCompletion
275 {
276 get { return InstallationState.Applied == this.root.InstallState; }
277 }
278
279 public bool IsFailedCompletion
280 {
281 get { return InstallationState.Failed == this.root.InstallState; }
282 }
283
284 public ICommand InstallCommand
285 {
286 get
287 {
288 if (this.installCommand == null)
289 {
290 this.installCommand = new RelayCommand(
291 param => WixBA.Plan(LaunchAction.Install),
292 param => this.root.DetectState == DetectionState.Absent && this.root.UpgradeDetectState != UpgradeDetectionState.Newer && this.root.InstallState == InstallationState.Waiting);
293 }
294
295 return this.installCommand;
296 }
297 }
298
299 public bool InstallEnabled
300 {
301 get { return this.InstallCommand.CanExecute(this); }
302 }
303
304 public ICommand RepairCommand
305 {
306 get
307 {
308 if (this.repairCommand == null)
309 {
310 this.repairCommand = new RelayCommand(param => WixBA.Plan(LaunchAction.Repair), param => this.root.DetectState == DetectionState.Present && this.root.InstallState == InstallationState.Waiting);
311 }
312
313 return this.repairCommand;
314 }
315 }
316
317 public bool RepairEnabled
318 {
319 get { return this.RepairCommand.CanExecute(this); }
320 }
321
322 public ICommand UninstallCommand
323 {
324 get
325 {
326 if (this.uninstallCommand == null)
327 {
328 this.uninstallCommand = new RelayCommand(param => WixBA.Plan(LaunchAction.Uninstall), param => this.root.DetectState == DetectionState.Present && this.root.InstallState == InstallationState.Waiting);
329 }
330
331 return this.uninstallCommand;
332 }
333 }
334
335 public bool UninstallEnabled
336 {
337 get { return this.UninstallCommand.CanExecute(this); }
338 }
339
340 public ICommand OpenLogCommand
341 {
342 get
343 {
344 if (this.openLogCommand == null)
345 {
346 this.openLogCommand = new RelayCommand(param => WixBA.OpenLog(new Uri(WixBA.Model.Engine.GetVariableString("WixBundleLog"))));
347 }
348 return this.openLogCommand;
349 }
350 }
351
352 public ICommand OpenLogFolderCommand
353 {
354 get
355 {
356 if (this.openLogFolderCommand == null)
357 {
358 string logFolder = IO.Path.GetDirectoryName(WixBA.Model.Engine.GetVariableString("WixBundleLog"));
359 this.openLogFolderCommand = new RelayCommand(param => WixBA.OpenLogFolder(logFolder));
360 }
361 return this.openLogFolderCommand;
362 }
363 }
364
365 public ICommand TryAgainCommand
366 {
367 get
368 {
369 if (this.tryAgainCommand == null)
370 {
371 this.tryAgainCommand = new RelayCommand(param =>
372 {
373 this.root.Canceled = false;
374 WixBA.Plan(WixBA.Model.PlannedAction);
375 }, param => this.IsFailedCompletion);
376 }
377
378 return this.tryAgainCommand;
379 }
380 }
381
382 public string StatusText
383 {
384 get
385 {
386 switch(this.root.InstallState)
387 {
388 case InstallationState.Applied:
389 return "Complete";
390 case InstallationState.Failed:
391 return this.root.Canceled ? "Cancelled" : "Failed";
392 default:
393 return "Unknown"; // this shouldn't be shown in the UI.
394 }
395 }
396 }
397
398 /// <summary>
399 /// Launches the license in the default viewer.
400 /// </summary>
401 private void LaunchLicense()
402 {
403 string folder = IO.Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location);
404 WixBA.LaunchUrl(IO.Path.Combine(folder, "License.txt"));
405 }
406
407 private void DetectBegin(object sender, DetectBeginEventArgs e)
408 {
409 this.root.DetectState = RegistrationType.Full == e.RegistrationType ? DetectionState.Present : DetectionState.Absent;
410 WixBA.Model.PlannedAction = LaunchAction.Unknown;
411 }
412
413 private void DetectedRelatedBundle(object sender, DetectRelatedBundleEventArgs e)
414 {
415 if (e.RelationType == RelationType.Upgrade)
416 {
417 if (WixBA.Model.Engine.CompareVersions(this.Version, e.Version) > 0)
418 {
419 if (this.root.UpgradeDetectState == UpgradeDetectionState.None)
420 {
421 this.root.UpgradeDetectState = UpgradeDetectionState.Older;
422 }
423 }
424 else
425 {
426 this.root.UpgradeDetectState = UpgradeDetectionState.Newer;
427 }
428 }
429
430 if (!WixBA.Model.BAManifest.Bundle.Packages.ContainsKey(e.ProductCode))
431 {
432 WixBA.Model.BAManifest.Bundle.AddRelatedBundleAsPackage(e.ProductCode, e.RelationType, e.PerMachine, e.Version);
433 }
434 }
435
436 private void DetectComplete(object sender, DetectCompleteEventArgs e)
437 {
438 // Parse the command line string before any planning.
439 this.ParseCommandLine();
440 this.root.InstallState = InstallationState.Waiting;
441
442 if (LaunchAction.Uninstall == WixBA.Model.Command.Action &&
443 ResumeType.Arp != WixBA.Model.Command.Resume) // MSI and WixStdBA require some kind of confirmation before proceeding so WixBA should, too.
444 {
445 WixBA.Model.Engine.Log(LogLevel.Verbose, "Invoking automatic plan for uninstall");
446 WixBA.Plan(LaunchAction.Uninstall);
447 }
448 else if (Hresult.Succeeded(e.Status))
449 {
450 if (this.root.UpgradeDetectState == UpgradeDetectionState.Newer)
451 {
452 this.Downgrade = true;
453 this.DowngradeMessage = "There is already a newer version of WiX installed on this machine.";
454 }
455
456 if (LaunchAction.Layout == WixBA.Model.Command.Action)
457 {
458 WixBA.PlanLayout();
459 }
460 else if (WixBA.Model.Command.Display != Display.Full)
461 {
462 // If we're not waiting for the user to click install, dispatch plan with the default action.
463 WixBA.Model.Engine.Log(LogLevel.Verbose, "Invoking automatic plan for non-interactive mode.");
464 WixBA.Plan(WixBA.Model.Command.Action);
465 }
466 }
467 else
468 {
469 this.root.InstallState = InstallationState.Failed;
470 }
471
472 // Force all commands to reevaluate CanExecute.
473 // InvalidateRequerySuggested must be run on the UI thread.
474 this.root.Dispatcher.Invoke(new Action(CommandManager.InvalidateRequerySuggested));
475 }
476
477 private void PlanPackageBegin(object sender, PlanPackageBeginEventArgs e)
478 {
479 // If we're able to run our BA, we don't want to install .NET since the one on the machine is already good enough.
480 if (e.PackageId.StartsWith("NetFx4", StringComparison.OrdinalIgnoreCase) || e.PackageId.StartsWith("DesktopNetCoreRuntime", StringComparison.OrdinalIgnoreCase))
481 {
482 e.State = RequestState.None;
483 }
484 }
485
486 private void PlanComplete(object sender, PlanCompleteEventArgs e)
487 {
488 if (Hresult.Succeeded(e.Status))
489 {
490 this.root.PreApplyState = this.root.InstallState;
491 this.root.InstallState = InstallationState.Applying;
492 WixBA.Model.Engine.Apply(this.root.ViewWindowHandle);
493 }
494 else
495 {
496 this.root.InstallState = InstallationState.Failed;
497 }
498 }
499
500 private void ApplyBegin(object sender, ApplyBeginEventArgs e)
501 {
502 this.downloadRetries.Clear();
503 }
504
505 private void CacheAcquireBegin(object sender, CacheAcquireBeginEventArgs e)
506 {
507 this.cachePackageStart = DateTime.Now;
508 }
509
510 private void CacheAcquireResolving(object sender, CacheAcquireResolvingEventArgs e)
511 {
512 if (e.Action == CacheResolveOperation.Download && !this.downloadRetries.ContainsKey(e.PackageOrContainerId))
513 {
514 this.downloadRetries.Add(e.PackageOrContainerId, 0);
515 }
516 }
517
518 private void CacheAcquireComplete(object sender, CacheAcquireCompleteEventArgs e)
519 {
520 this.AddPackageTelemetry("Cache", e.PackageOrContainerId ?? String.Empty, DateTime.Now.Subtract(this.cachePackageStart).TotalMilliseconds, e.Status);
521
522 if (e.Status < 0 && this.downloadRetries.TryGetValue(e.PackageOrContainerId, out var retries) && retries < 3)
523 {
524 this.downloadRetries[e.PackageOrContainerId] = retries + 1;
525 switch (e.Status)
526 {
527 case -2147023294: //HRESULT_FROM_WIN32(ERROR_INSTALL_USEREXIT)
528 case -2147024894: //HRESULT_FROM_WIN32(ERROR_FILE_NOT_FOUND)
529 case -2147012889: //HRESULT_FROM_WIN32(ERROR_INTERNET_NAME_NOT_RESOLVED)
530 break;
531 default:
532 e.Action = BOOTSTRAPPER_CACHEACQUIRECOMPLETE_ACTION.Retry;
533 break;
534 }
535 }
536 }
537
538 private void ExecutePackageBegin(object sender, ExecutePackageBeginEventArgs e)
539 {
540 lock (this)
541 {
542 this.executePackageStart = e.ShouldExecute ? DateTime.Now : DateTime.MinValue;
543 }
544 }
545
546 private void ExecutePackageComplete(object sender, ExecutePackageCompleteEventArgs e)
547 {
548 lock (this)
549 {
550 if (DateTime.MinValue < this.executePackageStart)
551 {
552 this.AddPackageTelemetry("Execute", e.PackageId ?? String.Empty, DateTime.Now.Subtract(this.executePackageStart).TotalMilliseconds, e.Status);
553 this.executePackageStart = DateTime.MinValue;
554 }
555 }
556 }
557
558 private void ExecuteError(object sender, ErrorEventArgs e)
559 {
560 lock (this)
561 {
562 if (!this.root.Canceled)
563 {
564 // If the error is a cancel coming from the engine during apply we want to go back to the preapply state.
565 if (InstallationState.Applying == this.root.InstallState && (int)Error.UserCancelled == e.ErrorCode)
566 {
567 this.root.InstallState = this.root.PreApplyState;
568 }
569 else
570 {
571 this.Message = e.ErrorMessage;
572
573 if (Display.Full == WixBA.Model.Command.Display)
574 {
575 // On HTTP authentication errors, have the engine try to do authentication for us.
576 if (ErrorType.HttpServerAuthentication == e.ErrorType || ErrorType.HttpProxyAuthentication == e.ErrorType)
577 {
578 e.Result = Result.TryAgain;
579 }
580 else // show an error dialog.
581 {
582 MessageBoxButton msgbox = MessageBoxButton.OK;
583 switch (e.UIHint & 0xF)
584 {
585 case 0:
586 msgbox = MessageBoxButton.OK;
587 break;
588 case 1:
589 msgbox = MessageBoxButton.OKCancel;
590 break;
591 // There is no 2! That would have been MB_ABORTRETRYIGNORE.
592 case 3:
593 msgbox = MessageBoxButton.YesNoCancel;
594 break;
595 case 4:
596 msgbox = MessageBoxButton.YesNo;
597 break;
598 // default: stay with MBOK since an exact match is not available.
599 }
600
601 MessageBoxResult result = MessageBoxResult.None;
602 WixBA.View.Dispatcher.Invoke((Action)delegate()
603 {
604 result = MessageBox.Show(WixBA.View, e.ErrorMessage, "WiX Toolset", msgbox, MessageBoxImage.Error);
605 }
606 );
607
608 // If there was a match from the UI hint to the msgbox value, use the result from the
609 // message box. Otherwise, we'll ignore it and return the default to Burn.
610 if ((e.UIHint & 0xF) == (int)msgbox)
611 {
612 e.Result = (Result)result;
613 }
614 }
615 }
616 }
617 }
618 else // canceled, so always return cancel.
619 {
620 e.Result = Result.Cancel;
621 }
622 }
623 }
624
625 private void ApplyComplete(object sender, ApplyCompleteEventArgs e)
626 {
627 WixBA.Model.Result = e.Status; // remember the final result of the apply.
628
629 // Set the state to applied or failed unless the state has already been set back to the preapply state
630 // which means we need to show the UI as it was before the apply started.
631 if (this.root.InstallState != this.root.PreApplyState)
632 {
633 this.root.InstallState = Hresult.Succeeded(e.Status) ? InstallationState.Applied : InstallationState.Failed;
634 }
635
636 // If we're not in Full UI mode, we need to alert the dispatcher to stop and close the window for passive.
637 if (Display.Full != WixBA.Model.Command.Display)
638 {
639 // If its passive, send a message to the window to close.
640 if (Display.Passive == WixBA.Model.Command.Display)
641 {
642 WixBA.Model.Engine.Log(LogLevel.Verbose, "Automatically closing the window for non-interactive install");
643 WixBA.Dispatcher.BeginInvoke(new Action(WixBA.View.Close));
644 }
645 else
646 {
647 WixBA.Dispatcher.InvokeShutdown();
648 }
649 return;
650 }
651 else if (Hresult.Succeeded(e.Status) && LaunchAction.UpdateReplace == WixBA.Model.PlannedAction) // if we successfully applied an update close the window since the new Bundle should be running now.
652 {
653 WixBA.Model.Engine.Log(LogLevel.Verbose, "Automatically closing the window since update successful.");
654 WixBA.Dispatcher.BeginInvoke(new Action(WixBA.View.Close));
655 return;
656 }
657 else if (this.root.AutoClose)
658 {
659 // Automatically closing since the user clicked the X button.
660 WixBA.Dispatcher.BeginInvoke(new Action(WixBA.View.Close));
661 return;
662 }
663
664 // Force all commands to reevaluate CanExecute.
665 // InvalidateRequerySuggested must be run on the UI thread.
666 this.root.Dispatcher.Invoke(new Action(CommandManager.InvalidateRequerySuggested));
667 }
668
669 private void ParseCommandLine()
670 {
671 // Get array of arguments based on the system parsing algorithm.
672 string[] args = BootstrapperCommand.ParseCommandLineToArgs(WixBA.Model.Command.CommandLine);
673 for (int i = 0; i < args.Length; ++i)
674 {
675 if (args[i].StartsWith("InstallFolder=", StringComparison.InvariantCultureIgnoreCase))
676 {
677 // Allow relative directory paths. Also validates.
678 string[] param = args[i].Split(new char[] {'='}, 2);
679 this.root.InstallDirectory = IO.Path.Combine(Environment.CurrentDirectory, param[1]);
680 }
681 }
682 }
683
684 private void AddPackageTelemetry(string prefix, string id, double time, int result)
685 {
686 lock (this)
687 {
688 string key = String.Format("{0}Time_{1}", prefix, id);
689 string value = time.ToString();
690 WixBA.Model.Telemetry.Add(new KeyValuePair<string, string>(key, value));
691
692 key = String.Format("{0}Result_{1}", prefix, id);
693 value = String.Concat("0x", result.ToString("x"));
694 WixBA.Model.Telemetry.Add(new KeyValuePair<string, string>(key, value));
695 }
696 }
697 }
698 }