Merge campaign-launcher-406: fix #406 — truthful client exit self-report + bounded stderr capture
The crashed-client 'graceful' status line was the CLIENT's own Dispose-path self-report, not the launcher's observation; Run() now latches the escaping failure and the shutdown report writes reason:'crashed'. Sessions also gain a bounded client.err.log beside status.jsonl on both spawn paths. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
commit
0b05b58514
13 changed files with 1150 additions and 38 deletions
|
|
@ -137,6 +137,14 @@ public sealed class GameWindow :
|
|||
_constructionCleanup = new();
|
||||
private readonly AcDream.App.World.WorldEnvironmentController _worldEnvironment;
|
||||
private readonly GameWindowLifetime _lifetime = new();
|
||||
// fix #406: set by Run()'s own catch the instant an exception escapes
|
||||
// the Silk.NET frame loop, BEFORE it is rethrown and unwinds through
|
||||
// Program.cs's `using var window = ...` (which calls Dispose() —
|
||||
// therefore CompleteShutdown() — while that exception is still in
|
||||
// flight). CompleteShutdown consults this so a crash is never reported
|
||||
// as the hardcoded "exited{code:0,reason:graceful}" the resource
|
||||
// teardown transaction's own convergence would otherwise imply.
|
||||
private Exception? _runFailure;
|
||||
private readonly DisplayFramePacingController _displayFramePacing;
|
||||
private readonly RuntimeSettingsController _runtimeSettings;
|
||||
|
||||
|
|
@ -820,6 +828,10 @@ public sealed class GameWindow :
|
|||
catch (Exception failure)
|
||||
{
|
||||
_constructionCleanup.RetainFrom(failure);
|
||||
// fix #406: latch BEFORE rethrowing — Dispose() (and therefore
|
||||
// CompleteShutdown) can run mid-unwind of this exact exception,
|
||||
// via Program.cs's `using var window = ...`.
|
||||
_runFailure = failure;
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
|
@ -1698,7 +1710,7 @@ public sealed class GameWindow :
|
|||
// OnClosing() native-window-close-request pass) represents the
|
||||
// process actually being done.
|
||||
if (releaseNativeWindow)
|
||||
_statusWriter.Exited(_options.SessionId ?? "app", 0, "graceful");
|
||||
ReportExited(report);
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -1715,12 +1727,41 @@ public sealed class GameWindow :
|
|||
Console.Error.WriteLine($"[shutdown] {report.Error}");
|
||||
|
||||
if (releaseNativeWindow)
|
||||
ReportExited(report);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Writes the ONE terminal "exited" status event for this session
|
||||
/// (fix #406). A resource-shutdown transaction can converge cleanly
|
||||
/// (<paramref name="report"/>'s own <see cref="GameWindowLifetimeReport.Status"/>
|
||||
/// says nothing about this) even though this <see cref="Dispose"/> call
|
||||
/// is running mid-unwind of an exception that escaped
|
||||
/// <see cref="Run"/>'s frame loop and is about to terminate the process
|
||||
/// via the CLR's unhandled-exception path — <see cref="_runFailure"/>
|
||||
/// is the one signal that actually distinguishes those two cases.
|
||||
/// Before this fix every such crash wrote the exact same
|
||||
/// "exited{code:0,reason:graceful}" as a real graceful shutdown,
|
||||
/// sending any launcher-side diagnosis in the wrong direction (#406).
|
||||
/// </summary>
|
||||
private void ReportExited(GameWindowLifetimeReport report)
|
||||
{
|
||||
string sessionId = _options.SessionId ?? "app";
|
||||
if (_runFailure is not null)
|
||||
{
|
||||
_statusWriter.Exited(
|
||||
_options.SessionId ?? "app",
|
||||
1,
|
||||
"shutdown-incomplete");
|
||||
// The real OS-level exit code (e.g. 0xE0434352 on Windows for
|
||||
// an unhandled .NET exception) is produced by the runtime AFTER
|
||||
// this method returns and the exception keeps propagating — it
|
||||
// cannot be predicted from here. "crashed" is the truthful,
|
||||
// platform-independent classification; the launcher's own
|
||||
// process supervisor observes the real OS exit code separately.
|
||||
_statusWriter.Exited(sessionId, 1, "crashed");
|
||||
return;
|
||||
}
|
||||
|
||||
if (report.Status == GameWindowLifetimeStatus.Complete)
|
||||
_statusWriter.Exited(sessionId, 0, "graceful");
|
||||
else
|
||||
_statusWriter.Exited(sessionId, 1, "shutdown-incomplete");
|
||||
}
|
||||
|
||||
private GameWindowShutdownRoots CaptureShutdownRoots() => new(
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue