feat(launcher): implement verified atomic updates

This commit is contained in:
Erik 2026-08-14 22:09:34 +02:00
parent 2198a0cc8e
commit 2d2a5b5046
34 changed files with 6755 additions and 61 deletions

View file

@ -4,6 +4,7 @@ using AcDream.Launcher.Core.Installation;
using AcDream.Launcher.Core.Launching;
using AcDream.Launcher.Core.Orchestration;
using AcDream.Launcher.Core.Profiles;
using AcDream.Launcher.Core.Updates;
namespace AcDream.Launcher.ViewModels;
@ -25,7 +26,8 @@ public sealed class LauncherWindowViewModel : ObservableObject, IDisposable
public LauncherWindowViewModel(
ILauncherOrchestrator orchestrator,
IUiDispatcher dispatcher,
ILauncherInstaller? installer = null)
ILauncherInstaller? installer = null,
ILauncherUpdater? updater = null)
{
_orchestrator = orchestrator ?? throw new ArgumentNullException(nameof(orchestrator));
_dispatcher = dispatcher ?? throw new ArgumentNullException(nameof(dispatcher));
@ -38,17 +40,16 @@ public sealed class LauncherWindowViewModel : ObservableObject, IDisposable
OnInstallCompleted,
() => CanInteract,
() => !IsBusy && Sessions.All(session => !session.IsActive));
UpdatePromptShell = new LauncherShellViewModel(
"Client update",
"Review a signed release manifest, verify the downloaded archive, "
+ "and atomically switch the installed client version. The updater "
+ "transaction lands in Campaign LA slice LA10.",
"Updater shell ready — implementation arrives in LA10.",
() => CanInteract);
UpdatePrompt = new LauncherUpdateViewModel(
updater ?? new UnavailableLauncherUpdater(),
dispatcher,
OnClientVersionChanged,
() => CanInteract,
() => !IsBusy && Sessions.All(session => !session.IsActive));
EditorDialog.PropertyChanged += OnModalPropertyChanged;
FirstRunWizardShell.PropertyChanged += OnModalPropertyChanged;
UpdatePromptShell.PropertyChanged += OnModalPropertyChanged;
UpdatePrompt.PropertyChanged += OnModalPropertyChanged;
AddServerCommand = new RelayCommand(OpenAddServerDialog, () => CanInteract);
AddAccountCommand = new RelayCommand(OpenAddAccountDialog, CanAddAccount);
@ -92,7 +93,7 @@ public sealed class LauncherWindowViewModel : ObservableObject, IDisposable
public FirstRunInstallerViewModel FirstRunWizardShell { get; }
public LauncherShellViewModel UpdatePromptShell { get; }
public LauncherUpdateViewModel UpdatePrompt { get; }
public LauncherTreeNodeViewModel? SelectedNode
{
@ -136,7 +137,7 @@ public sealed class LauncherWindowViewModel : ObservableObject, IDisposable
public bool IsModalOpen =>
EditorDialog.IsOpen
|| FirstRunWizardShell.IsOpen
|| UpdatePromptShell.IsOpen;
|| UpdatePrompt.IsOpen;
private bool CanInteract => !IsBusy && !IsModalOpen;
@ -300,6 +301,7 @@ public sealed class LauncherWindowViewModel : ObservableObject, IDisposable
}
RefreshFromCore();
_ = UpdatePrompt.StartupCheckAsync();
}
public void PollStatus()
@ -333,8 +335,9 @@ public sealed class LauncherWindowViewModel : ObservableObject, IDisposable
_orchestrator.StateChanged -= OnOrchestratorStateChanged;
EditorDialog.PropertyChanged -= OnModalPropertyChanged;
FirstRunWizardShell.PropertyChanged -= OnModalPropertyChanged;
UpdatePromptShell.PropertyChanged -= OnModalPropertyChanged;
UpdatePrompt.PropertyChanged -= OnModalPropertyChanged;
FirstRunWizardShell.Dispose();
UpdatePrompt.Dispose();
}
private void OnOrchestratorStateChanged(object? sender, EventArgs e) =>
@ -350,7 +353,8 @@ public sealed class LauncherWindowViewModel : ObservableObject, IDisposable
{
if (e.PropertyName != nameof(ProfileEditorDialogViewModel.IsOpen)
&& e.PropertyName != nameof(LauncherShellViewModel.IsOpen)
&& e.PropertyName != nameof(FirstRunInstallerViewModel.IsOpen))
&& e.PropertyName != nameof(FirstRunInstallerViewModel.IsOpen)
&& e.PropertyName != nameof(LauncherUpdateViewModel.IsOpen))
{
return;
}
@ -376,9 +380,9 @@ public sealed class LauncherWindowViewModel : ObservableObject, IDisposable
{
FirstRunWizardShell.Close();
}
else if (UpdatePromptShell.IsOpen)
else if (UpdatePrompt.IsOpen)
{
UpdatePromptShell.IsOpen = false;
UpdatePrompt.Close();
}
}
@ -976,6 +980,13 @@ public sealed class LauncherWindowViewModel : ObservableObject, IDisposable
RefreshFromCore();
}
private void OnClientVersionChanged()
{
OperationStatus = "Versioned client activation changed.";
LastError = null;
RefreshFromCore();
}
private void NotifyCommandStates()
{
AddServerCommand.NotifyCanExecuteChanged();
@ -996,7 +1007,7 @@ public sealed class LauncherWindowViewModel : ObservableObject, IDisposable
session.NotifyCommandState();
}
FirstRunWizardShell.NotifyCommandStates();
UpdatePromptShell.NotifyCommandStates();
UpdatePrompt.NotifyCommandStates();
}
private readonly record struct SelectionKey(