fix(launcher): harden updater crash recovery

This commit is contained in:
Erik 2026-08-14 23:12:15 +02:00
parent 2d2a5b5046
commit 1955ca8ab5
27 changed files with 2714 additions and 544 deletions

View file

@ -14,6 +14,10 @@
<PublishBakeTool Condition="'$(PublishBakeTool)' == ''">true</PublishBakeTool>
</PropertyGroup>
<ItemGroup>
<InternalsVisibleTo Include="AcDream.Launcher.Tests" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Avalonia" Version="12.1.1" />
<PackageReference Include="Avalonia.Desktop" Version="12.1.1" />

View file

@ -16,8 +16,7 @@ public sealed partial class App : Application
{
private LauncherOrchestrator? _orchestrator;
private LauncherWindowViewModel? _viewModel;
private HttpClient? _updateHttpClient;
private ReleaseManifestClient? _manifestClient;
private LauncherUpdateComposition? _updateComposition;
public override void Initialize() => AvaloniaXamlLoader.Load(this);
@ -52,38 +51,27 @@ public sealed partial class App : Application
$"Client content verification failed: {ex.Message}");
}
var clientVersions = new ClientVersionStore(paths);
_ = clientVersions.LoadAndRecoverAsync(rid)
.GetAwaiter()
.GetResult();
LauncherUpdateComposition updates = LauncherUpdateComposition.Create(
paths,
rid,
GetLauncherVersion(),
AppContext.BaseDirectory,
() => _orchestrator?.GetSnapshot().Sessions.Any(session => session.IsActive)
== true);
_updateComposition = updates;
_orchestrator = new LauncherOrchestrator(
profiles,
paths,
LauncherExecutableSet.FromCurrentVersionStore(clientVersions),
updates.Executables,
verification.Record,
installationStatus: verification.Status,
updateSessionBarrier: clientVersions.Barrier);
_updateHttpClient = new HttpClient();
_updateHttpClient.Timeout = TimeSpan.FromSeconds(15);
_updateHttpClient.DefaultRequestHeaders.UserAgent.ParseAdd(
"acdream-launcher/1");
_manifestClient = new ReleaseManifestClient(_updateHttpClient);
var selfUpdates = new LauncherSelfUpdateManager(paths, _updateHttpClient);
var updater = new LauncherUpdater(
_manifestClient,
_updateHttpClient,
clientVersions,
selfUpdates,
GetLauncherVersion(),
rid,
AppContext.BaseDirectory,
() => _orchestrator.GetSnapshot().Sessions.Any(session => session.IsActive));
updateSessionBarrier: updates.Versions.Barrier);
_viewModel = new LauncherWindowViewModel(
_orchestrator,
new AvaloniaUiDispatcher(),
installer,
updater);
updates.Updater);
_viewModel.Initialize();
desktop.MainWindow = new MainWindow
@ -100,12 +88,10 @@ public sealed partial class App : Application
{
_viewModel?.Dispose();
_orchestrator?.Dispose();
_manifestClient?.Dispose();
_updateHttpClient?.Dispose();
_updateComposition?.Dispose();
_viewModel = null;
_orchestrator = null;
_manifestClient = null;
_updateHttpClient = null;
_updateComposition = null;
}
private static LauncherVersion GetLauncherVersion()

View file

@ -0,0 +1,128 @@
using System.Net;
using System.Security;
using System.Text.Json;
using AcDream.Launcher.Core.Orchestration;
using AcDream.Launcher.Core.Updates;
using AcDream.Launcher.ViewModels;
using AcDream.Platform;
namespace AcDream.Launcher;
/// <summary>
/// Testable startup transaction for versioned-client/update services. Storage
/// failures produce a fail-closed executable resolver and an unavailable UI
/// projection; they do not abort profile/installer window construction.
/// </summary>
internal sealed class LauncherUpdateComposition : IDisposable
{
private readonly HttpClient? _artifactClient;
private readonly ReleaseManifestClient? _manifestClient;
private LauncherUpdateComposition(
ClientVersionStore versions,
LauncherExecutableSet executables,
ILauncherUpdater updater,
HttpClient? artifactClient,
ReleaseManifestClient? manifestClient)
{
Versions = versions;
Executables = executables;
Updater = updater;
_artifactClient = artifactClient;
_manifestClient = manifestClient;
}
public ClientVersionStore Versions { get; }
public LauncherExecutableSet Executables { get; }
public ILauncherUpdater Updater { get; }
public static LauncherUpdateComposition Create(
ApplicationPathSet paths,
string rid,
LauncherVersion launcherVersion,
string launcherTargetDirectory,
Func<bool> hasRunningSessions,
Func<ClientVersionStore, string, ClientVersionResolution>? initialize = null)
{
ArgumentNullException.ThrowIfNull(paths);
ArgumentNullException.ThrowIfNull(launcherVersion);
ArgumentNullException.ThrowIfNull(hasRunningSessions);
var versions = new ClientVersionStore(paths);
HttpClient? artifactClient = null;
ReleaseManifestClient? manifestClient = null;
try
{
_ = initialize is null
? versions.LoadAndRecoverAsync(rid).GetAwaiter().GetResult()
: initialize(versions, rid);
artifactClient = new HttpClient(
new HttpClientHandler
{
AllowAutoRedirect = false,
UseCookies = false,
AutomaticDecompression = DecompressionMethods.None,
},
disposeHandler: true)
{
Timeout = TimeSpan.FromSeconds(15),
};
artifactClient.DefaultRequestHeaders.UserAgent.ParseAdd("acdream-launcher/1");
manifestClient = new ReleaseManifestClient(TimeSpan.FromSeconds(15));
var selfUpdates = new LauncherSelfUpdateManager(paths, artifactClient);
var updater = new LauncherUpdater(
manifestClient,
artifactClient,
versions,
selfUpdates,
launcherVersion,
rid,
launcherTargetDirectory,
hasRunningSessions);
return new LauncherUpdateComposition(
versions,
LauncherExecutableSet.FromCurrentVersionStore(versions),
updater,
artifactClient,
manifestClient);
}
catch (Exception ex) when (IsStorageFailure(ex))
{
manifestClient?.Dispose();
artifactClient?.Dispose();
string status = "Versioned client update storage is unavailable: "
+ (string.IsNullOrWhiteSpace(ex.Message)
? "the storage operation failed."
: ex.Message);
var resolution = new ClientVersionResolution(
ClientVersionState.Invalid,
status,
null,
null,
null,
null);
return new LauncherUpdateComposition(
versions,
LauncherExecutableSet.Unavailable(status),
new UnavailableLauncherUpdater(status, resolution),
artifactClient: null,
manifestClient: null);
}
}
public void Dispose()
{
_manifestClient?.Dispose();
_artifactClient?.Dispose();
}
private static bool IsStorageFailure(Exception exception) => exception is
IOException
or UnauthorizedAccessException
or SecurityException
or JsonException
or FormatException
or NotSupportedException
or LauncherUpdateException;
}

View file

@ -479,42 +479,52 @@ public sealed class LauncherUpdateViewModel : ObservableObject, IDisposable
internal sealed class UnavailableLauncherUpdater : ILauncherUpdater
{
private static readonly ClientVersionResolution Missing = new(
ClientVersionState.Missing,
"Versioned client updater is unavailable.",
null,
null,
null,
null);
private readonly ClientVersionResolution _resolution;
private readonly string _status;
public ClientVersionResolution CurrentClient => Missing;
public UnavailableLauncherUpdater(
string status = "Versioned client updater is unavailable.",
ClientVersionResolution? resolution = null)
{
ArgumentException.ThrowIfNullOrWhiteSpace(status);
_status = status;
_resolution = resolution ?? new ClientVersionResolution(
ClientVersionState.Invalid,
status,
null,
null,
null,
null);
}
public ClientVersionResolution CurrentClient => _resolution;
public Task<ClientVersionResolution> InitializeAsync(
CancellationToken cancellationToken = default) =>
Task.FromResult(Missing);
Task.FromResult(_resolution);
public Task<LauncherUpdateCheckResult> CheckAsync(
CancellationToken cancellationToken = default) =>
Task.FromException<LauncherUpdateCheckResult>(
new LauncherUpdateException("Versioned client updater is unavailable."));
new LauncherUpdateException(_status));
public Task<ClientVersionResolution> InstallClientAsync(
LauncherUpdateCheckResult check,
IProgress<LauncherUpdateProgress>? progress = null,
CancellationToken cancellationToken = default) =>
Task.FromException<ClientVersionResolution>(
new LauncherUpdateException("Versioned client updater is unavailable."));
new LauncherUpdateException(_status));
public Task<SelfUpdateStageResult> StageLauncherAsync(
LauncherUpdateCheckResult check,
IProgress<LauncherUpdateProgress>? progress = null,
CancellationToken cancellationToken = default) =>
Task.FromException<SelfUpdateStageResult>(
new LauncherUpdateException("Versioned client updater is unavailable."));
new LauncherUpdateException(_status));
public Task<ClientVersionResolution> RollbackClientAsync(
IProgress<LauncherUpdateProgress>? progress = null,
CancellationToken cancellationToken = default) =>
Task.FromException<ClientVersionResolution>(
new LauncherUpdateException("Versioned client updater is unavailable."));
new LauncherUpdateException(_status));
}