fix(launcher): harden updater crash recovery
This commit is contained in:
parent
2d2a5b5046
commit
1955ca8ab5
27 changed files with 2714 additions and 544 deletions
|
|
@ -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" />
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
128
src/AcDream.Launcher/LauncherUpdateComposition.cs
Normal file
128
src/AcDream.Launcher/LauncherUpdateComposition.cs
Normal 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;
|
||||
}
|
||||
|
|
@ -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));
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue