[Lldb-commits] [lldb] 51e5b6c - [lldb-dap] Migrating 'stopped' event to structured types. (#176273)

via lldb-commits lldb-commits at lists.llvm.org
Tue Feb 3 08:51:40 PST 2026


Author: John Harrison
Date: 2026-02-03T08:43:41-08:00
New Revision: 51e5b6c6acc01a41581ac5631edcc7fc974310b5

URL: https://github.com/llvm/llvm-project/commit/51e5b6c6acc01a41581ac5631edcc7fc974310b5
DIFF: https://github.com/llvm/llvm-project/commit/51e5b6c6acc01a41581ac5631edcc7fc974310b5.diff

LOG: [lldb-dap] Migrating 'stopped' event to structured types. (#176273)

Updates the 'stopped' event to use structure types. 

Additionally, I adjusted the description to include the full
`GetStopDescription` that can have more details.

Added: 
    lldb/test/API/tools/lldb-dap/stopped-events/Makefile
    lldb/test/API/tools/lldb-dap/stopped-events/TestDAP_stopped_events.py
    lldb/test/API/tools/lldb-dap/stopped-events/main.cpp
    lldb/unittests/DAP/ProtocolEventsTest.cpp

Modified: 
    lldb/packages/Python/lldbsuite/test/tools/lldb-dap/dap_server.py
    lldb/packages/Python/lldbsuite/test/tools/lldb-dap/lldbdap_testcase.py
    lldb/test/API/tools/lldb-dap/breakpoint/TestDAP_setExceptionBreakpoints.py
    lldb/test/API/tools/lldb-dap/exception/objc/TestDAP_exception_objc.py
    lldb/test/API/tools/lldb-dap/threads/TestDAP_threads.py
    lldb/tools/lldb-dap/EventHelper.cpp
    lldb/tools/lldb-dap/JSONUtils.cpp
    lldb/tools/lldb-dap/JSONUtils.h
    lldb/tools/lldb-dap/Protocol/ProtocolEvents.cpp
    lldb/tools/lldb-dap/Protocol/ProtocolEvents.h
    lldb/tools/lldb-dap/SBAPIExtras.h
    lldb/unittests/DAP/CMakeLists.txt

Removed: 
    


################################################################################
diff  --git a/lldb/packages/Python/lldbsuite/test/tools/lldb-dap/dap_server.py b/lldb/packages/Python/lldbsuite/test/tools/lldb-dap/dap_server.py
index 14391db74302c..32e37c502e358 100644
--- a/lldb/packages/Python/lldbsuite/test/tools/lldb-dap/dap_server.py
+++ b/lldb/packages/Python/lldbsuite/test/tools/lldb-dap/dap_server.py
@@ -300,6 +300,7 @@ def __init__(
         self.stopped_thread: Optional[dict] = None
         self.thread_stacks: Optional[Dict[int, List[dict]]]
         self.thread_stop_reasons: Dict[str, Any] = {}
+        self.focused_tid: Optional[int] = None
         self.frame_scopes: Dict[str, Any] = {}
         # keyed by breakpoint id
         self.resolved_breakpoints: dict[int, Breakpoint] = {}
@@ -539,6 +540,8 @@ def _handle_event(self, packet: Event) -> None:
             self._process_stopped()
             tid = body["threadId"]
             self.thread_stop_reasons[tid] = body
+            if "preserveFocusHint" not in body or not body["preserveFocusHint"]:
+                self.focused_tid = tid
         elif event.startswith("progress"):
             # Progress events come in as 'progressStart', 'progressUpdate',
             # and 'progressEnd' events. Keep these around in case test
@@ -599,6 +602,7 @@ def _process_continued(self, all_threads_continued: bool):
         self.frame_scopes = {}
         if all_threads_continued:
             self.thread_stop_reasons = {}
+            self.focused_tid = None
 
     def _update_verified_breakpoints(self, breakpoints: List[Breakpoint]):
         for bp in breakpoints:
@@ -1593,7 +1597,7 @@ def request_threads(self):
                 tid = thread["id"]
                 if tid in self.thread_stop_reasons:
                     thread_stop_info = self.thread_stop_reasons[tid]
-                    copy_keys = ["reason", "description", "text"]
+                    copy_keys = ["reason", "description", "text", "hitBreakpointIds"]
                     for key in copy_keys:
                         if key in thread_stop_info:
                             thread[key] = thread_stop_info[key]

diff  --git a/lldb/packages/Python/lldbsuite/test/tools/lldb-dap/lldbdap_testcase.py b/lldb/packages/Python/lldbsuite/test/tools/lldb-dap/lldbdap_testcase.py
index d204e87a73acb..9f4780f5d9733 100644
--- a/lldb/packages/Python/lldbsuite/test/tools/lldb-dap/lldbdap_testcase.py
+++ b/lldb/packages/Python/lldbsuite/test/tools/lldb-dap/lldbdap_testcase.py
@@ -213,6 +213,7 @@ def verify_stop_exception_info(
         'exception' with the description matching 'expected_description' and
         text match 'expected_text', if specified."""
         stopped_events = self.dap_server.wait_for_stopped()
+        self.assertIsNotNone(stopped_events, "No stopped events detected")
         for stopped_event in stopped_events:
             body = stopped_event["body"]
             if body["reason"] != "exception":
@@ -235,7 +236,7 @@ def verify_stop_exception_info(
                     f"for stopped event {stopped_event!r}",
                 )
             return
-        self.fail(f"No valid stop exception info detected in {stopped_events}")
+        self.fail(f"No valid stop exception info detected in {stopped_events!r}")
 
     def verify_stop_on_entry(self) -> None:
         """Waits for the process to be stopped and then verifies at least one
@@ -471,9 +472,11 @@ def continue_to_breakpoints(self, breakpoint_ids: List[int]):
         self.do_continue()
         self.verify_breakpoint_hit(breakpoint_ids)
 
-    def continue_to_exception_breakpoint(self, description, text=None):
+    def continue_to_exception_breakpoint(
+        self, expected_description, expected_text=None
+    ):
         self.do_continue()
-        self.verify_stop_exception_info(description, text)
+        self.verify_stop_exception_info(expected_description, expected_text)
 
     def continue_to_exit(self, exitCode=0):
         self.do_continue()

diff  --git a/lldb/test/API/tools/lldb-dap/breakpoint/TestDAP_setExceptionBreakpoints.py b/lldb/test/API/tools/lldb-dap/breakpoint/TestDAP_setExceptionBreakpoints.py
index 5ed7e13fd0b44..2aac9310cb133 100644
--- a/lldb/test/API/tools/lldb-dap/breakpoint/TestDAP_setExceptionBreakpoints.py
+++ b/lldb/test/API/tools/lldb-dap/breakpoint/TestDAP_setExceptionBreakpoints.py
@@ -35,5 +35,9 @@ def test_functionality(self):
         if response:
             self.assertTrue(response["success"])
 
-        self.continue_to_exception_breakpoint(r"C\+\+ Throw")
-        self.continue_to_exception_breakpoint(r"C\+\+ Catch")
+        self.continue_to_exception_breakpoint(
+            expected_description=r"breakpoint 1\.1", expected_text=r"C\+\+ Throw"
+        )
+        self.continue_to_exception_breakpoint(
+            expected_description=r"breakpoint 2\.1", expected_text=r"C\+\+ Catch"
+        )

diff  --git a/lldb/test/API/tools/lldb-dap/exception/objc/TestDAP_exception_objc.py b/lldb/test/API/tools/lldb-dap/exception/objc/TestDAP_exception_objc.py
index 694cadb6ed2fe..89f96f428ec23 100644
--- a/lldb/test/API/tools/lldb-dap/exception/objc/TestDAP_exception_objc.py
+++ b/lldb/test/API/tools/lldb-dap/exception/objc/TestDAP_exception_objc.py
@@ -44,7 +44,10 @@ def test_break_on_throw_and_catch(self):
         if response:
             self.assertTrue(response["success"])
 
-        self.continue_to_exception_breakpoint("Objective-C Throw")
+        self.continue_to_exception_breakpoint(
+            expected_description="hit Objective-C exception",
+            expected_text="Objective-C Throw",
+        )
 
         # FIXME: Catching objc exceptions do not appear to be working.
         # Xcode appears to set a breakpoint on '__cxa_begin_catch' for objc

diff  --git a/lldb/test/API/tools/lldb-dap/stopped-events/Makefile b/lldb/test/API/tools/lldb-dap/stopped-events/Makefile
new file mode 100644
index 0000000000000..99998b20bcb05
--- /dev/null
+++ b/lldb/test/API/tools/lldb-dap/stopped-events/Makefile
@@ -0,0 +1,3 @@
+CXX_SOURCES := main.cpp
+
+include Makefile.rules

diff  --git a/lldb/test/API/tools/lldb-dap/stopped-events/TestDAP_stopped_events.py b/lldb/test/API/tools/lldb-dap/stopped-events/TestDAP_stopped_events.py
new file mode 100644
index 0000000000000..03dff39c82ed9
--- /dev/null
+++ b/lldb/test/API/tools/lldb-dap/stopped-events/TestDAP_stopped_events.py
@@ -0,0 +1,141 @@
+"""
+Test lldb-dap 'stopped' events.
+"""
+
+from lldbsuite.test.decorators import *
+from lldbsuite.test.lldbtest import *
+import lldbdap_testcase
+
+
+class TestDAP_stopped_events(lldbdap_testcase.DAPTestCaseBase):
+    """
+    Test validates 
diff erent operations that produce 'stopped' events.
+    """
+
+    ANY_THREAD = {}
+
+    def matches(self, a: dict, b: dict) -> bool:
+        """Returns true if 'a' is a subset of 'b', otherwise false."""
+        return a | b == a
+
+    def verify_threads(self, expected_threads):
+        threads_resp = self.dap_server.request_threads()
+        self.assertTrue(threads_resp["success"])
+        threads = threads_resp["body"]["threads"]
+        self.assertEqual(len(threads), len(expected_threads))
+        for idx, expected_thread in enumerate(expected_threads):
+            thread = threads[idx]
+            self.assertTrue(
+                self.matches(thread, expected_thread),
+                f"Invalid thread state in {threads_resp}",
+            )
+
+    @expectedFailureAll(
+        oslist=["freebsd"],
+        bugnumber="llvm.org/pr18190 thread states not properly maintained",
+    )
+    @expectedFailureNetBSD
+    @skipIfWindows  # This is flakey on Windows: llvm.org/pr24668, llvm.org/pr38373
+    def test_multiple_threads_sample_breakpoint(self):
+        """
+        Test that multiple threads being stopped on the same breakpoint only produces a single 'stopped' event.
+        """
+        program = self.getBuildArtifact("a.out")
+        self.build_and_launch(program)
+        line = line_number("main.cpp", "breakpoint")
+        [bp] = self.set_source_breakpoints("main.cpp", [line])
+
+        events = self.continue_to_next_stop()
+        self.assertEqual(len(events), 2, "Expected exactly two 'stopped' events")
+        for event in events:
+            body = event["body"]
+            self.assertEqual(body["reason"], "breakpoint")
+            self.assertEqual(body["text"], "breakpoint 1.1")
+            self.assertEqual(body["description"], "breakpoint 1.1")
+            self.assertEqual(body["hitBreakpointIds"], [int(bp)])
+            self.assertIsNotNone(body["threadId"])
+
+        # We should have three threads, something along the lines of:
+        #
+        # Process 1234 stopped
+        #   thread #1: tid = 0x01, 0x0a libsystem_pthread.dylib`pthread_mutex_lock + 12, queue = 'com.apple.main-thread'
+        # * thread #2: tid = 0x02, 0x0b a.out`add(a=1, b=2) at main.cpp:10:32, stop reason = breakpoint 1.1
+        #   thread #3: tid = 0x03, 0x0c a.out`add(a=4, b=5) at main.cpp:10:32, stop reason = breakpoint 1.1
+        self.verify_threads(
+            [
+                {},
+                {
+                    "reason": "breakpoint",
+                    "text": "breakpoint 1.1",
+                    "description": "breakpoint 1.1",
+                },
+                {
+                    "reason": "breakpoint",
+                    "text": "breakpoint 1.1",
+                    "description": "breakpoint 1.1",
+                },
+            ]
+        )
+
+        self.assertEqual(
+            self.dap_server.threads[1]["id"],
+            self.dap_server.focused_tid,
+            "Expected thread#2 to be focused",
+        )
+
+        self.continue_to_exit()
+
+    @expectedFailureAll(
+        oslist=["freebsd"],
+        bugnumber="llvm.org/pr18190 thread states not properly maintained",
+    )
+    @expectedFailureNetBSD
+    @skipIfWindows  # This is flakey on Windows: llvm.org/pr24668, llvm.org/pr38373
+    def test_multiple_breakpoints_same_location(self):
+        """
+        Test stopping at a location that reports multiple overlapping breakpoints.
+        """
+        program = self.getBuildArtifact("a.out")
+        self.build_and_launch(program)
+        line_1 = line_number("main.cpp", "breakpoint")
+        [bp1] = self.set_source_breakpoints("main.cpp", [line_1])
+        [bp2] = self.set_function_breakpoints(["my_add"])
+
+        events = self.continue_to_next_stop()
+        self.assertEqual(len(events), 2, "Expected two stopped events")
+        for event in events:
+            body = event["body"]
+            self.assertEqual(body["reason"], "breakpoint")
+            self.assertEqual(body["text"], "breakpoint 1.1 2.1")
+            self.assertEqual(body["description"], "breakpoint 1.1 2.1")
+            self.assertEqual(body["hitBreakpointIds"], [int(bp1), int(bp2)])
+            self.assertIsNotNone(body["threadId"])
+
+        # Should return something like:
+        # Process 1234 stopped
+        #   thread #1: tid = 0x01, 0x0a libsystem_pthread.dylib`pthread_mutex_lock + 12, queue = 'com.apple.main-thread'
+        # * thread #2: tid = 0x02, 0x0b a.out`add(a=1, b=2) at main.cpp:10:32, stop reason = breakpoint 1.1 2.1
+        #   thread #3: tid = 0x03, 0x0c a.out`add(a=4, b=5) at main.cpp:10:32, stop reason = breakpoint 1.1 2.1
+        self.verify_threads(
+            [
+                self.ANY_THREAD,
+                {
+                    "reason": "breakpoint",
+                    "description": "breakpoint 1.1 2.1",
+                    "text": "breakpoint 1.1 2.1",
+                },
+                {
+                    "reason": "breakpoint",
+                    "description": "breakpoint 1.1 2.1",
+                    "text": "breakpoint 1.1 2.1",
+                },
+            ]
+        )
+
+        self.assertEqual(
+            self.dap_server.threads[1]["id"],
+            self.dap_server.focused_tid,
+            "Expected thread#2 to be focused",
+        )
+
+        self.continue_to_exit()

diff  --git a/lldb/test/API/tools/lldb-dap/stopped-events/main.cpp b/lldb/test/API/tools/lldb-dap/stopped-events/main.cpp
new file mode 100644
index 0000000000000..4ad66cac33b08
--- /dev/null
+++ b/lldb/test/API/tools/lldb-dap/stopped-events/main.cpp
@@ -0,0 +1,29 @@
+#include "pseudo_barrier.h"
+#include <thread>
+
+pseudo_barrier_t g_barrier;
+
+static int my_add(int a, int b) { // breakpoint
+  return a + b;
+}
+
+int main(int argc, char const *argv[]) {
+  // Don't let either thread do anything until they're both ready.
+  pseudo_barrier_init(g_barrier, 2);
+
+  std::thread t1([] {
+    // Wait until both threads are running
+    pseudo_barrier_wait(g_barrier);
+    my_add(1, 2);
+  });
+  std::thread t2([] {
+    // Wait until both threads are running
+    pseudo_barrier_wait(g_barrier);
+    my_add(4, 5);
+  });
+
+  t1.join();
+  t2.join();
+
+  return 0;
+}

diff  --git a/lldb/test/API/tools/lldb-dap/threads/TestDAP_threads.py b/lldb/test/API/tools/lldb-dap/threads/TestDAP_threads.py
index acd6108853787..be6dd84ec4d44 100644
--- a/lldb/test/API/tools/lldb-dap/threads/TestDAP_threads.py
+++ b/lldb/test/API/tools/lldb-dap/threads/TestDAP_threads.py
@@ -39,8 +39,7 @@ def test_correct_thread(self):
                 "breakpoint %s." % breakpoint_ids[0]
             )
         )
-        self.assertFalse(stopped_event[0]["body"]["preserveFocusHint"])
-        self.assertTrue(stopped_event[0]["body"]["threadCausedFocus"])
+        self.assertNotIn("preserveFocusHint", stopped_event[0]["body"])
         # All threads should be named Thread {index}
         threads = self.dap_server.get_threads()
         self.assertTrue(all(len(t["name"]) > 0 for t in threads))

diff  --git a/lldb/tools/lldb-dap/EventHelper.cpp b/lldb/tools/lldb-dap/EventHelper.cpp
index cc34b30f4244c..dbf4823408b11 100644
--- a/lldb/tools/lldb-dap/EventHelper.cpp
+++ b/lldb/tools/lldb-dap/EventHelper.cpp
@@ -20,14 +20,20 @@
 #include "Protocol/ProtocolRequests.h"
 #include "Protocol/ProtocolTypes.h"
 #include "ProtocolUtils.h"
+#include "SBAPIExtras.h"
 #include "lldb/API/SBEvent.h"
 #include "lldb/API/SBFileSpec.h"
 #include "lldb/API/SBListener.h"
 #include "lldb/API/SBPlatform.h"
 #include "lldb/API/SBStream.h"
+#include "lldb/API/SBThread.h"
+#include "lldb/lldb-defines.h"
+#include "lldb/lldb-types.h"
 #include "llvm/Support/Error.h"
+#include "llvm/Support/ErrorHandling.h"
 #include "llvm/Support/FormatVariadic.h"
 #include "llvm/Support/Threading.h"
+#include "llvm/Support/raw_ostream.h"
 #include <mutex>
 #include <utility>
 
@@ -172,8 +178,79 @@ void SendProcessEvent(DAP &dap, LaunchMethod launch_method) {
   dap.SendJSON(llvm::json::Value(std::move(event)));
 }
 
-// Send a thread stopped event for all threads as long as the process
-// is stopped.
+static void SendStoppedEvent(DAP &dap, lldb::SBThread &thread, bool on_entry,
+                             bool all_threads_stopped, bool preserve_focus) {
+  protocol::StoppedEventBody body;
+  if (on_entry) {
+    body.reason = protocol::eStoppedReasonEntry;
+  } else {
+    switch (thread.GetStopReason()) {
+    case lldb::eStopReasonTrace:
+    case lldb::eStopReasonPlanComplete:
+    case lldb::eStopReasonProcessorTrace:
+    case lldb::eStopReasonHistoryBoundary:
+      body.reason = protocol::eStoppedReasonStep;
+      break;
+    case lldb::eStopReasonBreakpoint: {
+      ExceptionBreakpoint *exc_bp = dap.GetExceptionBPFromStopReason(thread);
+      if (exc_bp) {
+        body.reason = protocol::eStoppedReasonException;
+        body.text = exc_bp->GetLabel();
+      } else {
+        InstructionBreakpoint *inst_bp =
+            dap.GetInstructionBPFromStopReason(thread);
+        body.reason = inst_bp ? protocol::eStoppedReasonInstructionBreakpoint
+                              : protocol::eStoppedReasonBreakpoint;
+
+        llvm::raw_string_ostream OS(body.text);
+        OS << "breakpoint";
+        for (size_t idx = 0; idx < thread.GetStopReasonDataCount(); idx += 2) {
+          lldb::break_id_t bp_id = thread.GetStopReasonDataAtIndex(idx);
+          lldb::break_id_t bp_loc_id = thread.GetStopReasonDataAtIndex(idx + 1);
+          body.hitBreakpointIds.push_back(bp_id);
+          OS << " " << bp_id << "." << bp_loc_id;
+        }
+      }
+    } break;
+    case lldb::eStopReasonWatchpoint: {
+      body.reason = protocol::eStoppedReasonDataBreakpoint;
+      lldb::break_id_t bp_id = thread.GetStopReasonDataAtIndex(0);
+      body.hitBreakpointIds.push_back(bp_id);
+      body.text = llvm::formatv("data breakpoint {0}", bp_id).str();
+    } break;
+    case lldb::eStopReasonSignal:
+    case lldb::eStopReasonException:
+    case lldb::eStopReasonInstrumentation:
+      body.reason = protocol::eStoppedReasonException;
+      break;
+    case lldb::eStopReasonExec:
+    case lldb::eStopReasonFork:
+    case lldb::eStopReasonVFork:
+    case lldb::eStopReasonVForkDone:
+      body.reason = protocol::eStoppedReasonEntry;
+      break;
+    case lldb::eStopReasonInterrupt:
+      body.reason = protocol::eStoppedReasonPause;
+      break;
+    case lldb::eStopReasonThreadExiting:
+    case lldb::eStopReasonInvalid:
+    case lldb::eStopReasonNone:
+      return;
+    }
+  }
+  lldb::tid_t tid = thread.GetThreadID();
+  lldb::SBStream description;
+  thread.GetStopDescription(description);
+  body.description = {description.GetData(), description.GetSize()};
+  body.threadId = tid;
+  body.allThreadsStopped = all_threads_stopped;
+  body.preserveFocusHint = preserve_focus;
+
+  dap.Send(protocol::Event{"stopped", std::move(body)});
+}
+
+// Send a thread stopped event for the first stopped thread as the process is
+// stopped.
 llvm::Error SendThreadStoppedEvent(DAP &dap, bool on_entry) {
   lldb::SBMutex lock = dap.GetAPIMutex();
   std::lock_guard<lldb::SBMutex> guard(lock);
@@ -188,63 +265,38 @@ llvm::Error SendThreadStoppedEvent(DAP &dap, bool on_entry) {
 
   llvm::DenseSet<lldb::tid_t> old_thread_ids;
   old_thread_ids.swap(dap.thread_ids);
-  uint32_t stop_id = on_entry ? 0 : process.GetStopID();
-  const uint32_t num_threads = process.GetNumThreads();
-
-  // First make a pass through the threads to see if the focused thread
-  // has a stop reason. In case the focus thread doesn't have a stop
-  // reason, remember the first thread that has a stop reason so we can
-  // set it as the focus thread if below if needed.
-  lldb::tid_t first_tid_with_reason = LLDB_INVALID_THREAD_ID;
-  uint32_t num_threads_with_reason = 0;
-  bool focus_thread_exists = false;
-  for (uint32_t thread_idx = 0; thread_idx < num_threads; ++thread_idx) {
-    lldb::SBThread thread = process.GetThreadAtIndex(thread_idx);
-    const lldb::tid_t tid = thread.GetThreadID();
-    const bool has_reason = ThreadHasStopReason(thread);
-    // If the focus thread doesn't have a stop reason, clear the thread ID
-    if (tid == dap.focus_tid) {
-      focus_thread_exists = true;
-      if (!has_reason)
-        dap.focus_tid = LLDB_INVALID_THREAD_ID;
-    }
-    if (has_reason) {
-      ++num_threads_with_reason;
-      if (first_tid_with_reason == LLDB_INVALID_THREAD_ID)
-        first_tid_with_reason = tid;
-    }
-  }
 
-  // We will have cleared dap.focus_tid if the focus thread doesn't have
-  // a stop reason, so if it was cleared, or wasn't set, or doesn't exist,
-  // then set the focus thread to the first thread with a stop reason.
-  if (!focus_thread_exists || dap.focus_tid == LLDB_INVALID_THREAD_ID)
-    dap.focus_tid = first_tid_with_reason;
-
-  // If no threads stopped with a reason, then report the first one so
-  // we at least let the UI know we stopped.
-  if (num_threads_with_reason == 0) {
-    lldb::SBThread thread = process.GetThreadAtIndex(0);
-    dap.focus_tid = thread.GetThreadID();
-    dap.SendJSON(CreateThreadStopped(dap, thread, stop_id));
-  } else {
-    for (uint32_t thread_idx = 0; thread_idx < num_threads; ++thread_idx) {
-      lldb::SBThread thread = process.GetThreadAtIndex(thread_idx);
-      dap.thread_ids.insert(thread.GetThreadID());
-      if (ThreadHasStopReason(thread)) {
-        dap.SendJSON(CreateThreadStopped(dap, thread, stop_id));
-      }
-    }
+  lldb::tid_t focused_tid = LLDB_INVALID_THREAD_ID;
+  for (auto thread : process) {
+    // Collect all known thread ids for sending thread events.
+    dap.thread_ids.insert(thread.GetThreadID());
+
+    if (!ThreadHasStopReason(thread))
+      continue;
+
+    // When we stop, report allThreadsStopped for the *first* stopped thread to
+    // ensure the list of stopped threads is up to date.
+    bool first_stop = focused_tid == LLDB_INVALID_THREAD_ID;
+    SendStoppedEvent(dap, thread, on_entry, /*all_threads_stopped=*/first_stop,
+                     /*preserve_focus=*/!first_stop);
+
+    // Default focus to the first stopped thread.
+    if (focused_tid == LLDB_INVALID_THREAD_ID)
+      focused_tid = thread.GetThreadID();
   }
 
-  for (const auto &tid : old_thread_ids) {
-    auto end = dap.thread_ids.end();
-    auto pos = dap.thread_ids.find(tid);
-    if (pos == end)
+  if (focused_tid == LLDB_INVALID_THREAD_ID)
+    return make_error<DAPError>("no stopped threads");
+
+  // Update focused thread.
+  dap.focus_tid = focused_tid;
+
+  for (const auto &tid : old_thread_ids)
+    if (!dap.thread_ids.contains(tid))
       SendThreadExitedEvent(dap, tid);
-  }
 
   dap.RunStopCommands();
+
   return Error::success();
 }
 

diff  --git a/lldb/tools/lldb-dap/JSONUtils.cpp b/lldb/tools/lldb-dap/JSONUtils.cpp
index 9c32e3fac64ae..79925b4bb37d3 100644
--- a/lldb/tools/lldb-dap/JSONUtils.cpp
+++ b/lldb/tools/lldb-dap/JSONUtils.cpp
@@ -345,163 +345,6 @@ llvm::json::Object CreateEventObject(const llvm::StringRef event_name) {
   return event;
 }
 
-// "StoppedEvent": {
-//   "allOf": [ { "$ref": "#/definitions/Event" }, {
-//     "type": "object",
-//     "description": "Event message for 'stopped' event type. The event
-//                     indicates that the execution of the debuggee has stopped
-//                     due to some condition. This can be caused by a break
-//                     point previously set, a stepping action has completed,
-//                     by executing a debugger statement etc.",
-//     "properties": {
-//       "event": {
-//         "type": "string",
-//         "enum": [ "stopped" ]
-//       },
-//       "body": {
-//         "type": "object",
-//         "properties": {
-//           "reason": {
-//             "type": "string",
-//             "description": "The reason for the event. For backward
-//                             compatibility this string is shown in the UI if
-//                             the 'description' attribute is missing (but it
-//                             must not be translated).",
-//             "_enum": [ "step", "breakpoint", "exception", "pause", "entry" ]
-//           },
-//           "description": {
-//             "type": "string",
-//             "description": "The full reason for the event, e.g. 'Paused
-//                             on exception'. This string is shown in the UI
-//                             as is."
-//           },
-//           "threadId": {
-//             "type": "integer",
-//             "description": "The thread which was stopped."
-//           },
-//           "text": {
-//             "type": "string",
-//             "description": "Additional information. E.g. if reason is
-//                             'exception', text contains the exception name.
-//                             This string is shown in the UI."
-//           },
-//           "allThreadsStopped": {
-//             "type": "boolean",
-//             "description": "If allThreadsStopped is true, a debug adapter
-//                             can announce that all threads have stopped.
-//                             The client should use this information to
-//                             enable that all threads can be expanded to
-//                             access their stacktraces. If the attribute
-//                             is missing or false, only the thread with the
-//                             given threadId can be expanded."
-//           }
-//         },
-//         "required": [ "reason" ]
-//       }
-//     },
-//     "required": [ "event", "body" ]
-//   }]
-// }
-llvm::json::Value CreateThreadStopped(DAP &dap, lldb::SBThread &thread,
-                                      uint32_t stop_id) {
-  llvm::json::Object event(CreateEventObject("stopped"));
-  llvm::json::Object body;
-  switch (thread.GetStopReason()) {
-  case lldb::eStopReasonTrace:
-  case lldb::eStopReasonPlanComplete:
-    body.try_emplace("reason", "step");
-    break;
-  case lldb::eStopReasonBreakpoint: {
-    ExceptionBreakpoint *exc_bp = dap.GetExceptionBPFromStopReason(thread);
-    if (exc_bp) {
-      body.try_emplace("reason", "exception");
-      EmplaceSafeString(body, "description", exc_bp->GetLabel());
-    } else {
-      InstructionBreakpoint *inst_bp =
-          dap.GetInstructionBPFromStopReason(thread);
-      if (inst_bp) {
-        body.try_emplace("reason", "instruction breakpoint");
-      } else {
-        body.try_emplace("reason", "breakpoint");
-      }
-      std::vector<lldb::break_id_t> bp_ids;
-      std::ostringstream desc_sstream;
-      desc_sstream << "breakpoint";
-      for (size_t idx = 0; idx < thread.GetStopReasonDataCount(); idx += 2) {
-        lldb::break_id_t bp_id = thread.GetStopReasonDataAtIndex(idx);
-        lldb::break_id_t bp_loc_id = thread.GetStopReasonDataAtIndex(idx + 1);
-        bp_ids.push_back(bp_id);
-        desc_sstream << " " << bp_id << "." << bp_loc_id;
-      }
-      std::string desc_str = desc_sstream.str();
-      body.try_emplace("hitBreakpointIds", llvm::json::Array(bp_ids));
-      EmplaceSafeString(body, "description", desc_str);
-    }
-  } break;
-  case lldb::eStopReasonWatchpoint: {
-    body.try_emplace("reason", "data breakpoint");
-    lldb::break_id_t bp_id = thread.GetStopReasonDataAtIndex(0);
-    body.try_emplace("hitBreakpointIds",
-                     llvm::json::Array{llvm::json::Value(bp_id)});
-    EmplaceSafeString(body, "description",
-                      llvm::formatv("data breakpoint {0}", bp_id).str());
-  } break;
-  case lldb::eStopReasonInstrumentation:
-    body.try_emplace("reason", "exception");
-    break;
-  case lldb::eStopReasonProcessorTrace:
-    body.try_emplace("reason", "processor trace");
-    break;
-  case lldb::eStopReasonHistoryBoundary:
-    body.try_emplace("reason", "history boundary");
-    break;
-  case lldb::eStopReasonSignal:
-  case lldb::eStopReasonException:
-    body.try_emplace("reason", "exception");
-    break;
-  case lldb::eStopReasonExec:
-    body.try_emplace("reason", "entry");
-    break;
-  case lldb::eStopReasonFork:
-    body.try_emplace("reason", "fork");
-    break;
-  case lldb::eStopReasonVFork:
-    body.try_emplace("reason", "vfork");
-    break;
-  case lldb::eStopReasonVForkDone:
-    body.try_emplace("reason", "vforkdone");
-    break;
-  case lldb::eStopReasonInterrupt:
-    body.try_emplace("reason", "async interrupt");
-    break;
-  case lldb::eStopReasonThreadExiting:
-  case lldb::eStopReasonInvalid:
-  case lldb::eStopReasonNone:
-    break;
-  }
-  if (stop_id == 0)
-    body["reason"] = "entry";
-  const lldb::tid_t tid = thread.GetThreadID();
-  body.try_emplace("threadId", (int64_t)tid);
-  // If no description has been set, then set it to the default thread stopped
-  // description. If we have breakpoints that get hit and shouldn't be reported
-  // as breakpoints, then they will set the description above.
-  if (!ObjectContainsKey(body, "description")) {
-    char description[1024];
-    if (thread.GetStopDescription(description, sizeof(description))) {
-      EmplaceSafeString(body, "description", description);
-    }
-  }
-  // "threadCausedFocus" is used in tests to validate breaking behavior.
-  if (tid == dap.focus_tid) {
-    body.try_emplace("threadCausedFocus", true);
-  }
-  body.try_emplace("preserveFocusHint", tid != dap.focus_tid);
-  body.try_emplace("allThreadsStopped", true);
-  event.try_emplace("body", std::move(body));
-  return llvm::json::Value(std::move(event));
-}
-
 llvm::StringRef GetNonNullVariableName(lldb::SBValue &v) {
   const llvm::StringRef name = v.GetName();
   return !name.empty() ? name : "<null>";

diff  --git a/lldb/tools/lldb-dap/JSONUtils.h b/lldb/tools/lldb-dap/JSONUtils.h
index 15449d6ece62a..c2ffa11eceb95 100644
--- a/lldb/tools/lldb-dap/JSONUtils.h
+++ b/lldb/tools/lldb-dap/JSONUtils.h
@@ -234,36 +234,6 @@ void FillResponse(const llvm::json::Object &request,
 ///     definition outlined by Microsoft.
 llvm::json::Object CreateEventObject(const llvm::StringRef event_name);
 
-/// Create a "StoppedEvent" object for a LLDB thread object.
-///
-/// This function will fill in the following keys in the returned
-/// object's "body" object:
-///   "reason" - With a valid stop reason enumeration string value
-///              that Microsoft specifies
-///   "threadId" - The thread ID as an integer
-///   "description" - a stop description (like "breakpoint 12.3") as a
-///                   string
-///   "preserveFocusHint" - a boolean value that states if this thread
-///                         should keep the focus in the GUI.
-///   "allThreadsStopped" - set to True to indicate that all threads
-///                         stop when any thread stops.
-///
-/// \param[in] dap
-///     The DAP session associated with the stopped thread.
-///
-/// \param[in] thread
-///     The LLDB thread to use when populating out the "StoppedEvent"
-///     object.
-///
-/// \param[in] stop_id
-///     The stop id for this event.
-///
-/// \return
-///     A "StoppedEvent" JSON object with that follows the formal JSON
-///     definition outlined by Microsoft.
-llvm::json::Value CreateThreadStopped(DAP &dap, lldb::SBThread &thread,
-                                      uint32_t stop_id);
-
 /// \return
 ///     The variable name of \a value or a default placeholder.
 llvm::StringRef GetNonNullVariableName(lldb::SBValue &value);

diff  --git a/lldb/tools/lldb-dap/Protocol/ProtocolEvents.cpp b/lldb/tools/lldb-dap/Protocol/ProtocolEvents.cpp
index df6be06637a13..b1985cbb7d053 100644
--- a/lldb/tools/lldb-dap/Protocol/ProtocolEvents.cpp
+++ b/lldb/tools/lldb-dap/Protocol/ProtocolEvents.cpp
@@ -8,6 +8,8 @@
 
 #include "Protocol/ProtocolEvents.h"
 #include "JSONUtils.h"
+#include "lldb/lldb-defines.h"
+#include "llvm/Support/ErrorHandling.h"
 #include "llvm/Support/JSON.h"
 
 using namespace llvm;
@@ -64,4 +66,49 @@ llvm::json::Value toJSON(const MemoryEventBody &MEB) {
       {"count", MEB.count}};
 }
 
+static llvm::json::Value toJSON(const StoppedReason &SR) {
+  assert(SR != eStoppedReasonUninitialized && "StopReason Uninitialized");
+  switch (SR) {
+  case eStoppedReasonUninitialized:
+    return "";
+  case eStoppedReasonStep:
+    return "step";
+  case eStoppedReasonBreakpoint:
+    return "breakpoint";
+  case eStoppedReasonException:
+    return "exception";
+  case eStoppedReasonPause:
+    return "pause";
+  case eStoppedReasonEntry:
+    return "entry";
+  case eStoppedReasonGoto:
+    return "goto";
+  case eStoppedReasonFunctionBreakpoint:
+    return "function breakpoint";
+  case eStoppedReasonDataBreakpoint:
+    return "data breakpoint";
+  case eStoppedReasonInstructionBreakpoint:
+    return "instruction breakpoint";
+  }
+}
+
+llvm::json::Value toJSON(const StoppedEventBody &SEB) {
+  llvm::json::Object Result{{"reason", SEB.reason}};
+
+  if (!SEB.description.empty())
+    Result.insert({"description", SEB.description});
+  if (SEB.threadId != LLDB_INVALID_THREAD_ID)
+    Result.insert({"threadId", SEB.threadId});
+  if (SEB.preserveFocusHint)
+    Result.insert({"preserveFocusHint", SEB.preserveFocusHint});
+  if (!SEB.text.empty())
+    Result.insert({"text", SEB.text});
+  if (SEB.allThreadsStopped)
+    Result.insert({"allThreadsStopped", SEB.allThreadsStopped});
+  if (!SEB.hitBreakpointIds.empty())
+    Result.insert({"hitBreakpointIds", SEB.hitBreakpointIds});
+
+  return Result;
+}
+
 } // namespace lldb_dap::protocol

diff  --git a/lldb/tools/lldb-dap/Protocol/ProtocolEvents.h b/lldb/tools/lldb-dap/Protocol/ProtocolEvents.h
index 5cd5a843d284e..5c415f76c37fd 100644
--- a/lldb/tools/lldb-dap/Protocol/ProtocolEvents.h
+++ b/lldb/tools/lldb-dap/Protocol/ProtocolEvents.h
@@ -117,6 +117,68 @@ struct MemoryEventBody {
 };
 llvm::json::Value toJSON(const MemoryEventBody &);
 
+enum StoppedReason : unsigned {
+  eStoppedReasonUninitialized,
+  eStoppedReasonStep,
+  eStoppedReasonBreakpoint,
+  eStoppedReasonException,
+  eStoppedReasonPause,
+  eStoppedReasonEntry,
+  eStoppedReasonGoto,
+  eStoppedReasonFunctionBreakpoint,
+  eStoppedReasonDataBreakpoint,
+  eStoppedReasonInstructionBreakpoint,
+};
+
+/// The event indicates that the execution of the debuggee has stopped due to
+/// some condition.
+///
+/// This can be caused by a breakpoint previously set, a stepping request has
+/// completed, by executing a debugger statement etc.
+struct StoppedEventBody {
+  /// The reason for the event.
+  ///
+  /// For backward compatibility this string is shown in the UI if the
+  /// `description` attribute is missing (but it must not be translated).
+  StoppedReason reason = eStoppedReasonUninitialized;
+
+  /// The full reason for the event, e.g. 'Paused on exception'. This string is
+  /// shown in the UI as is and can be translated.
+  std::string description;
+
+  /// The thread which was stopped.
+  lldb::tid_t threadId = LLDB_INVALID_THREAD_ID;
+
+  /// A value of true hints to the client that this event should not change the
+  /// focus.
+  bool preserveFocusHint = false;
+
+  /// Additional information. E.g. if reason is `exception`, text contains the
+  /// exception name. This string is shown in the UI.
+  std::string text;
+
+  /// "If `allThreadsStopped` is true, a debug adapter can announce that all
+  /// threads have stopped.
+  ///
+  /// - The client should use this information to enable that all threads can be
+  /// expanded to access their stacktraces.
+  /// - If the attribute is missing or false, only the thread with the given
+  /// `threadId` can be expanded.
+  bool allThreadsStopped = false;
+
+  /// Ids of the breakpoints that triggered the event. In most cases there is
+  /// only a single breakpoint but here are some examples for multiple
+  /// breakpoints:
+  ///
+  /// - Different types of breakpoints map to the same location.
+  /// - Multiple source breakpoints get collapsed to the same instruction by the
+  /// compiler/runtime.
+  /// - Multiple function breakpoints with 
diff erent function names map to the
+  /// same location.
+  std::vector<lldb::break_id_t> hitBreakpointIds;
+};
+llvm::json::Value toJSON(const StoppedEventBody &);
+
 } // end namespace lldb_dap::protocol
 
 #endif

diff  --git a/lldb/tools/lldb-dap/SBAPIExtras.h b/lldb/tools/lldb-dap/SBAPIExtras.h
index 0745b2e043c21..eb59cb08ea4fd 100644
--- a/lldb/tools/lldb-dap/SBAPIExtras.h
+++ b/lldb/tools/lldb-dap/SBAPIExtras.h
@@ -8,6 +8,7 @@
 // Extensions on SB API.
 //===----------------------------------------------------------------------===//
 
+#include "lldb/API/SBProcess.h"
 #include "lldb/API/SBStream.h"
 #include "lldb/API/SBStructuredData.h"
 #include "lldb/API/SBThread.h"
@@ -34,11 +35,19 @@ struct iter {
   bool operator!=(const iter &other) { return index != other.index; }
 };
 
+/// SBProcess thread iterator.
+using process_thread_iter =
+    iter<SBProcess, SBThread, size_t, &SBProcess::GetThreadAtIndex>;
+inline process_thread_iter begin(SBProcess P) { return {P, 0}; }
+inline process_thread_iter end(SBProcess P) { return {P, P.GetNumThreads()}; }
+
 /// SBThreadCollection thread iterator.
-using thread_iter = iter<SBThreadCollection, SBThread, size_t,
-                         &SBThreadCollection::GetThreadAtIndex>;
-inline thread_iter begin(SBThreadCollection TC) { return {TC, 0}; }
-inline thread_iter end(SBThreadCollection TC) { return {TC, TC.GetSize()}; }
+using thread_collection_iter = iter<SBThreadCollection, SBThread, size_t,
+                                    &SBThreadCollection::GetThreadAtIndex>;
+inline thread_collection_iter begin(SBThreadCollection TC) { return {TC, 0}; }
+inline thread_collection_iter end(SBThreadCollection TC) {
+  return {TC, TC.GetSize()};
+}
 
 /// SBThread frame iterator.
 using frame_iter =

diff  --git a/lldb/unittests/DAP/CMakeLists.txt b/lldb/unittests/DAP/CMakeLists.txt
index 9fef37e00ed5d..97f9cad7477ed 100644
--- a/lldb/unittests/DAP/CMakeLists.txt
+++ b/lldb/unittests/DAP/CMakeLists.txt
@@ -10,6 +10,7 @@ add_lldb_unittest(DAPTests
   Handler/ContinueTest.cpp
   JSONUtilsTest.cpp
   LLDBUtilsTest.cpp
+  ProtocolEventsTest.cpp
   ProtocolRequestsTest.cpp
   ProtocolTypesTest.cpp
   ProtocolUtilsTest.cpp

diff  --git a/lldb/unittests/DAP/ProtocolEventsTest.cpp b/lldb/unittests/DAP/ProtocolEventsTest.cpp
new file mode 100644
index 0000000000000..b6efc2791e578
--- /dev/null
+++ b/lldb/unittests/DAP/ProtocolEventsTest.cpp
@@ -0,0 +1,45 @@
+//===----------------------------------------------------------------------===//
+//
+// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
+// See https://llvm.org/LICENSE.txt for license information.
+// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
+//
+//===----------------------------------------------------------------------===//
+
+#include "Protocol/ProtocolEvents.h"
+#include "TestingSupport/TestUtilities.h"
+#include "llvm/Testing/Support/Error.h"
+#include <gtest/gtest.h>
+
+using namespace llvm;
+using namespace lldb_dap::protocol;
+using lldb_private::PrettyPrint;
+using llvm::json::parse;
+using llvm::json::Value;
+
+TEST(ProtocolEventsTest, StoppedEventBody) {
+  StoppedEventBody body;
+  body.reason = lldb_dap::protocol::eStoppedReasonBreakpoint;
+  Expected<Value> expected_body = parse(R"({
+    "reason": "breakpoint"
+  })");
+  ASSERT_THAT_EXPECTED(expected_body, llvm::Succeeded());
+  EXPECT_EQ(PrettyPrint(*expected_body), PrettyPrint(body));
+
+  body.reason = eStoppedReasonBreakpoint;
+  body.description = "desc";
+  body.text = "text";
+  body.preserveFocusHint = true;
+  body.allThreadsStopped = true;
+  body.hitBreakpointIds = {1, 2, 3};
+  expected_body = parse(R"({
+    "reason": "breakpoint",
+    "allThreadsStopped": true,
+    "description": "desc",
+    "text": "text",
+    "preserveFocusHint": true,
+    "hitBreakpointIds": [1, 2, 3]
+  })");
+  ASSERT_THAT_EXPECTED(expected_body, llvm::Succeeded());
+  EXPECT_EQ(PrettyPrint(*expected_body), PrettyPrint(body));
+}


        


More information about the lldb-commits mailing list