Add UpdateManager
MODIFY
WalletWasabi.Fluent/Global.cs
WalletWasabi.Fluent/Global.cs
@@ -54,6 +54,7 @@ public class Global
private TorProcessManager? TorManager { get; set; }
public CoreNode? BitcoinCoreNode { get; private set; }
public TorStatusChecker TorStatusChecker { get; set; }
+ public UpdateManager UpdateManager { get; set; }
public HostedServices HostedServices { get; }
public UiConfig UiConfig { get; }
@@ -102,6 +103,7 @@ public class Global
Synchronizer = new WasabiSynchronizer(BitcoinStore, HttpClientFactory);
LegalChecker = new(DataDir);
+ UpdateManager = new(DataDir);
TransactionBroadcaster = new TransactionBroadcaster(Network, BitcoinStore, HttpClientFactory, WalletManager);
TorStatusChecker = new TorStatusChecker(TimeSpan.FromHours(6), HttpClientFactory.NewHttpClient(Mode.DefaultCircuit), new XmlIssueListParser());
@@ -145,6 +147,8 @@ public class Global
HostedServices.Register<UpdateChecker>(() => new UpdateChecker(TimeSpan.FromMinutes(7), Synchronizer), "Software Update Checker");
await LegalChecker.InitializeAsync(HostedServices.Get<UpdateChecker>()).ConfigureAwait(false);
+ UpdateManager.Initialize(HostedServices.Get<UpdateChecker>());
+
cancel.ThrowIfCancellationRequested();
await StartTorProcessManagerAsync(cancel).ConfigureAwait(false);

MODIFY
WalletWasabi.Fluent/Services.cs
WalletWasabi.Fluent/Services.cs
@@ -36,6 +36,7 @@ public static class Services
public static SingleInstanceChecker SingleInstanceChecker { get; private set; } = null!;
public static TorStatusChecker TorStatusChecker { get; private set; } = null!;
+ public static UpdateManager UpdateManager { get; private set; } = null!;
public static bool IsInitialized { get; private set; }
@@ -57,6 +58,7 @@ public static class Services
Guard.NotNull(nameof(global.HostedServices), global.HostedServices);
Guard.NotNull(nameof(global.UiConfig), global.UiConfig);
Guard.NotNull(nameof(global.TorStatusChecker), global.TorStatusChecker);
+ Guard.NotNull(nameof(global.UpdateManager), global.UpdateManager);
DataDir = global.DataDir;
TorSettings = global.TorSettings;
@@ -71,6 +73,7 @@ public static class Services
UiConfig = global.UiConfig;
SingleInstanceChecker = singleInstanceChecker;
TorStatusChecker = global.TorStatusChecker;
+ UpdateManager = global.UpdateManager;
IsInitialized = true;
}

MODIFY
WalletWasabi.Fluent/ViewModels/StatusIcon/StatusIconViewModel.cs
WalletWasabi.Fluent/ViewModels/StatusIcon/StatusIconViewModel.cs
@@ -27,6 +27,7 @@ public partial class StatusIconViewModel : IStatusIconViewModel, IDisposable
[AutoNotify] private int _peers;
[AutoNotify] private bool _updateAvailable;
[AutoNotify] private bool _criticalUpdateAvailable;
+ [AutoNotify] private bool _isReadyToInstall;
[AutoNotify] private bool _isConnectionIssueDetected;
[AutoNotify] private string? _versionText;
private readonly ObservableAsPropertyHelper<ICollection<Issue>> _torIssues;
@@ -37,9 +38,20 @@ public partial class StatusIconViewModel : IStatusIconViewModel, IDisposable
TorStatus = UseTor ? Services.Synchronizer.TorStatus : TorStatus.TurnedOff;
UseBitcoinCore = Services.Config.StartLocalBitcoinCoreOnStartup;
- UpdateCommand = ReactiveCommand.CreateFromTask( () => IoHelpers.OpenBrowserAsync("https://wasabiwallet.io/#download"));
+ ManualUpdateCommand = ReactiveCommand.CreateFromTask(() => IoHelpers.OpenBrowserAsync("https://wasabiwallet.io/#download"));
+ UpdateCommand = ReactiveCommand.Create(() =>
+ {
+ Services.UpdateManager.UpdateOnClose = true;
+ UpdateAvailable = false;
+ });
+
+ AskMeLaterCommand = ReactiveCommand.Create(() =>
+ {
+ Services.UpdateManager.UpdateOnClose = false;
+ UpdateAvailable = false;
+ });
+
OpenTorStatusSiteCommand = ReactiveCommand.CreateFromTask(() => IoHelpers.OpenBrowserAsync("https://status.torproject.org"));
- AskMeLaterCommand = ReactiveCommand.Create(() => UpdateAvailable = false);
this.WhenAnyValue(
x => x.TorStatus,
@@ -76,6 +88,7 @@ public partial class StatusIconViewModel : IStatusIconViewModel, IDisposable
public ICommand OpenTorStatusSiteCommand { get; }
public ICommand UpdateCommand { get; }
+ public ICommand ManualUpdateCommand { get; }
public ICommand AskMeLaterCommand { get; }
@@ -126,6 +139,7 @@ public partial class StatusIconViewModel : IStatusIconViewModel, IDisposable
var synchronizer = Services.Synchronizer;
var rpcMonitor = Services.HostedServices.GetOrDefault<RpcMonitor>();
var updateChecker = Services.HostedServices.Get<UpdateChecker>();
+ var updateManager = Services.UpdateManager;
BitcoinCoreStatus = rpcMonitor?.RpcStatus ?? RpcStatus.Unresponsive;
@@ -168,7 +182,7 @@ public partial class StatusIconViewModel : IStatusIconViewModel, IDisposable
.DisposeWith(Disposables);
}
- Observable.FromEventPattern<UpdateStatus>(updateChecker, nameof(updateChecker.UpdateStatusChanged))
+ Observable.FromEventPattern<UpdateStatus>(updateManager, nameof(updateManager.UpdateAvailableToGet))
.ObserveOn(RxApp.MainThreadScheduler)
.Subscribe(e =>
{
@@ -176,11 +190,16 @@ public partial class StatusIconViewModel : IStatusIconViewModel, IDisposable
UpdateAvailable = !updateStatus.ClientUpToDate;
CriticalUpdateAvailable = !updateStatus.BackendCompatible;
+ IsReadyToInstall = updateStatus.IsReadyToInstall;
if (CriticalUpdateAvailable)
{
VersionText = $"Critical update required";
}
+ else if (IsReadyToInstall)
+ {
+ VersionText = $"Version {updateStatus.ClientVersion} is now ready to install";
+ }
else if (UpdateAvailable)
{
VersionText = $"Version {updateStatus.ClientVersion} is now available";

MODIFY
WalletWasabi/Models/UpdateStatus.cs
WalletWasabi/Models/UpdateStatus.cs
@@ -13,6 +13,7 @@ public class UpdateStatus : IEquatable<UpdateStatus>
public bool ClientUpToDate { get; }
public bool BackendCompatible { get; }
+ public bool IsReadyToInstall { get; set; } = false;
public Version LegalDocumentsVersion { get; }
public ushort CurrentBackendMajorVersion { get; }

ADD
WalletWasabi/Services/UpdateManager.cs
+using System;
+using System.Collections.Generic;
+using System.Diagnostics;
+using System.IO;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+using WalletWasabi.Microservices;
+using WalletWasabi.Models;
+
+namespace WalletWasabi.Services;
+
+public class UpdateManager
+{
+ public UpdateManager(string dataDir)
+ {
+ DataDir = dataDir;
+ }
+
+ private void UpdateChecker_UpdateStatusChanged(object? sender, UpdateStatus updateStatus)
+ {
+ bool updateAvailable = !updateStatus.ClientUpToDate || !updateStatus.BackendCompatible;
+ if (updateAvailable)
+ {
+ try
+ {
+ // TODO: download MSI to DataDir/downloads
+ bool downloadSuccessful = GetInstaller(updateStatus);
+ updateStatus.IsReadyToInstall = downloadSuccessful;
+ }
+ catch (Exception)
+ {
+ throw;
+ }
+ finally
+ {
+ UpdateAvailableToGet?.Invoke(this, updateStatus);
+ }
+ }
+ }
+
+ private bool GetInstaller(UpdateStatus updateStatus)
+ {
+ var resp = GetInstallerAsync(updateStatus);
+ return resp.Result;
+ }
+
+ private async Task<bool> GetInstallerAsync(UpdateStatus updateStatus)
+ {
+ await Task.Delay(5000).ConfigureAwait(false);
+ // Download installer here
+ // based on target OS
+ return true;
+ }
+
+ public event EventHandler<UpdateStatus>? UpdateAvailableToGet;
+
+ public string DataDir { get; }
+ public bool UpdateOnClose { get; set; }
+
+ public void Initialize(UpdateChecker updateChecker)
+ {
+ updateChecker.UpdateStatusChanged += UpdateChecker_UpdateStatusChanged;
+ }
+
+ public void InstallNewVersion()
+ {
+ var installerPath = Path.Combine(DataDir, "Downloads", "<downloaded_installer_name>");
+ if (File.Exists(installerPath))
+ {
+ ProcessStartInfo startInfo = ProcessStartInfoFactory.Make(installerPath, "");
+ using Process? p = Process.Start(startInfo);
+ }
+ }
+}

Add UpdateManager