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