fix(launcher): Campaign LA LA3 narrow review fixes

This commit is contained in:
Erik 2026-08-14 17:06:47 +02:00
parent 26feba8186
commit 347a1a5d16
9 changed files with 495 additions and 145 deletions

View file

@ -1,3 +1,5 @@
using System.Runtime.ExceptionServices;
namespace AcDream.Launcher.Core.Launching;
/// <summary>
@ -10,19 +12,41 @@ public sealed class LauncherProcessSupervisor : IDisposable
{
private readonly ILauncherChildProcessFactory _factory;
private readonly object _gate = new();
private readonly Queue<LauncherSessionState> _pendingStateChanges = [];
private ILauncherChildProcess? _process;
private LauncherSessionState _state = LauncherSessionState.Starting;
private int? _exitCode;
private bool _publishingStateChanges;
public LauncherProcessSupervisor(ILauncherChildProcessFactory? factory = null)
{
_factory = factory ?? new SystemChildProcessFactory();
}
public LauncherSessionState State { get; private set; } = LauncherSessionState.Starting;
public LauncherSessionState State
{
get
{
lock (_gate)
{
return _state;
}
}
}
/// <summary>Set once <see cref="State"/> reaches
/// <see cref="LauncherSessionState.Exited"/>; null before then.
/// </summary>
public int? ExitCode { get; private set; }
public int? ExitCode
{
get
{
lock (_gate)
{
return _exitCode;
}
}
}
/// <summary>Fires on every <see cref="LauncherSessionState"/>
/// transition, in order.</summary>
@ -148,14 +172,15 @@ public sealed class LauncherProcessSupervisor : IDisposable
private void OnProcessExited(object? sender, EventArgs e)
{
ILauncherChildProcess? process;
int? exitCode;
lock (_gate)
{
process = _process;
exitCode = _process is { HasExited: true } process
? process.ExitCode
: null;
}
ExitCode = process is { HasExited: true } ? process.ExitCode : null;
SetState(LauncherSessionState.Exited);
SetState(LauncherSessionState.Exited, exitCode);
}
/// <summary>
@ -171,19 +196,79 @@ public sealed class LauncherProcessSupervisor : IDisposable
/// — without this guard, "Running" would silently resurrect a
/// process that has already reported its exit.
/// </summary>
private void SetState(LauncherSessionState state)
private void SetState(LauncherSessionState state, int? exitCode = null)
{
bool publish;
lock (_gate)
{
if (State == LauncherSessionState.Exited)
if (_state == LauncherSessionState.Exited)
{
return;
}
State = state;
_state = state;
if (state == LauncherSessionState.Exited)
{
_exitCode = exitCode;
}
_pendingStateChanges.Enqueue(state);
publish = !_publishingStateChanges;
if (publish)
{
_publishingStateChanges = true;
}
}
StateChanged?.Invoke(this, state);
if (publish)
{
PublishPendingStateChanges();
}
}
/// <summary>
/// Drains state notifications through one publisher. Transition storage
/// stays under <see cref="_gate"/>, but user callbacks run outside it so
/// they may re-enter the supervisor or wait for another thread reading
/// state without deadlocking. A concurrent/re-entrant transition queues
/// behind the notification already in flight, preserving storage order in
/// the externally observed event stream.
/// </summary>
private void PublishPendingStateChanges()
{
Exception? firstException = null;
while (true)
{
LauncherSessionState state;
lock (_gate)
{
if (_pendingStateChanges.Count == 0)
{
_publishingStateChanges = false;
break;
}
state = _pendingStateChanges.Dequeue();
}
try
{
StateChanged?.Invoke(this, state);
}
catch (Exception ex)
{
// Preserve the previous propagation behavior, but finish
// publishing any transition already committed concurrently
// (especially terminal Exited) before rethrowing the first
// observer failure to the initiating caller.
firstException ??= ex;
}
}
if (firstException is not null)
{
ExceptionDispatchInfo.Capture(firstException).Throw();
}
}
public void Dispose()