From 19bfc075a94437e5e551896ce28f1b999fc71f5e Mon Sep 17 00:00:00 2001 From: Xusheng Date: Wed, 20 May 2026 15:02:44 -0400 Subject: [PATCH 1/3] Hold a strong reference to DebuggerController in detached worker threads The 17 `Launch`/`Attach`/`Connect`/`Go`/`Step*`/`RunTo*`/`Restart`/`Detach`/ `Quit`/`Pause` methods each spawn a detached `std::thread` that runs the corresponding `*AndWait` work, capturing `this` by reference. Nothing keeps the controller alive for the duration of the detached call, so if the controller's refcount drops to zero before the worker exits (e.g. the user closes the tab), the worker dereferences a dangling pointer. Capture a `DbgRef` by value into each lambda so the controller stays alive until the worker thread finishes. Fixes #1083. Co-Authored-By: Claude Opus 4.7 (1M context) --- core/debuggercontroller.cpp | 51 ++++++++++++++++++++++++------------- 1 file changed, 34 insertions(+), 17 deletions(-) diff --git a/core/debuggercontroller.cpp b/core/debuggercontroller.cpp index 1dd3be70..d876a4d1 100644 --- a/core/debuggercontroller.cpp +++ b/core/debuggercontroller.cpp @@ -290,7 +290,8 @@ bool DebuggerController::Launch() if (!CanStartDebgging()) return false; - std::thread([&]() { LaunchAndWait(); }).detach(); + DbgRef self = this; + std::thread([self]() { self->LaunchAndWait(); }).detach(); return true; } @@ -353,7 +354,8 @@ bool DebuggerController::Attach() if (!CanStartDebgging()) return false; - std::thread([&]() { AttachAndWait(); }).detach(); + DbgRef self = this; + std::thread([self]() { self->AttachAndWait(); }).detach(); return true; } @@ -404,7 +406,8 @@ bool DebuggerController::Connect() if (!CanStartDebgging()) return false; - std::thread([&]() { ConnectAndWait(); }).detach(); + DbgRef self = this; + std::thread([self]() { self->ConnectAndWait(); }).detach(); return true; } @@ -543,7 +546,8 @@ bool DebuggerController::Go() if (!CanResumeTarget()) return false; - std::thread([&]() { GoAndWait(); }).detach(); + DbgRef self = this; + std::thread([self]() { self->GoAndWait(); }).detach(); return true; } @@ -554,7 +558,8 @@ bool DebuggerController::GoReverse() if (!CanResumeTarget()) return false; - std::thread([&]() { GoReverseAndWait(); }).detach(); + DbgRef self = this; + std::thread([self]() { self->GoReverseAndWait(); }).detach(); return true; } @@ -817,7 +822,8 @@ bool DebuggerController::StepInto(BNFunctionGraphType il) if (!CanResumeTarget()) return false; - std::thread([&, il]() { StepIntoAndWait(il); }).detach(); + DbgRef self = this; + std::thread([self, il]() { self->StepIntoAndWait(il); }).detach(); return true; } @@ -827,7 +833,8 @@ bool DebuggerController::StepIntoReverse(BNFunctionGraphType il) if (!CanResumeTarget()) return false; - std::thread([&, il]() { StepIntoReverseAndWait(il); }).detach(); + DbgRef self = this; + std::thread([self, il]() { self->StepIntoReverseAndWait(il); }).detach(); return true; } @@ -1078,7 +1085,8 @@ bool DebuggerController::StepOver(BNFunctionGraphType il) if (!CanResumeTarget()) return false; - std::thread([&, il]() { StepOverAndWait(il); }).detach(); + DbgRef self = this; + std::thread([self, il]() { self->StepOverAndWait(il); }).detach(); return true; } @@ -1089,7 +1097,8 @@ bool DebuggerController::StepOverReverse(BNFunctionGraphType il) if (!CanResumeTarget()) return false; - std::thread([&, il]() { StepOverReverseAndWait(il); }).detach(); + DbgRef self = this; + std::thread([self, il]() { self->StepOverReverseAndWait(il); }).detach(); return true; } @@ -1184,7 +1193,8 @@ bool DebuggerController::StepReturn() if (!CanResumeTarget()) return false; - std::thread([&]() { StepReturnAndWait(); }).detach(); + DbgRef self = this; + std::thread([self]() { self->StepReturnAndWait(); }).detach(); return true; } @@ -1195,7 +1205,8 @@ bool DebuggerController::StepReturnReverse() if (!CanResumeTarget()) return false; - std::thread([&]() { StepReturnReverseAndWait(); }).detach(); + DbgRef self = this; + std::thread([self]() { self->StepReturnReverseAndWait(); }).detach(); return true; } @@ -1295,7 +1306,8 @@ bool DebuggerController::RunTo(const std::vector& remoteAddresses) if (!CanResumeTarget()) return false; - std::thread([&, remoteAddresses]() { RunToAndWait(remoteAddresses); }).detach(); + DbgRef self = this; + std::thread([self, remoteAddresses]() { self->RunToAndWait(remoteAddresses); }).detach(); return true; } @@ -1307,7 +1319,8 @@ bool DebuggerController::RunToReverse(const std::vector& remoteAddress if (!CanResumeTarget()) return false; - std::thread([&, remoteAddresses]() { RunToReverseAndWait(remoteAddresses); }).detach(); + DbgRef self = this; + std::thread([self, remoteAddresses]() { self->RunToReverseAndWait(remoteAddresses); }).detach(); return true; } @@ -1457,7 +1470,8 @@ bool DebuggerController::Restart() if (!m_state->IsConnected()) return false; - std::thread([&]() { RestartAndWait(); }).detach(); + DbgRef self = this; + std::thread([self]() { self->RestartAndWait(); }).detach(); return true; } @@ -1517,7 +1531,8 @@ void DebuggerController::Detach() if (!m_state->IsConnected()) return; - std::thread([&]() { DetachAndWait(); }).detach(); + DbgRef self = this; + std::thread([self]() { self->DetachAndWait(); }).detach(); } @@ -1550,7 +1565,8 @@ void DebuggerController::Quit() if (!m_state->IsConnected()) return; - std::thread([&]() { QuitAndWait(); }).detach(); + DbgRef self = this; + std::thread([self]() { self->QuitAndWait(); }).detach(); } @@ -1589,7 +1605,8 @@ bool DebuggerController::Pause() if (!m_state->IsConnected()) return false; - std::thread([&]() { PauseAndWait(); }).detach(); + DbgRef self = this; + std::thread([self]() { self->PauseAndWait(); }).detach(); return true; } From 3882c0df4b1eae1b54fdc5978fabee83b13a132f Mon Sep 17 00:00:00 2001 From: Xusheng Date: Wed, 20 May 2026 15:24:16 -0400 Subject: [PATCH 2/3] Route all DebuggerController operations through a per-controller worker queue Replaces the previous "spawn one std::thread per operation and detach it" pattern with a single persistent worker thread per controller plus a FIFO work queue. Every public Xxx()/XxxAndWait() method now goes through Submit(). The worker thread is owned by the controller, started in the constructor and joined in the destructor (after draining), so any task can no longer outlive the controller it touches -- the lifetime guarantee is structural rather than reliant on per-call DbgRef captures (which this PR removes). Highlights: * Submit() detects re-entrant calls from the worker itself and runs them inline, so e.g. RestartAndWaitOnWorker can call QuitAndWaitOnWorker and LaunchAndWaitOnWorker without deadlocking on the queue. * SubmitAndWait() submits a task and waits on the resulting future. Every XxxAndWait now takes an optional std::chrono::milliseconds timeout, defaulting to milliseconds::max() ("wait forever") so existing callers in ffi.cpp and uinotification.cpp keep their current behavior. If a non-infinite timeout elapses, the engine is signaled to break and we still wait for the in-flight op to settle before returning. * Pause()/PauseAndWait() are special-cased: they bypass the queue and call m_adapter->BreakInto() directly on the caller's thread. The worker is blocked inside ExecuteAdapterAndWait for whatever resume op is in flight; BreakInto unsticks it. PauseAndWait then waits for the worker to drain past the running task by submitting a no-op probe. The existing public/internal split (XxxAndWaitInternal does the actual work; the old XxxAndWait was the lock-Internal-notify wrapper) is preserved. The old wrapper is renamed to XxxAndWaitOnWorker (private) and the new public XxxAndWait(timeout) is a thin SubmitAndWait wrapper around it. This keeps the public API surface identical to today for any caller that doesn't pass a timeout. Out of scope for this PR: adapter-internal threads (DbgEngAdapter::Attach spawned thread, EngineLoop, LLDB EventListener, RSP send/receive loops). Those have their own lifetime concerns and will be handled separately. Co-Authored-By: Claude Opus 4.7 (1M context) --- core/debuggercontroller.cpp | 262 ++++++++++++++++++++++++++++-------- core/debuggercontroller.h | 134 +++++++++++++++--- 2 files changed, 321 insertions(+), 75 deletions(-) diff --git a/core/debuggercontroller.cpp b/core/debuggercontroller.cpp index d876a4d1..7dca3848 100644 --- a/core/debuggercontroller.cpp +++ b/core/debuggercontroller.cpp @@ -39,11 +39,22 @@ DebuggerController::DebuggerController(BinaryViewRef data): BinaryDataNotificati RegisterEventCallback([this](const DebuggerEvent& event) { EventHandler(event); }, "Debugger Core"); m_debuggerEventThread = std::thread([&]{ DebuggerMainThread(); }); + + m_workerShouldExit = false; + m_workerThread = std::thread([this] { WorkerThreadMain(); }); } DebuggerController::~DebuggerController() { + { + std::lock_guard lock(m_workQueueMutex); + m_workerShouldExit = true; + } + m_workQueueCv.notify_all(); + if (m_workerThread.joinable()) + m_workerThread.join(); + m_shouldExit = true; m_cv.notify_all(); if (m_debuggerEventThread.joinable()) @@ -60,6 +71,27 @@ DebuggerController::~DebuggerController() } +void DebuggerController::WorkerThreadMain() +{ + m_workerThreadId = std::this_thread::get_id(); + while (true) + { + std::function task; + { + std::unique_lock lock(m_workQueueMutex); + m_workQueueCv.wait(lock, [this] { + return m_workerShouldExit || !m_workQueue.empty(); + }); + if (m_workerShouldExit && m_workQueue.empty()) + break; + task = std::move(m_workQueue.front()); + m_workQueue.pop(); + } + task(); + } +} + + void DebuggerController::AddBreakpoint(uint64_t address) { m_state->AddBreakpoint(address); @@ -290,8 +322,7 @@ bool DebuggerController::Launch() if (!CanStartDebgging()) return false; - DbgRef self = this; - std::thread([self]() { self->LaunchAndWait(); }).detach(); + Submit([this] { LaunchAndWaitOnWorker(); }); return true; } @@ -330,7 +361,7 @@ DebugStopReason DebuggerController::LaunchAndWaitInternal() } -DebugStopReason DebuggerController::LaunchAndWait() +DebugStopReason DebuggerController::LaunchAndWaitOnWorker() { // This is an API function of the debugger. We only do these checks at the API level. if (!CanStartDebgging()) @@ -348,14 +379,19 @@ DebugStopReason DebuggerController::LaunchAndWait() } +DebugStopReason DebuggerController::LaunchAndWait(std::chrono::milliseconds timeout) +{ + return SubmitAndWait([this] { return LaunchAndWaitOnWorker(); }, timeout); +} + + bool DebuggerController::Attach() { // This is an API function of the debugger. We only do these checks at the API level. if (!CanStartDebgging()) return false; - DbgRef self = this; - std::thread([self]() { self->AttachAndWait(); }).detach(); + Submit([this] { AttachAndWaitOnWorker(); }); return true; } @@ -382,7 +418,7 @@ DebugStopReason DebuggerController::AttachAndWaitInternal() } -DebugStopReason DebuggerController::AttachAndWait() +DebugStopReason DebuggerController::AttachAndWaitOnWorker() { // This is an API function of the debugger. We only do these checks at the API level. if (!CanStartDebgging()) @@ -400,14 +436,19 @@ DebugStopReason DebuggerController::AttachAndWait() } +DebugStopReason DebuggerController::AttachAndWait(std::chrono::milliseconds timeout) +{ + return SubmitAndWait([this] { return AttachAndWaitOnWorker(); }, timeout); +} + + bool DebuggerController::Connect() { // This is an API function of the debugger. We only do these checks at the API level. if (!CanStartDebgging()) return false; - DbgRef self = this; - std::thread([self]() { self->ConnectAndWait(); }).detach(); + Submit([this] { ConnectAndWaitOnWorker(); }); return true; } @@ -434,7 +475,7 @@ DebugStopReason DebuggerController::ConnectAndWaitInternal() } -DebugStopReason DebuggerController::ConnectAndWait() +DebugStopReason DebuggerController::ConnectAndWaitOnWorker() { // This is an API function of the debugger. We only do these checks at the API level. if (!CanStartDebgging()) @@ -452,6 +493,12 @@ DebugStopReason DebuggerController::ConnectAndWait() } +DebugStopReason DebuggerController::ConnectAndWait(std::chrono::milliseconds timeout) +{ + return SubmitAndWait([this] { return ConnectAndWaitOnWorker(); }, timeout); +} + + bool DebuggerController::Execute() { std::unique_lock lock(m_targetControlMutex); @@ -546,8 +593,7 @@ bool DebuggerController::Go() if (!CanResumeTarget()) return false; - DbgRef self = this; - std::thread([self]() { self->GoAndWait(); }).detach(); + Submit([this] { GoAndWaitOnWorker(); }); return true; } @@ -558,14 +604,13 @@ bool DebuggerController::GoReverse() if (!CanResumeTarget()) return false; - DbgRef self = this; - std::thread([self]() { self->GoReverseAndWait(); }).detach(); + Submit([this] { GoReverseAndWaitOnWorker(); }); return true; } -DebugStopReason DebuggerController::GoAndWait() +DebugStopReason DebuggerController::GoAndWaitOnWorker() { // This is an API function of the debugger. We only do these checks at the API level. if (!CanResumeTarget()) @@ -582,7 +627,14 @@ DebugStopReason DebuggerController::GoAndWait() return reason; } -DebugStopReason DebuggerController::GoReverseAndWait() + +DebugStopReason DebuggerController::GoAndWait(std::chrono::milliseconds timeout) +{ + return SubmitAndWait([this] { return GoAndWaitOnWorker(); }, timeout); +} + + +DebugStopReason DebuggerController::GoReverseAndWaitOnWorker() { // This is an API function of the debugger. We only do these checks at the API level. if (!CanResumeTarget()) @@ -600,6 +652,12 @@ DebugStopReason DebuggerController::GoReverseAndWait() } +DebugStopReason DebuggerController::GoReverseAndWait(std::chrono::milliseconds timeout) +{ + return SubmitAndWait([this] { return GoReverseAndWaitOnWorker(); }, timeout); +} + + DebugStopReason DebuggerController::StepIntoIL(BNFunctionGraphType il) { switch (il) @@ -822,8 +880,7 @@ bool DebuggerController::StepInto(BNFunctionGraphType il) if (!CanResumeTarget()) return false; - DbgRef self = this; - std::thread([self, il]() { self->StepIntoAndWait(il); }).detach(); + Submit([this, il] { StepIntoAndWaitOnWorker(il); }); return true; } @@ -833,13 +890,12 @@ bool DebuggerController::StepIntoReverse(BNFunctionGraphType il) if (!CanResumeTarget()) return false; - DbgRef self = this; - std::thread([self, il]() { self->StepIntoReverseAndWait(il); }).detach(); + Submit([this, il] { StepIntoReverseAndWaitOnWorker(il); }); return true; } -DebugStopReason DebuggerController::StepIntoReverseAndWait(BNFunctionGraphType il) +DebugStopReason DebuggerController::StepIntoReverseAndWaitOnWorker(BNFunctionGraphType il) { // This is an API function of the debugger. We only do these checks at the API level. if (!CanResumeTarget()) @@ -856,7 +912,13 @@ DebugStopReason DebuggerController::StepIntoReverseAndWait(BNFunctionGraphType i return reason; } -DebugStopReason DebuggerController::StepIntoAndWait(BNFunctionGraphType il) +DebugStopReason DebuggerController::StepIntoReverseAndWait(BNFunctionGraphType il, + std::chrono::milliseconds timeout) +{ + return SubmitAndWait([this, il] { return StepIntoReverseAndWaitOnWorker(il); }, timeout); +} + +DebugStopReason DebuggerController::StepIntoAndWaitOnWorker(BNFunctionGraphType il) { // This is an API function of the debugger. We only do these checks at the API level. if (!CanResumeTarget()) @@ -873,6 +935,12 @@ DebugStopReason DebuggerController::StepIntoAndWait(BNFunctionGraphType il) return reason; } +DebugStopReason DebuggerController::StepIntoAndWait(BNFunctionGraphType il, + std::chrono::milliseconds timeout) +{ + return SubmitAndWait([this, il] { return StepIntoAndWaitOnWorker(il); }, timeout); +} + DebugStopReason DebuggerController::StepOverIL(BNFunctionGraphType il) { switch (il) @@ -1085,8 +1153,7 @@ bool DebuggerController::StepOver(BNFunctionGraphType il) if (!CanResumeTarget()) return false; - DbgRef self = this; - std::thread([self, il]() { self->StepOverAndWait(il); }).detach(); + Submit([this, il] { StepOverAndWaitOnWorker(il); }); return true; } @@ -1097,14 +1164,13 @@ bool DebuggerController::StepOverReverse(BNFunctionGraphType il) if (!CanResumeTarget()) return false; - DbgRef self = this; - std::thread([self, il]() { self->StepOverReverseAndWait(il); }).detach(); + Submit([this, il] { StepOverReverseAndWaitOnWorker(il); }); return true; } -DebugStopReason DebuggerController::StepOverAndWait(BNFunctionGraphType il) +DebugStopReason DebuggerController::StepOverAndWaitOnWorker(BNFunctionGraphType il) { // This is an API function of the debugger. We only do these checks at the API level. if (!CanResumeTarget()) @@ -1122,7 +1188,14 @@ DebugStopReason DebuggerController::StepOverAndWait(BNFunctionGraphType il) } -DebugStopReason DebuggerController::StepOverReverseAndWait(BNFunctionGraphType il) +DebugStopReason DebuggerController::StepOverAndWait(BNFunctionGraphType il, + std::chrono::milliseconds timeout) +{ + return SubmitAndWait([this, il] { return StepOverAndWaitOnWorker(il); }, timeout); +} + + +DebugStopReason DebuggerController::StepOverReverseAndWaitOnWorker(BNFunctionGraphType il) { // This is an API function of the debugger. We only do these checks at the API level. if (!CanResumeTarget()) @@ -1140,6 +1213,13 @@ DebugStopReason DebuggerController::StepOverReverseAndWait(BNFunctionGraphType i } +DebugStopReason DebuggerController::StepOverReverseAndWait(BNFunctionGraphType il, + std::chrono::milliseconds timeout) +{ + return SubmitAndWait([this, il] { return StepOverReverseAndWaitOnWorker(il); }, timeout); +} + + DebugStopReason DebuggerController::EmulateStepReturnAndWait() { uint64_t address = m_state->IP(); @@ -1193,8 +1273,7 @@ bool DebuggerController::StepReturn() if (!CanResumeTarget()) return false; - DbgRef self = this; - std::thread([self]() { self->StepReturnAndWait(); }).detach(); + Submit([this] { StepReturnAndWaitOnWorker(); }); return true; } @@ -1205,14 +1284,13 @@ bool DebuggerController::StepReturnReverse() if (!CanResumeTarget()) return false; - DbgRef self = this; - std::thread([self]() { self->StepReturnReverseAndWait(); }).detach(); + Submit([this] { StepReturnReverseAndWaitOnWorker(); }); return true; } -DebugStopReason DebuggerController::StepReturnAndWait() +DebugStopReason DebuggerController::StepReturnAndWaitOnWorker() { // This is an API function of the debugger. We only do these checks at the API level. if (!CanResumeTarget()) @@ -1230,7 +1308,13 @@ DebugStopReason DebuggerController::StepReturnAndWait() } -DebugStopReason DebuggerController::StepReturnReverseAndWait() +DebugStopReason DebuggerController::StepReturnAndWait(std::chrono::milliseconds timeout) +{ + return SubmitAndWait([this] { return StepReturnAndWaitOnWorker(); }, timeout); +} + + +DebugStopReason DebuggerController::StepReturnReverseAndWaitOnWorker() { // This is an API function of the debugger. We only do these checks at the API level. if (!CanResumeTarget()) @@ -1248,6 +1332,12 @@ DebugStopReason DebuggerController::StepReturnReverseAndWait() } +DebugStopReason DebuggerController::StepReturnReverseAndWait(std::chrono::milliseconds timeout) +{ + return SubmitAndWait([this] { return StepReturnReverseAndWaitOnWorker(); }, timeout); +} + + DebugStopReason DebuggerController::RunToAndWaitInternal(const std::vector& remoteAddresses) { m_userRequestedBreak = false; @@ -1306,8 +1396,7 @@ bool DebuggerController::RunTo(const std::vector& remoteAddresses) if (!CanResumeTarget()) return false; - DbgRef self = this; - std::thread([self, remoteAddresses]() { self->RunToAndWait(remoteAddresses); }).detach(); + Submit([this, remoteAddresses] { RunToAndWaitOnWorker(remoteAddresses); }); return true; } @@ -1319,14 +1408,13 @@ bool DebuggerController::RunToReverse(const std::vector& remoteAddress if (!CanResumeTarget()) return false; - DbgRef self = this; - std::thread([self, remoteAddresses]() { self->RunToReverseAndWait(remoteAddresses); }).detach(); + Submit([this, remoteAddresses] { RunToReverseAndWaitOnWorker(remoteAddresses); }); return true; } -DebugStopReason DebuggerController::RunToAndWait(const std::vector& remoteAddresses) +DebugStopReason DebuggerController::RunToAndWaitOnWorker(const std::vector& remoteAddresses) { // This is an API function of the debugger. We only do these checks at the API level. if (!CanResumeTarget()) @@ -1344,7 +1432,15 @@ DebugStopReason DebuggerController::RunToAndWait(const std::vector& re } -DebugStopReason DebuggerController::RunToReverseAndWait(const std::vector& remoteAddresses) +DebugStopReason DebuggerController::RunToAndWait(const std::vector& remoteAddresses, + std::chrono::milliseconds timeout) +{ + return SubmitAndWait( + [this, remoteAddresses] { return RunToAndWaitOnWorker(remoteAddresses); }, timeout); +} + + +DebugStopReason DebuggerController::RunToReverseAndWaitOnWorker(const std::vector& remoteAddresses) { // This is an API function of the debugger. We only do these checks at the API level. if (!CanResumeTarget()) @@ -1362,6 +1458,14 @@ DebugStopReason DebuggerController::RunToReverseAndWait(const std::vector& remoteAddresses, + std::chrono::milliseconds timeout) +{ + return SubmitAndWait( + [this, remoteAddresses] { return RunToReverseAndWaitOnWorker(remoteAddresses); }, timeout); +} + + bool DebuggerController::CreateDebuggerBinaryView() { BinaryViewRef data = GetData(); @@ -1470,19 +1574,26 @@ bool DebuggerController::Restart() if (!m_state->IsConnected()) return false; - DbgRef self = this; - std::thread([self]() { self->RestartAndWait(); }).detach(); + Submit([this] { RestartAndWaitOnWorker(); }); return true; } -DebugStopReason DebuggerController::RestartAndWait() +DebugStopReason DebuggerController::RestartAndWaitOnWorker() { if (!m_state->IsConnected()) return InvalidStatusOrOperation; - QuitAndWait(); - return LaunchAndWait(); + // Bypass the public sync wrappers; we are already on the worker and want to + // run these inline without re-entering Submit. + QuitAndWaitOnWorker(); + return LaunchAndWaitOnWorker(); +} + + +DebugStopReason DebuggerController::RestartAndWait(std::chrono::milliseconds timeout) +{ + return SubmitAndWait([this] { return RestartAndWaitOnWorker(); }, timeout); } @@ -1531,12 +1642,11 @@ void DebuggerController::Detach() if (!m_state->IsConnected()) return; - DbgRef self = this; - std::thread([self]() { self->DetachAndWait(); }).detach(); + Submit([this] { DetachAndWaitOnWorker(); }); } -void DebuggerController::DetachAndWait() +void DebuggerController::DetachAndWaitOnWorker() { bool locked = false; if (m_targetControlMutex.try_lock()) @@ -1560,17 +1670,22 @@ void DebuggerController::DetachAndWait() } +void DebuggerController::DetachAndWait(std::chrono::milliseconds timeout) +{ + SubmitAndWait([this] { DetachAndWaitOnWorker(); }, timeout); +} + + void DebuggerController::Quit() { if (!m_state->IsConnected()) return; - DbgRef self = this; - std::thread([self]() { self->QuitAndWait(); }).detach(); + Submit([this] { QuitAndWaitOnWorker(); }); } -void DebuggerController::QuitAndWait() +void DebuggerController::QuitAndWaitOnWorker() { bool locked = false; if (m_targetControlMutex.try_lock()) @@ -1585,7 +1700,11 @@ void DebuggerController::QuitAndWait() if (m_state->IsRunning()) { - // We must pause the target if it is currently running, at least for DbgEngAdapter + // We must pause the target if it is currently running, at least for DbgEngAdapter. + // In the queue model the worker is the only thread running adapter operations, + // so reaching this branch implies a re-entrant call (e.g. Restart). PauseAndWait + // dispatches BreakInto out-of-band; the running op (if any) will settle and we + // proceed to issue the Quit below. PauseAndWait(); } @@ -1600,13 +1719,25 @@ void DebuggerController::QuitAndWait() } +void DebuggerController::QuitAndWait(std::chrono::milliseconds timeout) +{ + SubmitAndWait([this] { QuitAndWaitOnWorker(); }, timeout); +} + + bool DebuggerController::Pause() { if (!m_state->IsConnected()) return false; - DbgRef self = this; - std::thread([self]() { self->PauseAndWait(); }).detach(); + // Out-of-band: signal the engine to break on the caller's thread. The worker + // is presumed to be blocked inside ExecuteAdapterAndWait for whatever op is + // in flight (Go/Step/RunTo/etc.); when the engine receives the break it will + // report a stop, the worker's op will return, and its OnWorker wrapper will + // call NotifyStopped. We do not queue any work for the worker here. + m_userRequestedBreak = true; + if (m_adapter) + m_adapter->BreakInto(); return true; } @@ -1619,15 +1750,30 @@ DebugStopReason DebuggerController::PauseAndWaitInternal() } -DebugStopReason DebuggerController::PauseAndWait() +DebugStopReason DebuggerController::PauseAndWait(std::chrono::milliseconds timeout) { if (!m_state->IsConnected()) return InvalidStatusOrOperation; - auto reason = PauseAndWaitInternal(); - if ((reason != ProcessExited) && (reason != InternalError)) - NotifyStopped(reason); - return reason; + m_userRequestedBreak = true; + if (m_adapter) + m_adapter->BreakInto(); + + // Wait for the currently-running worker task (if any) to finish processing + // the break. Submitting a no-op gives us a future that resolves once the + // worker drains past whatever was in flight at the time of the break. + auto fut = Submit([] {}); + if (timeout == std::chrono::milliseconds::max()) + { + fut.wait(); + } + else if (fut.wait_for(timeout) != std::future_status::ready) + { + // BreakInto has already been signaled; there's nothing else to do. + return InternalError; + } + + return DebugStopReason::UserRequestedBreak; } diff --git a/core/debuggercontroller.h b/core/debuggercontroller.h index f298d902..3a44f59a 100644 --- a/core/debuggercontroller.h +++ b/core/debuggercontroller.h @@ -168,6 +168,27 @@ namespace BinaryNinjaDebugger { DebugStopReason RunToAndWaitInternal(const std::vector &remoteAddresses); DebugStopReason RunToReverseAndWaitInternal(const std::vector &remoteAddresses); + // Worker-thread bodies. Each runs on m_workerThread (via Submit) and performs the + // existing lock-Internal-notify wrapper. The public `XxxAndWait(timeout)` methods + // below submit one of these and wait on the resulting future. + DebugStopReason LaunchAndWaitOnWorker(); + DebugStopReason AttachAndWaitOnWorker(); + DebugStopReason ConnectAndWaitOnWorker(); + DebugStopReason GoAndWaitOnWorker(); + DebugStopReason GoReverseAndWaitOnWorker(); + DebugStopReason StepIntoAndWaitOnWorker(BNFunctionGraphType il); + DebugStopReason StepIntoReverseAndWaitOnWorker(BNFunctionGraphType il); + DebugStopReason StepOverAndWaitOnWorker(BNFunctionGraphType il); + DebugStopReason StepOverReverseAndWaitOnWorker(BNFunctionGraphType il); + DebugStopReason StepReturnAndWaitOnWorker(); + DebugStopReason StepReturnReverseAndWaitOnWorker(); + DebugStopReason RunToAndWaitOnWorker(const std::vector& remoteAddresses); + DebugStopReason RunToReverseAndWaitOnWorker(const std::vector& remoteAddresses); + DebugStopReason RestartAndWaitOnWorker(); + void DetachAndWaitOnWorker(); + void QuitAndWaitOnWorker(); + DebugStopReason PauseAndWaitOnWorker(); + // Whether we can start debugging, e.g., launch/attach/connec to a target bool CanStartDebgging(); // Whether we can resume the execution of the target, including stepping. @@ -201,6 +222,64 @@ namespace BinaryNinjaDebugger { std::thread m_debuggerEventThread; void DebuggerMainThread(); + // Worker queue: serializes all controller operations on a single thread. + // Replaces the per-op `std::thread(...).detach()` pattern. Tasks submitted from any + // thread run in order on m_workerThread; lifetime is owned and joined in the destructor. + // If Submit is called from the worker thread itself, the task runs inline to avoid + // deadlock when an operation needs to invoke another (e.g. Restart calls Quit + Launch). + std::thread m_workerThread; + std::thread::id m_workerThreadId; + std::mutex m_workQueueMutex; + std::condition_variable m_workQueueCv; + std::queue> m_workQueue; + std::atomic_bool m_workerShouldExit; + void WorkerThreadMain(); + + template + auto Submit(F&& f) -> std::future> + { + using R = std::invoke_result_t; + auto task = std::make_shared>(std::forward(f)); + auto future = task->get_future(); + + if (std::this_thread::get_id() == m_workerThreadId) + { + // Re-entrant call from the worker thread itself. Run inline so an outer + // operation can invoke an inner one without deadlocking on the queue. + (*task)(); + return future; + } + + { + std::lock_guard lock(m_workQueueMutex); + if (m_workerShouldExit) + return future; // future is left unset; caller's get() will throw broken_promise + m_workQueue.push([task]() { (*task)(); }); + } + m_workQueueCv.notify_one(); + return future; + } + + // Submit a worker task and block the caller until the task completes (or the timeout + // elapses, in which case the engine is signaled to break and we still wait for the + // in-flight op to settle before returning). A timeout of milliseconds::max() means + // "wait forever" and bypasses wait_for entirely (avoids overflow inside the stdlib). + template + auto SubmitAndWait(F&& f, std::chrono::milliseconds timeout) + -> std::invoke_result_t + { + auto fut = Submit(std::forward(f)); + if (timeout != std::chrono::milliseconds::max()) + { + if (fut.wait_for(timeout) != std::future_status::ready) + { + if (m_adapter) + m_adapter->BreakInto(); + } + } + return fut.get(); + } + std::unique_ptr m_uiCallbacks; uint64_t m_oldViewBase, m_newViewBase; @@ -351,23 +430,44 @@ namespace BinaryNinjaDebugger { DebugStopReason ExecuteAdapterAndWait(const DebugAdapterOperation operation); // Synchronous APIs - DebugStopReason LaunchAndWait(); - DebugStopReason GoAndWait(); - DebugStopReason GoReverseAndWait(); - DebugStopReason AttachAndWait(); - DebugStopReason RestartAndWait(); - DebugStopReason ConnectAndWait(); - DebugStopReason StepIntoAndWait(BNFunctionGraphType il = NormalFunctionGraph); - DebugStopReason StepIntoReverseAndWait(BNFunctionGraphType il = NormalFunctionGraph); - DebugStopReason StepOverAndWait(BNFunctionGraphType il = NormalFunctionGraph); - DebugStopReason StepOverReverseAndWait(BNFunctionGraphType il); - DebugStopReason StepReturnAndWait(); - DebugStopReason StepReturnReverseAndWait(); - DebugStopReason RunToAndWait(const std::vector& remoteAddresses); - DebugStopReason RunToReverseAndWait(const std::vector& remoteAddresses); - DebugStopReason PauseAndWait(); - void DetachAndWait(); - void QuitAndWait(); + // Synchronous APIs. They submit the operation to the worker thread and block the + // caller until it completes (or the optional timeout elapses, in which case the + // engine is signaled to break and the call returns once the in-flight op settles). + // Default timeout is "wait forever" so existing callers do not need to change. + DebugStopReason LaunchAndWait( + std::chrono::milliseconds timeout = std::chrono::milliseconds::max()); + DebugStopReason GoAndWait( + std::chrono::milliseconds timeout = std::chrono::milliseconds::max()); + DebugStopReason GoReverseAndWait( + std::chrono::milliseconds timeout = std::chrono::milliseconds::max()); + DebugStopReason AttachAndWait( + std::chrono::milliseconds timeout = std::chrono::milliseconds::max()); + DebugStopReason RestartAndWait( + std::chrono::milliseconds timeout = std::chrono::milliseconds::max()); + DebugStopReason ConnectAndWait( + std::chrono::milliseconds timeout = std::chrono::milliseconds::max()); + DebugStopReason StepIntoAndWait(BNFunctionGraphType il = NormalFunctionGraph, + std::chrono::milliseconds timeout = std::chrono::milliseconds::max()); + DebugStopReason StepIntoReverseAndWait(BNFunctionGraphType il = NormalFunctionGraph, + std::chrono::milliseconds timeout = std::chrono::milliseconds::max()); + DebugStopReason StepOverAndWait(BNFunctionGraphType il = NormalFunctionGraph, + std::chrono::milliseconds timeout = std::chrono::milliseconds::max()); + DebugStopReason StepOverReverseAndWait(BNFunctionGraphType il, + std::chrono::milliseconds timeout = std::chrono::milliseconds::max()); + DebugStopReason StepReturnAndWait( + std::chrono::milliseconds timeout = std::chrono::milliseconds::max()); + DebugStopReason StepReturnReverseAndWait( + std::chrono::milliseconds timeout = std::chrono::milliseconds::max()); + DebugStopReason RunToAndWait(const std::vector& remoteAddresses, + std::chrono::milliseconds timeout = std::chrono::milliseconds::max()); + DebugStopReason RunToReverseAndWait(const std::vector& remoteAddresses, + std::chrono::milliseconds timeout = std::chrono::milliseconds::max()); + DebugStopReason PauseAndWait( + std::chrono::milliseconds timeout = std::chrono::milliseconds::max()); + void DetachAndWait( + std::chrono::milliseconds timeout = std::chrono::milliseconds::max()); + void QuitAndWait( + std::chrono::milliseconds timeout = std::chrono::milliseconds::max()); // getters DebugAdapter* GetAdapter() { return m_adapter; } From b0619e79892a2ab21770f988ce1802854ca523fd Mon Sep 17 00:00:00 2001 From: Xusheng Date: Wed, 20 May 2026 16:40:36 -0400 Subject: [PATCH 3/3] Route adapter stops through an internal channel; remove dispatcher's stop machinery Adapter stops are now an internal signal from the adapter thread to the worker, not events on the public dispatcher queue. PostDebuggerEvent intercepts AdapterStoppedEventType and delivers the stop reason to a new mutex+condvar pair (m_adapterStopMutex / m_adapterStopCv / m_adapterStopPending). The worker consumes them via two paths: 1. In-flight ops: ExecuteAdapterAndWait now claims the channel for its entire duration (m_inAdapterWait = true), kicks off the adapter operation, then loops on WaitForAdapterStop. The conditional- breakpoint check moves here -- on a false condition we silently resume via m_adapter->Go() and wait again, without releasing the channel. This eliminates the old "drive m_adapter->Go() from the dispatcher thread" hack and the m_suppressResumeEvent flag that papered over it. 2. Spontaneous stops: when the user types e.g. `si` directly into the LLDB REPL while no controller op is in flight, m_inAdapterWait is false, so PostDebuggerEvent queues HandleSpontaneousAdapterStop on the worker -- which updates caches and calls NotifyStopped, the same effect as the dispatcher synthesis the old code did at debuggercontroller.cpp:2279. Target-exit / detach events continue through the dispatcher queue (they're user-visible) but ALSO signal the adapter-stop channel so an in-flight WaitForAdapterStop unblocks when the target dies. The dispatcher (DebuggerMainThread) no longer has any AdapterStoppedEventType-specific code paths. m_lastAdapterStopEventConsumed and m_suppressResumeEvent are removed; the temporary RegisterEventCallback/Semaphore pattern inside ExecuteAdapterAndWait is replaced by the direct WaitForAdapterStop wait. AdapterStoppedEventType remains in the public enum so adapters keep posting it the same way -- the change is purely in how the controller routes it once it arrives at PostDebuggerEvent. Adapter implementations need no changes. Stacked on the worker-queue refactor (this PR / #1087). Implements #1089. Co-Authored-By: Claude Opus 4.7 (1M context) --- core/debuggercontroller.cpp | 248 ++++++++++++++++++++++-------------- core/debuggercontroller.h | 21 ++- 2 files changed, 167 insertions(+), 102 deletions(-) diff --git a/core/debuggercontroller.cpp b/core/debuggercontroller.cpp index 7dca3848..397bb73a 100644 --- a/core/debuggercontroller.cpp +++ b/core/debuggercontroller.cpp @@ -52,6 +52,8 @@ DebuggerController::~DebuggerController() m_workerShouldExit = true; } m_workQueueCv.notify_all(); + // Wake any in-flight WaitForAdapterStop so the worker can observe shutdown. + m_adapterStopCv.notify_all(); if (m_workerThread.joinable()) m_workerThread.join(); @@ -2159,11 +2161,47 @@ bool DebuggerController::RemoveEventCallbackInternal(size_t index) void DebuggerController::PostDebuggerEvent(const DebuggerEvent& event) { - // During conditional breakpoint auto-resume, suppress the ResumeEventType that adapters - // post inside Go(). The target is already considered running by the UI, and posting this - // event from the dispatcher thread would trigger a re-entrant warning. - if (m_suppressResumeEvent && event.type == ResumeEventType) + // Adapter stops are an internal signal to the worker, not a user-facing event. + // Route them to the adapter-stop channel and skip the public dispatcher queue. + if (event.type == AdapterStoppedEventType) + { + DebugStopReason reason = event.data.targetStoppedData.reason; + bool inWait; + { + std::lock_guard lk(m_adapterStopMutex); + inWait = m_inAdapterWait; + if (inWait) + m_adapterStopPending = reason; + } + if (inWait) + { + m_adapterStopCv.notify_all(); + } + else + { + // No controller op is in flight — the adapter stopped on its own (e.g. + // the user typed `si` directly into the LLDB REPL). Queue a handler on + // the worker to update caches and synthesize a TargetStoppedEvent. + Submit([this, reason] { HandleSpontaneousAdapterStop(reason); }); + } return; + } + + // Target-exit / detach are user-facing events that ALSO need to unblock any + // in-flight WaitForAdapterStop (the engine isn't going to issue a separate stop). + if (event.type == TargetExitedEventType || event.type == DetachedEventType) + { + bool inWait; + { + std::lock_guard lk(m_adapterStopMutex); + inWait = m_inAdapterWait; + if (inWait) + m_adapterStopPending = ProcessExited; + } + if (inWait) + m_adapterStopCv.notify_all(); + // Fall through: still goes through the public dispatcher queue. + } auto pending = std::make_shared(); pending->event = event; @@ -2189,6 +2227,67 @@ void DebuggerController::PostDebuggerEvent(const DebuggerEvent& event) } +DebugStopReason DebuggerController::WaitForAdapterStop() +{ + std::unique_lock lk(m_adapterStopMutex); + m_adapterStopCv.wait(lk, [this] { + return m_adapterStopPending.has_value() || m_workerShouldExit; + }); + if (m_workerShouldExit && !m_adapterStopPending.has_value()) + return InternalError; + DebugStopReason reason = *m_adapterStopPending; + m_adapterStopPending = std::nullopt; + return reason; +} + + +bool DebuggerController::ShouldSilentResumeAfterStop() +{ + // Only breakpoint stops are candidates for silent resume on a false condition. + // Step operations always surface, even if they land on a breakpoint. + bool isStepOperation = (m_lastOperation == DebugAdapterStepInto) + || (m_lastOperation == DebugAdapterStepOver) + || (m_lastOperation == DebugAdapterStepReturn) + || (m_lastOperation == DebugAdapterStepIntoReverse) + || (m_lastOperation == DebugAdapterStepOverReverse) + || (m_lastOperation == DebugAdapterStepReturnReverse); + if (isStepOperation) + return false; + + m_state->SetConnectionStatus(DebugAdapterConnectedStatus); + m_state->SetExecutionStatus(DebugAdapterPausedStatus); + m_state->MarkDirty(); + m_state->UpdateCaches(); + AddRegisterValuesToExpressionParser(); + AddModuleValuesToExpressionParser(); + + uint64_t ip = m_state->IP(); + if (!m_state->GetBreakpoints()->ContainsAbsolute(ip)) + return false; + if (m_userRequestedBreak) + return false; + if (EvaluateBreakpointCondition(ip)) + return false; + + return true; +} + + +void DebuggerController::HandleSpontaneousAdapterStop(DebugStopReason reason) +{ + // The adapter reported a stop with no controller op in flight. This is the + // case the dispatcher previously synthesized a TargetStoppedEvent for at + // `debuggercontroller.cpp:2279` in the pre-refactor code. + m_state->SetConnectionStatus(DebugAdapterConnectedStatus); + m_state->SetExecutionStatus(DebugAdapterPausedStatus); + m_state->MarkDirty(); + m_state->UpdateCaches(); + AddRegisterValuesToExpressionParser(); + AddModuleValuesToExpressionParser(); + NotifyStopped(reason); +} + + void DebuggerController::DebuggerMainThread() { m_shouldExit = false; @@ -2212,49 +2311,11 @@ void DebuggerController::DebuggerMainThread() callbackLock.unlock(); auto event = current->event; - if (event.type == AdapterStoppedEventType) - m_lastAdapterStopEventConsumed = false; - if (event.type == AdapterStoppedEventType && - event.data.targetStoppedData.reason == Breakpoint) - { - // update the caches so registers are available for condition evaluation - m_state->SetConnectionStatus(DebugAdapterConnectedStatus); - m_state->SetExecutionStatus(DebugAdapterPausedStatus); - m_state->MarkDirty(); - m_state->UpdateCaches(); - AddRegisterValuesToExpressionParser(); - AddModuleValuesToExpressionParser(); - - // skip conditional breakpoint evaluation for step operations - when the user explicitly - // steps onto a breakpoint, they expect to stop there regardless of the condition. - bool isStepOperation = (m_lastOperation == DebugAdapterStepInto) - || (m_lastOperation == DebugAdapterStepOver) - || (m_lastOperation == DebugAdapterStepReturn) - || (m_lastOperation == DebugAdapterStepIntoReverse) - || (m_lastOperation == DebugAdapterStepOverReverse) - || (m_lastOperation == DebugAdapterStepReturnReverse); - - if (uint64_t ip = m_state->IP(); - !isStepOperation && m_state->GetBreakpoints()->ContainsAbsolute(ip)) - { - if (!EvaluateBreakpointCondition(ip) && !m_userRequestedBreak) - { - m_lastAdapterStopEventConsumed = true; - current->done.set_value(); - // Using m_adapter->Go() directly instead of Go() to avoid mutex deadlock - // since we're already inside ExecuteAdapterAndWait's event processing. - // Suppress the ResumeEventType that some adapters post synchronously inside - // Go() — the UI already considers the target running, and posting from the - // dispatcher thread would be unexpected. - m_suppressResumeEvent = true; - m_adapter->Go(); - m_suppressResumeEvent = false; - m_state->SetExecutionStatus(DebugAdapterRunningStatus); - continue; - } - } - } + // AdapterStoppedEventType no longer reaches the dispatcher: PostDebuggerEvent + // intercepts it and routes the reason to the worker's adapter-stop channel. + // Conditional-breakpoint silent-resume and spontaneous-stop synthesis now live + // in ExecuteAdapterAndWait / HandleSpontaneousAdapterStop on the worker. DebuggerEvent eventToSend = event; if ((eventToSend.type == TargetStoppedEventType) && !m_initialBreakpointSeen) @@ -2273,29 +2334,6 @@ void DebuggerController::DebuggerMainThread() cb.function(eventToSend); } - // If the current event is an AdapterStoppedEvent, and it is not consumed by any callback, then the adapter - // stop is not caused by the debugger core. This can happen when the user run a "ni" command directly. - // Notify a target stop reason in this case. - if (event.type == AdapterStoppedEventType && !m_lastAdapterStopEventConsumed) - { - DebuggerEvent stopEvent = event; - stopEvent.type = TargetStoppedEventType; - if (!m_initialBreakpointSeen) - { - m_initialBreakpointSeen = true; - stopEvent.data.targetStoppedData.reason = InitialBreakpoint; - } - for (const DebuggerEventCallback& cb : eventCallbacks) - { - std::unique_lock callbackLock2(m_callbackMutex); - if (m_disabledCallbacks.find(cb.index) != m_disabledCallbacks.end()) - continue; - - callbackLock2.unlock(); - cb.function(stopEvent); - } - } - CleanUpDisabledEvent(); current->done.set_value(); } @@ -2857,30 +2895,17 @@ DebugStopReason DebuggerController::ExecuteAdapterAndWait(const DebugAdapterOper } } - Semaphore sem; - DebugStopReason reason = UnknownReason; - size_t callback = RegisterEventCallback( - [&](const DebuggerEvent& event) { - switch (event.type) - { - case AdapterStoppedEventType: - reason = event.data.targetStoppedData.reason; - sem.Release(); - break; - // It is a little awkward to add two cases for these events, but we must take them into account, - // since after we resume the target, the target can either or exit. - case TargetExitedEventType: - case DetachedEventType: - // There is no DebugStopReason for "detach", so we use ProcessExited for now - reason = ProcessExited; - sem.Release(); - break; - default: - break; - } - m_lastAdapterStopEventConsumed = true; - }, - "WaitForAdapterStop"); + // Claim the adapter-stop channel for the duration of this call. Any AdapterStoppedEvent + // posted by the adapter from now until we clear m_inAdapterWait is delivered to + // WaitForAdapterStop below, not treated as spontaneous. We hold this across the + // entire silent-resume loop so that an adapter stop between iterations (after we + // kick off m_adapter->Go() for a false breakpoint condition) is still consumed + // by us, not synthesized as a spontaneous stop. + { + std::lock_guard lk(m_adapterStopMutex); + m_inAdapterWait = true; + m_adapterStopPending = std::nullopt; + } m_lastOperation = operation; @@ -2952,12 +2977,41 @@ DebugStopReason DebuggerController::ExecuteAdapterAndWait(const DebugAdapterOper ok = true; } - if (ok) - sem.Wait(); - else + DebugStopReason reason = UnknownReason; + if (!ok) + { reason = InternalError; + } + else + { + // Loop: wait for the adapter to stop. If the stop is a breakpoint whose + // condition evaluates to false (and the user didn't explicitly step or + // request a break), silently resume and wait again. Otherwise return. + while (true) + { + reason = WaitForAdapterStop(); + if (reason == ProcessExited || reason == InternalError) + break; + if (reason == Breakpoint && ShouldSilentResumeAfterStop()) + { + m_state->SetExecutionStatus(DebugAdapterRunningStatus); + if (!m_adapter || !m_adapter->Go()) + { + reason = InternalError; + break; + } + continue; + } + break; + } + } + + { + std::lock_guard lk(m_adapterStopMutex); + m_inAdapterWait = false; + m_adapterStopPending = std::nullopt; + } - RemoveEventCallback(callback); if ((operation != DebugAdapterPause) && (operation != DebugAdapterQuit) && (operation != DebugAdapterDetach)) m_adapterMutex.unlock(); else diff --git a/core/debuggercontroller.h b/core/debuggercontroller.h index 3a44f59a..32ba4491 100644 --- a/core/debuggercontroller.h +++ b/core/debuggercontroller.h @@ -22,6 +22,7 @@ limitations under the License. #include #include #include +#include #include #include "ffi_global.h" #include "refcountobject.h" @@ -119,11 +120,21 @@ namespace BinaryNinjaDebugger { bool m_userRequestedBreak = false; DebugAdapterOperation m_lastOperation = DebugAdapterGo; - bool m_lastAdapterStopEventConsumed = true; - - // When true, ResumeEventType events are suppressed in PostDebuggerEvent. - // Used during conditional breakpoint auto-resume to avoid posting events from the dispatcher thread. - bool m_suppressResumeEvent = false; + // Adapter-stop channel: internal signal from the adapter thread to the worker. + // AdapterStoppedEventType posted via PostDebuggerEvent is intercepted and routed + // here rather than dispatched through the public event queue. WaitForAdapterStop + // blocks on m_adapterStopCv until either an adapter stop arrives or shutdown is + // requested. m_inAdapterWait is true for the entire duration of an in-flight + // ExecuteAdapterAndWait call (including the silent-resume loop between iterations + // for conditional breakpoints) so that any stop during that window is consumed + // by WaitForAdapterStop and not treated as spontaneous. + std::mutex m_adapterStopMutex; + std::condition_variable m_adapterStopCv; + std::optional m_adapterStopPending; + bool m_inAdapterWait = false; + DebugStopReason WaitForAdapterStop(); + void HandleSpontaneousAdapterStop(DebugStopReason reason); + bool ShouldSilentResumeAfterStop(); bool m_inputFileLoaded = false; bool m_initialBreakpointSeen = false;