End the process on a second Ctrl+C after a save Commit in flight finishes, not through the default handler's ExitProcess.

win32.c con_ctrl returned FALSE for a second Ctrl+C, Ctrl+Break or close while the first one's end ran, so the next
handler, the default one, ran ExitProcess at once: a save Commit in flight was cut (the next start rolls it back)
and every library's detach ran. Only a game started from a console has the handler; the launcher starts it with no
console.

The second event now waits for a Commit under way through fs_save_crash_wait (2 s at most, no lock; weak in win32.c
for builds without fs_host.c, as dispatch.c has it) and ends the process with plat_exit and STATUS_CONTROL_C_EXIT, the
code the default handler gives. The force-quit stays; it takes up to 2 s longer when a Commit runs.

Not reproduced before the change (a play-test item from a console: Ctrl+C twice). By review; fatal_exit_check,
logq_check and save_share_check, which link win32.c beside a weak or a strong fs_save_crash_wait, and the compile
checks pass.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
chrg6
2026-11-13 17:44:33 -05:00
parent dc94771051
commit 0efa9a4c78

View File

@@ -1332,13 +1332,22 @@ static DWORD con_mode;
static void (*con_close)(const char *what);
static volatile LONG con_closing;
// fs_host.c: a save Commit under way finishes first, 2 s at most and without a lock; builds without it have no saves
__attribute__((weak)) void fs_save_crash_wait(void) {}
// Runs on a thread the system starts for the event. A close returns only when on_close does, which it must not.
static BOOL WINAPI con_ctrl(DWORD ev) {
const char *what = ev == CTRL_CLOSE_EVENT ? "closed" : ev == CTRL_C_EVENT ? "Ctrl+C"
: ev == CTRL_BREAK_EVENT ? "Ctrl+Break" : NULL;
// log-off and shutdown reach a process with a window as WM_ENDSESSION (gfx_window_win.c), not here; a second event
// while on_close runs takes the default path, which ends the process at once
if (!what || !con_close || InterlockedExchange(&con_closing, 1)) return FALSE;
// log-off and shutdown reach a process with a window as WM_ENDSESSION (gfx_window_win.c), not here
if (!what || !con_close) return FALSE;
if (InterlockedExchange(&con_closing, 1)) {
// A second event while on_close runs: the player wants out now. Left to the default handler, its ExitProcess
// cut a save Commit in flight and ran every library's detach; the Commit finishes first, and the process ends
// with the code the default handler gives (STATUS_CONTROL_C_EXIT).
fs_save_crash_wait();
plat_exit((int)0xC000013Au);
}
con_close(what);
return TRUE;
}