From c0764b4882d5e8705e142a0e993612f79d5f93a7 Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Thu, 15 Oct 2026 14:19:16 -0500 Subject: [PATCH] fix hang on dfhack exit on windows This fixed a lifelong bug in the implementation of DFHack's console on WIndows: calling `FreeConsole` while another thread is waiting in `ReadConsoleInput` on the same console has undefined behavior in Windows, and recent releases of WIndows _changed the behavior_ in a manner that is detrimental to our use. This change adds a Windows event object that the console now waits for in parallel with the console read handle. Shutdown signals the event object, causing the console thread to exit cleanly when the core wants the console to shut down. The console never waits in `ReadConsoleInput` as this is only called when console input is known to be available. The same problem would theoretically exist for Linux, but the Linux implementation has always had a pipe (used as an expensive standin for a event object as Linux doesn't have event objects that can be waited for in parallel with file handles) to yield the same result. Fixes #6003 --- docs/changelog.txt | 1 + library/Console-windows.cpp | 20 +++++++++++++++++--- 2 files changed, 18 insertions(+), 3 deletions(-) diff --git a/docs/changelog.txt b/docs/changelog.txt index 2d21901ca3..39232d081a 100644 --- a/docs/changelog.txt +++ b/docs/changelog.txt @@ -62,6 +62,7 @@ Template for new versions: ## Fixes - Fixed ``scriptmanager.getModStatePath()`` causing scripts to crash when called - `prospect`: don't warn about a missing magma sea in the pre-embark estimate when the world was generated without one +- Core: Windows console will no longer hang on close (on some builds of Windows) ## Misc Improvements - `overlay`: errors raised by a bad C function result count or corrupted Lua call stack are now reported as Lua errors instead of crashing the game diff --git a/library/Console-windows.cpp b/library/Console-windows.cpp index 7dce13d56d..bb6b1fc8fb 100644 --- a/library/Console-windows.cpp +++ b/library/Console-windows.cpp @@ -73,6 +73,7 @@ namespace DFHack rawmode = 0; console_in = 0; console_out = 0; + exit_event = 0; ConsoleWindow = 0; default_attributes = 0; state = con_unclaimed; @@ -281,16 +282,21 @@ namespace DFHack inf.dwCursorPosition.X = (SHORT)plen; SetConsoleCursorPosition(console_out, inf.dwCursorPosition); + // exit_event comes first so shutdown wins if input is also + // pending. The console input handle is signaled whenever its + // input queue is non-empty, so the read below cannot block. + const HANDLE wait_handles[] = { exit_event, console_in }; while (1) { INPUT_RECORD rec; DWORD count; lock->unlock(); - if (ReadConsoleInputA(console_in, &rec, 1, &count) == 0) { - lock->lock(); + DWORD wait_result = WaitForMultipleObjects(2, wait_handles, FALSE, INFINITE); + lock->lock(); + if (wait_result != WAIT_OBJECT_0 + 1 + || ReadConsoleInputA(console_in, &rec, 1, &count) == 0) { return Console::SHUTDOWN; } - lock->lock(); if (rec.EventType != KEY_EVENT || !rec.Event.KeyEvent.bKeyDown) continue; switch (rec.Event.KeyEvent.wVirtualKeyCode) @@ -396,6 +402,7 @@ namespace DFHack int rawmode; HANDLE console_in; HANDLE console_out; + HANDLE exit_event; HWND ConsoleWindow; HWND MainWindow; WORD default_attributes; @@ -479,6 +486,9 @@ bool Console::init(bool) d->console_owned = AllocConsole() != FALSE; d->ConsoleWindow = GetConsoleWindow(); wlock = new std::recursive_mutex(); + d->exit_event = CreateEventW(NULL, TRUE, FALSE, NULL); + if (!d->exit_event) + return false; if (d->console_owned) { HMENU hm = GetSystemMenu(d->ConsoleWindow,false); @@ -537,6 +547,10 @@ bool Console::shutdown(void) { assert(inited); std::lock_guard lock{*wlock}; + // Wake any thread waiting on console input so it leaves prompt_loop + // before we detach or reconfigure the console below. + if (d->exit_event) + SetEvent(d->exit_event); if (d->console_owned) { FreeConsole();