fix(launcher): harden updater crash recovery
This commit is contained in:
parent
2d2a5b5046
commit
1955ca8ab5
27 changed files with 2714 additions and 544 deletions
|
|
@ -1,23 +1,9 @@
|
|||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
using AcDream.Launcher.Core.Updates;
|
||||
|
||||
namespace AcDream.Launcher.Core.Tests.Updates;
|
||||
|
||||
public sealed class LauncherSelfUpdateManagerTests : IDisposable
|
||||
{
|
||||
private static readonly JsonSerializerOptions PlanOptions = new()
|
||||
{
|
||||
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
|
||||
WriteIndented = true,
|
||||
Converters =
|
||||
{
|
||||
new JsonStringEnumConverter<SelfUpdatePlanState>(
|
||||
JsonNamingPolicy.CamelCase,
|
||||
allowIntegerValues: false),
|
||||
},
|
||||
};
|
||||
|
||||
private readonly string _root = Path.Combine(
|
||||
Path.GetTempPath(),
|
||||
"acdream-self-update-tests",
|
||||
|
|
@ -47,6 +33,12 @@ public sealed class LauncherSelfUpdateManagerTests : IDisposable
|
|||
await File.WriteAllTextAsync(unrelated, "preserve");
|
||||
|
||||
Assert.Null(await harness.Manager.LoadPendingAsync());
|
||||
using UpdateSessionBarrier.ExclusiveLease lease =
|
||||
harness.Manager.Barrier.AcquireExclusive();
|
||||
Assert.True(harness.Manager.CleanupOwnedResidueUnderLease(
|
||||
pending: null,
|
||||
harness.Target,
|
||||
lease));
|
||||
|
||||
Assert.False(Directory.Exists(orphan));
|
||||
Assert.False(File.Exists(temporary));
|
||||
|
|
@ -121,49 +113,91 @@ public sealed class LauncherSelfUpdateManagerTests : IDisposable
|
|||
}
|
||||
|
||||
[Fact]
|
||||
public async Task CrashDuringApplyingReplaysReverseJournalToStagedState()
|
||||
public async Task PriorOwnershipRemovesObsoleteFilesAndRollbackRestoresThem()
|
||||
{
|
||||
using var harness = new Harness(_root);
|
||||
byte[] firstArchive = UpdateTestData.CreateZip(
|
||||
[
|
||||
(harness.LauncherName, "launcher-v2"u8.ToArray(), 0x81ED),
|
||||
("support.dat", "support-v2"u8.ToArray(), 0x81A4),
|
||||
("obsolete.dll", "obsolete-v2"u8.ToArray(), 0x81A4),
|
||||
]);
|
||||
_ = await harness.StageAsync("2.0.0", firstArchive);
|
||||
SelfUpdatePlan first = await harness.Manager.ApplyPendingAsync(harness.Target);
|
||||
await harness.Manager.ConfirmAsync(
|
||||
first.TransactionId,
|
||||
harness.Target,
|
||||
harness.LauncherPath);
|
||||
await harness.Manager.CompleteConfirmedAsync(first.TransactionId, harness.Target);
|
||||
string obsoletePath = Path.Combine(harness.Target, "obsolete.dll");
|
||||
Assert.Equal("obsolete-v2", await File.ReadAllTextAsync(obsoletePath));
|
||||
|
||||
byte[] secondArchive = UpdateTestData.CreateZip(
|
||||
[
|
||||
(harness.LauncherName, "launcher-v3"u8.ToArray(), 0x81ED),
|
||||
("support.dat", "support-v3"u8.ToArray(), 0x81A4),
|
||||
]);
|
||||
_ = await harness.StageAsync("3.0.0", secondArchive);
|
||||
SelfUpdatePlan applied = await harness.Manager.ApplyPendingAsync(harness.Target);
|
||||
|
||||
Assert.False(File.Exists(obsoletePath));
|
||||
Assert.Contains(
|
||||
applied.Apply!,
|
||||
entry => entry.Path == "obsolete.dll"
|
||||
&& entry.Operation == SelfUpdateApplyOperation.Remove
|
||||
&& entry.HadOriginal);
|
||||
|
||||
SelfUpdatePlan rolledBack = await harness.Manager
|
||||
.RollbackAwaitingConfirmationAsync(harness.Target);
|
||||
|
||||
Assert.Equal(SelfUpdatePlanState.Staged, rolledBack.State);
|
||||
Assert.Equal("launcher-v2", await File.ReadAllTextAsync(harness.LauncherPath));
|
||||
Assert.Equal("support-v2", await File.ReadAllTextAsync(harness.SupportPath));
|
||||
Assert.Equal("obsolete-v2", await File.ReadAllTextAsync(obsoletePath));
|
||||
|
||||
SelfUpdatePlan retried = await harness.Manager.ApplyPendingAsync(harness.Target);
|
||||
await harness.Manager.ConfirmAsync(
|
||||
retried.TransactionId,
|
||||
harness.Target,
|
||||
harness.LauncherPath);
|
||||
await harness.Manager.CompleteConfirmedAsync(retried.TransactionId, harness.Target);
|
||||
|
||||
Assert.False(File.Exists(obsoletePath));
|
||||
string ownership = await File.ReadAllTextAsync(Path.Combine(
|
||||
harness.Target,
|
||||
LauncherSelfUpdateManager.InstallRecordFileName));
|
||||
Assert.DoesNotContain("obsolete.dll", ownership, StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ApplyFailpointAfterCanonicalAtomicReplaceRollsBackToStagedState()
|
||||
{
|
||||
using var harness = new Harness(_root);
|
||||
_ = await harness.StageAsync();
|
||||
SelfUpdatePlan staged = Assert.IsType<SelfUpdatePlan>(
|
||||
await harness.Manager.LoadPendingAsync());
|
||||
SelfUpdateApplyEntry[] apply = staged.Files
|
||||
.Select(file => new SelfUpdateApplyEntry(
|
||||
file.Path,
|
||||
File.Exists(Path.Combine(
|
||||
harness.Target,
|
||||
file.Path.Replace('/', Path.DirectorySeparatorChar)))))
|
||||
.ToArray();
|
||||
SelfUpdatePlan applying = staged with
|
||||
LauncherSelfUpdateManager faulting = harness.CreateManagerWithObserver(observation =>
|
||||
{
|
||||
State = SelfUpdatePlanState.Applying,
|
||||
Apply = apply,
|
||||
};
|
||||
await File.WriteAllTextAsync(
|
||||
harness.Manager.PendingPlanPath,
|
||||
JsonSerializer.Serialize(applying, PlanOptions));
|
||||
if (observation.Boundary == SelfUpdateApplyBoundary.AfterTargetMutation
|
||||
&& string.Equals(
|
||||
observation.Path,
|
||||
harness.LauncherName,
|
||||
StringComparison.Ordinal))
|
||||
{
|
||||
Assert.True(File.Exists(harness.LauncherPath));
|
||||
throw new InvalidOperationException("failpoint");
|
||||
}
|
||||
});
|
||||
|
||||
SelfUpdateApplyEntry first = apply[0];
|
||||
string payload = Path.Combine(
|
||||
harness.Manager.GetPayloadDirectory(staged.TransactionId),
|
||||
first.Path.Replace('/', Path.DirectorySeparatorChar));
|
||||
string target = Path.Combine(
|
||||
harness.Target,
|
||||
first.Path.Replace('/', Path.DirectorySeparatorChar));
|
||||
string backup = Path.Combine(
|
||||
harness.Manager.GetBackupDirectory(staged.TransactionId),
|
||||
first.Path.Replace('/', Path.DirectorySeparatorChar));
|
||||
Directory.CreateDirectory(Path.GetDirectoryName(backup)!);
|
||||
File.Move(target, backup);
|
||||
File.Move(payload, target);
|
||||
|
||||
SelfUpdatePlan recovered = await harness.Manager.RecoverApplyingAsync(harness.Target);
|
||||
InvalidOperationException failure = await Assert.ThrowsAsync<InvalidOperationException>(
|
||||
() => faulting.ApplyPendingAsync(harness.Target));
|
||||
SelfUpdatePlan recovered = Assert.IsType<SelfUpdatePlan>(
|
||||
await harness.Manager.LoadPendingAsync());
|
||||
|
||||
Assert.Equal("failpoint", failure.Message);
|
||||
Assert.Equal(SelfUpdatePlanState.Staged, recovered.State);
|
||||
Assert.Equal("old-launcher", await File.ReadAllTextAsync(harness.LauncherPath));
|
||||
Assert.Equal("old-support", await File.ReadAllTextAsync(harness.SupportPath));
|
||||
Assert.Equal("new-launcher", await File.ReadAllTextAsync(Path.Combine(
|
||||
harness.Manager.GetPayloadDirectory(staged.TransactionId),
|
||||
harness.Manager.GetPayloadDirectory(recovered.TransactionId),
|
||||
harness.LauncherName)));
|
||||
}
|
||||
|
||||
|
|
@ -219,8 +253,8 @@ public sealed class LauncherSelfUpdateManagerTests : IDisposable
|
|||
|
||||
Assert.False(confirmation.ShouldExit);
|
||||
Assert.Empty(confirmation.RemainingArguments);
|
||||
Assert.True(harness.Manager.IsConfirmed(applied.TransactionId));
|
||||
await harness.Manager.CompleteConfirmedAsync(applied.TransactionId, harness.Target);
|
||||
Assert.False(File.Exists(harness.Manager.PendingPlanPath));
|
||||
Assert.False(harness.Manager.IsConfirmed(applied.TransactionId));
|
||||
}
|
||||
|
||||
private sealed class Harness : IDisposable
|
||||
|
|
@ -229,9 +263,11 @@ public sealed class LauncherSelfUpdateManagerTests : IDisposable
|
|||
private readonly HttpClient _http = new();
|
||||
private readonly byte[] _archive;
|
||||
private readonly ReleaseArtifact _artifact;
|
||||
private readonly string _root;
|
||||
|
||||
public Harness(string root)
|
||||
{
|
||||
_root = root;
|
||||
Target = Path.Combine(root, "published launcher");
|
||||
Directory.CreateDirectory(Target);
|
||||
Rid = LauncherRuntimeIdentity.DetectRid();
|
||||
|
|
@ -270,6 +306,25 @@ public sealed class LauncherSelfUpdateManagerTests : IDisposable
|
|||
progress: null,
|
||||
CancellationToken.None);
|
||||
|
||||
public Task<SelfUpdateStageResult> StageAsync(string version, byte[] archive)
|
||||
{
|
||||
_server.Add("launcher.zip", archive);
|
||||
return Manager.StageAsync(
|
||||
LauncherVersion.Parse(version),
|
||||
Rid,
|
||||
new ReleaseArtifact(
|
||||
_server.UriFor("launcher.zip"),
|
||||
UpdateTestData.Sha256(archive),
|
||||
archive.LongLength),
|
||||
Target,
|
||||
progress: null,
|
||||
CancellationToken.None);
|
||||
}
|
||||
|
||||
public LauncherSelfUpdateManager CreateManagerWithObserver(
|
||||
Action<SelfUpdateApplyObservation> observer) =>
|
||||
new(UpdateTestData.Paths(_root), _http, null, observer);
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_http.Dispose();
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue