Fix deadlock when stopping a game with a debugger request in flight

The WebSocket thread holds lifecycleLock across a whole event handler, and
handlers do their real work through Core_RunOnCPUThread(), which blocks until
the CPU thread drains the queue. Meanwhile PSP_Shutdown() ->
Core_NotifyLifecycle(STOPPING) took that same lock on the CPU thread. So the
debugger thread waited for the CPU thread while the CPU thread waited for the
lock the debugger thread was holding, and neither ever moved.

Drain the CPU queue while waiting for the lock instead of blocking on it. Core
state is still alive at STOPPING (it's notified before CPU_Shutdown), so running
those queued callbacks then is safe, and it lets the debugger thread finish and
release the lock.

Verified with a temporary instrumented build - a 3s sleep inside a handler while
holding lifecycleLock, arranged to overlap the game's shutdown - which hangs
reliably on the old code and exits cleanly with this change.

lifecycleLock stays for now: roughly half the subscribers and all the
broadcasters still read core state directly on the WebSocket thread instead of
going through the queue, and this is what keeps that from racing with teardown.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GZq8ZtJmFY7bkX5FVkr3P9
This commit is contained in:
Henrik Rydgård
2026-08-17 00:34:01 +02:00
co-authored by Claude Opus 5
parent 81511e7a08
commit 707275b46a
2 changed files with 48 additions and 1 deletions
+11 -1
View File
@@ -19,6 +19,8 @@
#include <condition_variable>
#include "Common/Thread/ThreadUtil.h"
#include "Common/TimeUtil.h"
#include "Core/Core.h"
#include "Core/Debugger/WebSocket.h"
#include "Core/Debugger/WebSocket/WebSocketUtils.h"
@@ -108,7 +110,15 @@ static void WebSocketNotifyLifecycle(CoreLifecycle stage) {
if (debuggersConnected > 0) {
DEBUG_LOG(Log::System, "Waiting for debugger to complete on shutdown");
}
lifecycleLock.lock();
// Keep draining the CPU queue while we wait, instead of a plain blocking lock(). We're on
// the CPU thread here, and a debugger thread holding this lock may be parked inside
// Core_RunOnCPUThread() waiting for us to run its callback - so blocking outright means
// neither side can ever move. Core state is still fully alive at this point (STOPPING is
// notified before CPU_Shutdown), so running those callbacks now is safe.
while (!lifecycleLock.try_lock()) {
Core_ProcessCPUQueue();
sleep_ms(1, "debugger-lifecycle");
}
break;
case CoreLifecycle::START_COMPLETE: