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

@ -55,6 +55,42 @@ public sealed class LauncherOrchestratorTests : IDisposable
using UpdateSessionBarrier.ExclusiveLease update = barrier.AcquireExclusive();
}
[Fact]
public async Task DisposeKeepsUpdateLeaseUntilLiveChildIsObservedTerminal()
{
var supervisors = new BlockingStopSupervisorFactory();
var barrier = new UpdateSessionBarrier(_paths.DataDirectory);
LauncherOrchestrator orchestrator = CreateOrchestrator(
supervisorFactory: supervisors,
updateSessionBarrier: barrier);
try
{
_ = await orchestrator.LaunchAsync(
"Local ACE",
"testaccount",
"+Acdream",
LaunchMode.Headless);
BlockingStopSupervisor supervisor = Assert.Single(supervisors.Created);
Task disposal = Task.Run(orchestrator.Dispose);
Assert.True(supervisor.StopEntered.Wait(TimeSpan.FromSeconds(5)));
Assert.False(disposal.IsCompleted);
Assert.Throws<LauncherUpdateException>(barrier.AcquireExclusive);
supervisor.AllowTerminal.Set();
await disposal.WaitAsync(TimeSpan.FromSeconds(5));
using UpdateSessionBarrier.ExclusiveLease update = barrier.AcquireExclusive();
Assert.Equal(LauncherSessionState.Exited, supervisor.State);
Assert.True(supervisor.Disposed);
}
finally
{
supervisors.AllowEveryStop();
orchestrator.Dispose();
}
}
[Fact]
public void SnapshotProjectsTheFullHierarchyWithoutTheCredential()
{
@ -756,6 +792,59 @@ public sealed class LauncherOrchestratorTests : IDisposable
}
}
private sealed class BlockingStopSupervisorFactory : ILauncherProcessSupervisorFactory
{
public List<BlockingStopSupervisor> Created { get; } = [];
public ILauncherProcessSupervisor Create()
{
var supervisor = new BlockingStopSupervisor();
Created.Add(supervisor);
return supervisor;
}
public void AllowEveryStop()
{
foreach (BlockingStopSupervisor supervisor in Created)
{
supervisor.AllowTerminal.Set();
}
}
}
private sealed class BlockingStopSupervisor : ILauncherProcessSupervisor
{
public ManualResetEventSlim StopEntered { get; } = new(false);
public ManualResetEventSlim AllowTerminal { get; } = new(false);
public LauncherSessionState State { get; private set; } =
LauncherSessionState.Starting;
public int? ExitCode { get; private set; }
public bool Disposed { get; private set; }
public event EventHandler<LauncherSessionState>? StateChanged;
public void Start(LauncherProcessSpec spec, string? password)
{
State = LauncherSessionState.Running;
StateChanged?.Invoke(this, State);
}
public void Stop(TimeSpan timeout)
{
StopEntered.Set();
AllowTerminal.Wait();
State = LauncherSessionState.Exited;
ExitCode = 0;
StateChanged?.Invoke(this, State);
}
public void Dispose() => Disposed = true;
}
private sealed class QueueStatusSourceFactory : IStatusEventSourceFactory
{
public List<QueueStatusSource> Created { get; } = [];