[Lldb-commits] [lldb] [lldb][Windows] Report all queued exceptions in one lldb-server stop (PR #227353)

Charles Zablit via lldb-commits lldb-commits at lists.llvm.org
Wed Oct 14 04:27:20 PDT 2026


https://github.com/charles-zablit updated https://github.com/llvm/llvm-project/pull/227353

>From 474631e80dc588ef5483e426abd24894e2feb473 Mon Sep 17 00:00:00 2001
From: Charles Zablit <c_zablit at apple.com>
Date: Tue, 13 Oct 2026 14:15:51 +0100
Subject: [PATCH] [lldb][Windows] Report debug events queued together in one
 stop

---
 .../lldb/Host/common/NativeProcessProtocol.h  |  5 ++-
 .../Process/Windows/Common/DebuggerThread.cpp | 37 ++++++++++++++++--
 .../Process/Windows/Common/DebuggerThread.h   |  4 ++
 .../Process/Windows/Common/IDebugDelegate.h   |  3 ++
 .../Windows/Common/NativeProcessWindows.cpp   | 38 ++++++++++++++++---
 .../Windows/Common/NativeProcessWindows.h     | 17 +++++++++
 6 files changed, 94 insertions(+), 10 deletions(-)

diff --git a/lldb/include/lldb/Host/common/NativeProcessProtocol.h b/lldb/include/lldb/Host/common/NativeProcessProtocol.h
index 209cb068aba215..da1fa26abefa9e 100644
--- a/lldb/include/lldb/Host/common/NativeProcessProtocol.h
+++ b/lldb/include/lldb/Host/common/NativeProcessProtocol.h
@@ -544,8 +544,11 @@ class NativeProcessProtocol {
 
   NativeThreadProtocol *GetThreadByIDUnlocked(lldb::tid_t tid);
 
-private:
+  /// Notify the delegates of \p state. For plugins that set a state with
+  /// SetState(state, /*notify_delegates=*/false) and report it later.
   void SynchronouslyNotifyProcessStateChanged(lldb::StateType state);
+
+private:
   llvm::Expected<SoftwareBreakpoint>
   EnableSoftwareBreakpoint(lldb::addr_t addr, uint32_t size_hint);
 };
diff --git a/lldb/source/Plugins/Process/Windows/Common/DebuggerThread.cpp b/lldb/source/Plugins/Process/Windows/Common/DebuggerThread.cpp
index cb1e31b4334f54..6dd051e574885b 100644
--- a/lldb/source/Plugins/Process/Windows/Common/DebuggerThread.cpp
+++ b/lldb/source/Plugins/Process/Windows/Common/DebuggerThread.cpp
@@ -262,6 +262,13 @@ void DebuggerThread::ContinueAsyncException(ExceptionResult result) {
   m_exception_pred.SetValue(result, eBroadcastAlways);
 }
 
+void DebuggerThread::ReportDeferredStopAndWaitForResume() {
+  m_dll_event_pred.SetValue(false, eBroadcastNever);
+  m_debug_delegate->ReportDeferredStop();
+  if (!m_is_shutting_down)
+    m_dll_event_pred.WaitForValueEqualTo(true);
+}
+
 void DebuggerThread::ContinueAsyncDllEvent() {
   m_dll_event_pred.SetValue(true, eBroadcastAlways);
 }
@@ -279,10 +286,28 @@ void DebuggerThread::DebugLoop() {
   Log *log = GetLog(WindowsLog::Event);
   DEBUG_EVENT dbe = {};
   bool should_debug = true;
+  constexpr unsigned max_drained_events = 64;
+  unsigned drained_events = 0;
   LLDB_LOG_VERBOSE(log, "Entering WaitForDebugEventEx loop");
   while (should_debug) {
+    bool draining = !m_is_shutting_down && m_debug_delegate->HasDeferredStop();
+    if (draining && drained_events >= max_drained_events) {
+      LLDB_LOG(log, "reporting the stop after taking {0} queued debug events",
+               drained_events);
+      ReportDeferredStopAndWaitForResume();
+      draining = false;
+    }
+    if (!draining)
+      drained_events = 0;
     LLDB_LOG_VERBOSE(log, "Calling WaitForDebugEvent");
-    BOOL wait_result = g_wait_for_debug_event(&dbe, INFINITE);
+    BOOL wait_result = g_wait_for_debug_event(&dbe, draining ? 0 : INFINITE);
+    if (draining) {
+      if (!wait_result) {
+        ReportDeferredStopAndWaitForResume();
+        continue;
+      }
+      ++drained_events;
+    }
     if (wait_result) {
       DWORD continue_status = DBG_CONTINUE;
       bool shutting_down = m_is_shutting_down;
@@ -447,6 +472,8 @@ DebuggerThread::HandleExceptionEvent(const EXCEPTION_DEBUG_INFO &info,
   // result is what the wait below returns.
   if (result != ExceptionResult::BreakInDebugger)
     ContinueAsyncException(result);
+  else
+    m_debug_delegate->ReportDeferredStop();
 
   LLDB_LOG(log, "waiting for ExceptionPred != BreakInDebugger");
   result = *m_exception_pred.WaitForValueNotEqualTo(
@@ -864,8 +891,10 @@ DebuggerThread::HandleLoadDllEvent(const LOAD_DLL_DEBUG_INFO &info,
   if (info.hFile != nullptr)
     ::CloseHandle(info.hFile);
 
-  if (action == DllEventAction::ParkDebugLoop && !m_is_shutting_down.load())
+  if (action == DllEventAction::ParkDebugLoop && !m_is_shutting_down.load()) {
+    m_debug_delegate->ReportDeferredStop();
     m_dll_event_pred.WaitForValueEqualTo(true);
+  }
   return DBG_CONTINUE;
 }
 
@@ -879,8 +908,10 @@ DebuggerThread::HandleUnloadDllEvent(const UNLOAD_DLL_DEBUG_INFO &info,
   m_dll_event_pred.SetValue(false, eBroadcastNever);
   DllEventAction action = m_debug_delegate->OnUnloadDll(
       reinterpret_cast<lldb::addr_t>(info.lpBaseOfDll), thread_id);
-  if (action == DllEventAction::ParkDebugLoop && !m_is_shutting_down.load())
+  if (action == DllEventAction::ParkDebugLoop && !m_is_shutting_down.load()) {
+    m_debug_delegate->ReportDeferredStop();
     m_dll_event_pred.WaitForValueEqualTo(true);
+  }
   return DBG_CONTINUE;
 }
 
diff --git a/lldb/source/Plugins/Process/Windows/Common/DebuggerThread.h b/lldb/source/Plugins/Process/Windows/Common/DebuggerThread.h
index a9fa3ec0c7599d..cc2d3f740b77e9 100644
--- a/lldb/source/Plugins/Process/Windows/Common/DebuggerThread.h
+++ b/lldb/source/Plugins/Process/Windows/Common/DebuggerThread.h
@@ -55,6 +55,10 @@ class DebuggerThread : public std::enable_shared_from_this<DebuggerThread> {
   void ContinueAsyncDllEvent();
 
 private:
+  /// Have the delegate report the stop it collected, then wait until the
+  /// client resumes before taking the next debug event.
+  void ReportDeferredStopAndWaitForResume();
+
   void FreeProcessHandles();
   void DebugLoop();
   ExceptionResult HandleExceptionEvent(const EXCEPTION_DEBUG_INFO &info,
diff --git a/lldb/source/Plugins/Process/Windows/Common/IDebugDelegate.h b/lldb/source/Plugins/Process/Windows/Common/IDebugDelegate.h
index cecd58a15f9c2c..f5c9b53447f3b9 100644
--- a/lldb/source/Plugins/Process/Windows/Common/IDebugDelegate.h
+++ b/lldb/source/Plugins/Process/Windows/Common/IDebugDelegate.h
@@ -37,6 +37,9 @@ class IDebugDelegate {
                                    lldb::tid_t thread_id) = 0;
   virtual DllEventAction OnUnloadDll(lldb::addr_t module_addr,
                                      lldb::tid_t thread_id) = 0;
+  virtual bool HasDeferredStop() { return false; }
+  /// Report the stop the delegate holds, if any.
+  virtual void ReportDeferredStop() {}
   virtual void OnDebugString(lldb::addr_t debug_string_addr, bool is_unicode,
                              uint16_t length_lower_word) = 0;
   virtual void OnDebuggerError(const Status &error, uint32_t type) = 0;
diff --git a/lldb/source/Plugins/Process/Windows/Common/NativeProcessWindows.cpp b/lldb/source/Plugins/Process/Windows/Common/NativeProcessWindows.cpp
index 9d2cdf9f58510c..6885ca024b06a7 100644
--- a/lldb/source/Plugins/Process/Windows/Common/NativeProcessWindows.cpp
+++ b/lldb/source/Plugins/Process/Windows/Common/NativeProcessWindows.cpp
@@ -533,6 +533,9 @@ void NativeProcessWindows::OnExitProcess(uint32_t exit_code) {
   bool started = m_session_data && m_session_data->m_initial_stop_received;
   ProcessDebugger::OnExitProcess(exit_code);
 
+  // The exit replaces a stop that has not been reported yet.
+  m_deferred_stop = false;
+
   // No signal involved.  It is just an exit event.
   WaitStatus wait_status(WaitStatus::Exit, exit_code);
   SetExitStatus(wait_status, started);
@@ -583,6 +586,29 @@ void NativeProcessWindows::OnDebuggerConnected(lldb::addr_t image_base) {
       *this, m_session_data->m_debugger->GetMainThread()));
 }
 
+void NativeProcessWindows::DeferStop() {
+  if (GetState() == eStateStopped) {
+    if (!m_deferred_stop)
+      LLDB_LOG(GetLog(WindowsLog::Exception),
+               "debug event on thread {0:x} while a reported stop is current",
+               GetCurrentThreadID());
+    return;
+  }
+  SetState(eStateStopped, /*notify_delegates=*/false);
+  m_deferred_stop = true;
+  m_deferred_stop_tid = GetCurrentThreadID();
+}
+
+void NativeProcessWindows::ReportDeferredStop() {
+  llvm::sys::ScopedLock lock(m_mutex);
+  if (!m_deferred_stop)
+    return;
+  m_deferred_stop = false;
+  if (GetThreadByID(m_deferred_stop_tid))
+    SetCurrentThreadID(m_deferred_stop_tid);
+  SynchronouslyNotifyProcessStateChanged(eStateStopped);
+}
+
 bool NativeProcessWindows::RewindTrapOfRemovedBreakpoint(
     const ExceptionRecord &record) {
   if (!m_initial_stop_seen)
@@ -646,7 +672,7 @@ NativeProcessWindows::HandleSingleStepException(const ExceptionRecord &record) {
     StopThread(record.GetThreadID(), StopReason::eStopReasonTrace);
   }
 
-  SetState(eStateStopped, true);
+  DeferStop();
   return ExceptionResult::MaskException;
 }
 
@@ -665,7 +691,7 @@ NativeProcessWindows::HandleBreakpointException(const ExceptionRecord &record) {
       StopThread(thread_id, StopReason::eStopReasonBreakpoint);
       // The current PC is AFTER the BP opcode, on all architectures.
       reg_ctx.SetPC(reg_ctx.GetPC() - GetSoftwareBreakpointPCOffset());
-      SetState(eStateStopped, true);
+      DeferStop();
       return ExceptionResult::MaskException;
     }
 
@@ -692,7 +718,7 @@ NativeProcessWindows::HandleBreakpointException(const ExceptionRecord &record) {
                     exception_addr)
                 .str();
         StopThread(thread_id, StopReason::eStopReasonWatchpoint, desc);
-        SetState(eStateStopped, true);
+        DeferStop();
         return ExceptionResult::MaskException;
       }
     }
@@ -750,7 +776,7 @@ NativeProcessWindows::HandleBreakpointException(const ExceptionRecord &record) {
     SetCurrentThreadID(thread_id);
     if (NativeThreadWindows *injected = GetThreadByID(thread_id))
       injected->SetStopReason(signal_info, "interrupt");
-    SetState(eStateStopped, true);
+    DeferStop();
     return ExceptionResult::BreakInDebugger;
   }
 
@@ -767,7 +793,7 @@ NativeProcessWindows::HandleBreakpointException(const ExceptionRecord &record) {
                              record.GetExceptionValue(), exception_addr)
                          .str();
   StopThread(thread_id, StopReason::eStopReasonException, std::move(desc));
-  SetState(eStateStopped, true);
+  DeferStop();
   return ExceptionResult::MaskException;
 }
 
@@ -793,7 +819,7 @@ NativeProcessWindows::HandleGenericException(bool first_chance,
   StopThread(record.GetThreadID(), StopReason::eStopReasonException,
              std::move(desc));
 
-  SetState(eStateStopped, true);
+  DeferStop();
   return ExceptionResult::BreakInDebugger;
 }
 
diff --git a/lldb/source/Plugins/Process/Windows/Common/NativeProcessWindows.h b/lldb/source/Plugins/Process/Windows/Common/NativeProcessWindows.h
index e79596fecc9f80..79046dd6846a46 100644
--- a/lldb/source/Plugins/Process/Windows/Common/NativeProcessWindows.h
+++ b/lldb/source/Plugins/Process/Windows/Common/NativeProcessWindows.h
@@ -140,6 +140,8 @@ class NativeProcessWindows : public NativeProcessProtocol,
                              lldb::tid_t thread_id) override;
   void OnDebugString(lldb::addr_t debug_string_addr, bool is_unicode,
                      uint16_t length_lower_word) override;
+  bool HasDeferredStop() const { return m_deferred_stop; }
+  void ReportDeferredStop();
 
 protected:
   NativeThreadWindows *GetThreadByID(lldb::tid_t thread_id);
@@ -167,6 +169,9 @@ class NativeProcessWindows : public NativeProcessProtocol,
   NativeProcessWindows(lldb::pid_t pid, int terminal_fd,
                        NativeDelegate &delegate, llvm::Error &E);
 
+  /// Stop the process without notifying the delegate yet.
+  void DeferStop();
+
   /// A breakpoint trap whose debug event was only delivered after the client
   /// removed that breakpoint: the thread executed the trap before the stop
   /// that removed it, but its event was held back until it ran again. Moves
@@ -196,6 +201,14 @@ class NativeProcessWindows : public NativeProcessProtocol,
   /// Set when Halt() / Interrupt() schedules a DebugBreakProcess injection.
   bool m_pending_halt = false;
 
+  /// Set by DeferStop(), cleared once the stop is reported or the process
+  /// exits.
+  bool m_deferred_stop = false;
+
+  /// The thread whose debug event started the deferred stop. Reported as the
+  /// stop's thread even if later events made another thread current.
+  lldb::tid_t m_deferred_stop_tid = LLDB_INVALID_THREAD_ID;
+
   bool m_client_supports_libraries_read = false;
 
   /// PseudoConsole for the lldb-server stdio-forwarding path.
@@ -264,6 +277,10 @@ class NativeDebugDelegate : public IDebugDelegate {
     return m_process.OnDebuggerError(error, type);
   }
 
+  bool HasDeferredStop() override { return m_process.HasDeferredStop(); }
+
+  void ReportDeferredStop() override { m_process.ReportDeferredStop(); }
+
 private:
   NativeProcessWindows &m_process;
 };



More information about the lldb-commits mailing list