[Lldb-commits] [lldb] [lldb-dap] Migrating 'stopped' event to structured types. (PR #176273)
John Harrison via lldb-commits
lldb-commits at lists.llvm.org
Wed Jan 21 09:02:09 PST 2026
https://github.com/ashgti updated https://github.com/llvm/llvm-project/pull/176273
>From 1e0e815ca9fb34f401b87fbf9be09785c74847b2 Mon Sep 17 00:00:00 2001
From: John Harrison <harjohn at google.com>
Date: Fri, 16 Jan 2026 11:33:33 -0800
Subject: [PATCH 1/7] [lldb-dap] Migrating 'stopped' to structured types.
Updates the 'stopped' event to use structure types. Additionally, I adjusted the description to include the full `GetStopDescription` that can have more details.
---
.../tools/lldb-dap/threads/TestDAP_threads.py | 3 +-
lldb/tools/lldb-dap/EventHelper.cpp | 123 +++++++++-----
lldb/tools/lldb-dap/JSONUtils.cpp | 157 ------------------
lldb/tools/lldb-dap/JSONUtils.h | 30 ----
.../lldb-dap/Protocol/ProtocolEvents.cpp | 46 +++++
lldb/tools/lldb-dap/Protocol/ProtocolEvents.h | 62 +++++++
lldb/unittests/DAP/CMakeLists.txt | 1 +
lldb/unittests/DAP/ProtocolEventsTest.cpp | 46 +++++
8 files changed, 241 insertions(+), 227 deletions(-)
create mode 100644 lldb/unittests/DAP/ProtocolEventsTest.cpp
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 6c5a9127f131b..2bc64fe8d2582 100644
--- a/lldb/tools/lldb-dap/EventHelper.cpp
+++ b/lldb/tools/lldb-dap/EventHelper.cpp
@@ -25,9 +25,12 @@
#include "lldb/API/SBListener.h"
#include "lldb/API/SBPlatform.h"
#include "lldb/API/SBStream.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>
@@ -188,54 +191,98 @@ 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;
+ lldb::tid_t stopped_thread_idx = 0;
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;
- }
+ dap.thread_ids.insert(thread.GetThreadID());
+
+ if (stopped_thread_idx || !ThreadHasStopReason(thread))
+ continue;
+
+ // Stop at the first thread with a stop reason.
+ stopped_thread_idx = thread_idx;
}
- // 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));
+ lldb::SBThread thread = process.GetThreadAtIndex(stopped_thread_idx);
+ assert(thread.IsValid() && "no valid thread found, process not stopped");
+
+ protocol::StoppedEventBody body;
+ if (on_entry) {
+ body.reason = protocol::eStopReasonEntry;
} 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));
+ switch (thread.GetStopReason()) {
+ case lldb::eStopReasonTrace:
+ case lldb::eStopReasonPlanComplete:
+ body.reason = protocol::eStopReasonStep;
+ break;
+ case lldb::eStopReasonBreakpoint: {
+ ExceptionBreakpoint *exc_bp = dap.GetExceptionBPFromStopReason(thread);
+ if (exc_bp) {
+ body.reason = protocol::eStopReasonException;
+ body.text = exc_bp->GetLabel();
+ } else {
+ InstructionBreakpoint *inst_bp =
+ dap.GetInstructionBPFromStopReason(thread);
+ body.reason = inst_bp ? protocol::eStopReasonInstructionBreakpoint
+ : protocol::eStopReasonBreakpoint;
+
+ 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::eStopReasonDataBreakpoint;
+ 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::eStopReasonProcessorTrace:
+ body.reason = protocol::eStopReasonStep; // fallback reason
+ break;
+ case lldb::eStopReasonHistoryBoundary:
+ body.reason = protocol::eStopReasonStep; // fallback reason
+ break;
+ case lldb::eStopReasonSignal:
+ case lldb::eStopReasonException:
+ case lldb::eStopReasonInstrumentation:
+ body.reason = protocol::eStopReasonException;
+ break;
+ case lldb::eStopReasonExec:
+ case lldb::eStopReasonFork:
+ case lldb::eStopReasonVFork:
+ case lldb::eStopReasonVForkDone:
+ body.reason = protocol::eStopReasonEntry;
+ break;
+ case lldb::eStopReasonInterrupt:
+ body.reason = protocol::eStopReasonPause;
+ break;
+ case lldb::eStopReasonThreadExiting:
+ case lldb::eStopReasonInvalid:
+ case lldb::eStopReasonNone:
+ llvm_unreachable("invalid stop reason, thread is not stopped");
+ break;
}
}
+ lldb::tid_t tid = thread.GetThreadID();
+ lldb::SBStream description;
+ thread.GetStopDescription(description);
+ body.description = {description.GetData(), description.GetSize()};
+ body.threadId = tid;
+ body.preserveFocusHint = tid == dap.focus_tid;
+ body.allThreadsStopped = true;
+
+ // Update focused thread.
+ dap.focus_tid = tid;
+
+ dap.Send(protocol::Event{"stopped", std::move(body)});
for (const auto &tid : old_thread_ids) {
auto end = dap.thread_ids.end();
diff --git a/lldb/tools/lldb-dap/JSONUtils.cpp b/lldb/tools/lldb-dap/JSONUtils.cpp
index 5c33c6aa591a6..643ec5c6a685d 100644
--- a/lldb/tools/lldb-dap/JSONUtils.cpp
+++ b/lldb/tools/lldb-dap/JSONUtils.cpp
@@ -430,163 +430,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", "breakpoint");
- 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..1bc656b0458b2 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,48 @@ llvm::json::Value toJSON(const MemoryEventBody &MEB) {
{"count", MEB.count}};
}
+[[maybe_unused]] static llvm::json::Value toJSON(const StopReason &SR) {
+ switch (SR) {
+ case eStopReasonStep:
+ return "step";
+ case eStopReasonBreakpoint:
+ return "breakpoint";
+ case eStopReasonException:
+ return "exception";
+ case eStopReasonPause:
+ return "pause";
+ case eStopReasonEntry:
+ return "entry";
+ case eStopReasonGoto:
+ return "goto";
+ case eStopReasonFunctionBreakpoint:
+ return "function breakpoint";
+ case eStopReasonDataBreakpoint:
+ return "data breakpoint";
+ case eStopReasonInstructionBreakpoint:
+ return "instruction breakpoint";
+ case eStopReasonInvalid:
+ return "";
+ }
+}
+
+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..230d28f7e2810 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 StopReason : unsigned {
+ eStopReasonInvalid,
+ eStopReasonStep,
+ eStopReasonBreakpoint,
+ eStopReasonException,
+ eStopReasonPause,
+ eStopReasonEntry,
+ eStopReasonGoto,
+ eStopReasonFunctionBreakpoint,
+ eStopReasonDataBreakpoint,
+ eStopReasonInstructionBreakpoint,
+};
+
+/// 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).
+ StopReason reason = eStopReasonInvalid;
+
+ /// 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 different 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/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..bb7a1e9574fc8
--- /dev/null
+++ b/lldb/unittests/DAP/ProtocolEventsTest.cpp
@@ -0,0 +1,46 @@
+//===----------------------------------------------------------------------===//
+//
+// 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 llvm::json::parse;
+using llvm::json::Value;
+
+/// Returns a pretty printed json string of a `llvm::json::Value`.
+static std::string pp(const Value &E) { return formatv("{0:2}", E).str(); }
+
+TEST(ProtocolEventsTest, StoppedEventBody) {
+ StoppedEventBody body;
+ Expected<Value> expected_body = parse(R"({
+ "reason": ""
+ })");
+ ASSERT_THAT_EXPECTED(expected_body, llvm::Succeeded());
+ EXPECT_EQ(pp(*expected_body), pp(body));
+
+ body.reason = eStopReasonBreakpoint;
+ 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(pp(*expected_body), pp(body));
+}
>From 427f1c2c8052e370f195870df1bdcdfe0c243b7b Mon Sep 17 00:00:00 2001
From: John Harrison <harjohn at google.com>
Date: Fri, 16 Jan 2026 12:04:56 -0800
Subject: [PATCH 2/7] Fixing tests after splitting up PR.
---
.../test/tools/lldb-dap/lldbdap_testcase.py | 50 ++++++++++++-------
.../TestDAP_setExceptionBreakpoints.py | 8 ++-
.../lldb-dap/exception/TestDAP_exception.py | 2 +-
.../exception/cpp/TestDAP_exception_cpp.py | 3 +-
.../exception/objc/TestDAP_exception_objc.py | 9 ++--
5 files changed, 46 insertions(+), 26 deletions(-)
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 e4a3e1c786ffe..015feb40096c0 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
@@ -208,25 +208,40 @@ def verify_all_breakpoints_hit(self, breakpoint_ids):
return
self.assertTrue(False, f"breakpoints not hit, stopped_events={stopped_events}")
- def verify_stop_exception_info(self, expected_description):
+ def verify_stop_exception_info(
+ self, expected_description: str, expected_text: Optional[str] = None
+ ):
"""Wait for the process we are debugging to stop, and verify the stop
reason is 'exception' and that the description matches
'expected_description'
"""
stopped_events = self.dap_server.wait_for_stopped()
+ self.assertIsNotNone(stopped_events, "No stopped events detected")
for stopped_event in stopped_events:
- if "body" in stopped_event:
- body = stopped_event["body"]
- if "reason" not in body:
- continue
- if body["reason"] != "exception":
- continue
- if "description" not in body:
- continue
- description = body["description"]
- if expected_description == description:
- return True
- return False
+ if (
+ "body" not in stopped_event
+ or stopped_event["body"]["reason"] != "exception"
+ ):
+ continue
+ self.assertIn(
+ "description",
+ stopped_event["body"],
+ f"stopped event missing description {stopped_event}",
+ )
+ description = stopped_event["body"]["description"]
+ self.assertRegex(
+ description,
+ expected_description,
+ f"for 'stopped' event {stopped_event!r}",
+ )
+ if expected_text:
+ self.assertRegex(
+ stopped_event["body"]["text"],
+ expected_text,
+ f"for stopped event {stopped_event!r}",
+ )
+ return
+ self.fail(f"No valid stop exception info detected in {stopped_events}")
def verify_stop_on_entry(self) -> None:
"""Waits for the process to be stopped and then verifies at least one
@@ -437,12 +452,11 @@ def continue_to_breakpoints(self, breakpoint_ids):
self.do_continue()
self.verify_breakpoint_hit(breakpoint_ids)
- def continue_to_exception_breakpoint(self, filter_label):
+ def continue_to_exception_breakpoint(
+ self, expected_description, expected_text=None
+ ):
self.do_continue()
- self.assertTrue(
- self.verify_stop_exception_info(filter_label),
- 'verify we got "%s"' % (filter_label),
- )
+ 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 4ca733a9a59ca..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("C++ Throw")
- self.continue_to_exception_breakpoint("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/TestDAP_exception.py b/lldb/test/API/tools/lldb-dap/exception/TestDAP_exception.py
index f044bcae41892..b92c3290ceb4c 100644
--- a/lldb/test/API/tools/lldb-dap/exception/TestDAP_exception.py
+++ b/lldb/test/API/tools/lldb-dap/exception/TestDAP_exception.py
@@ -18,7 +18,7 @@ def test_stopped_description(self):
self.build_and_launch(program)
self.do_continue()
- self.assertTrue(self.verify_stop_exception_info("signal SIGABRT"))
+ self.verify_stop_exception_info("signal SIGABRT")
exceptionInfo = self.get_exceptionInfo()
self.assertEqual(exceptionInfo["breakMode"], "always")
self.assertEqual(exceptionInfo["description"], "signal SIGABRT")
diff --git a/lldb/test/API/tools/lldb-dap/exception/cpp/TestDAP_exception_cpp.py b/lldb/test/API/tools/lldb-dap/exception/cpp/TestDAP_exception_cpp.py
index 6471e2b87251a..96fcc2cae2c04 100644
--- a/lldb/test/API/tools/lldb-dap/exception/cpp/TestDAP_exception_cpp.py
+++ b/lldb/test/API/tools/lldb-dap/exception/cpp/TestDAP_exception_cpp.py
@@ -2,7 +2,6 @@
Test exception behavior in DAP with c++ throw.
"""
-
from lldbsuite.test.decorators import *
from lldbsuite.test.lldbtest import *
import lldbdap_testcase
@@ -18,7 +17,7 @@ def test_stopped_description(self):
program = self.getBuildArtifact("a.out")
self.build_and_launch(program)
self.dap_server.request_continue()
- self.assertTrue(self.verify_stop_exception_info("signal SIGABRT"))
+ self.verify_stop_exception_info("signal SIGABRT")
exceptionInfo = self.get_exceptionInfo()
self.assertEqual(exceptionInfo["breakMode"], "always")
self.assertEqual(exceptionInfo["description"], "signal SIGABRT")
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 ddedf7a6de8c6..3eb8d7885dc35 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
@@ -16,7 +16,7 @@ def test_stopped_description(self):
program = self.getBuildArtifact("a.out")
self.build_and_launch(program)
self.dap_server.request_continue()
- self.assertTrue(self.verify_stop_exception_info("signal SIGABRT"))
+ self.verify_stop_exception_info("signal SIGABRT")
exception_info = self.get_exceptionInfo()
self.assertEqual(exception_info["breakMode"], "always")
self.assertEqual(exception_info["description"], "signal SIGABRT")
@@ -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
@@ -54,7 +57,7 @@ def test_break_on_throw_and_catch(self):
self.do_continue()
- self.assertTrue(self.verify_stop_exception_info("signal SIGABRT"))
+ self.verify_stop_exception_info("signal SIGABRT")
exception_info = self.get_exceptionInfo()
self.assertEqual(exception_info["breakMode"], "always")
self.assertEqual(exception_info["description"], "signal SIGABRT")
>From 910b2510a94bdd6430873e079e63f7502dbee647 Mon Sep 17 00:00:00 2001
From: John Harrison <harjohn at google.com>
Date: Fri, 16 Jan 2026 16:30:28 -0800
Subject: [PATCH 3/7] Adding additional tests to ensure we can have multiple
threads stop but only produce a single 'stopped' event.
---
.../API/tools/lldb-dap/stop-events/Makefile | 3 +
.../stop-events/TestDAP_stop_events.py | 94 +++++++++++++++++++
.../API/tools/lldb-dap/stop-events/main.cpp | 27 ++++++
.../lldb-dap/Protocol/ProtocolEvents.cpp | 4 +-
lldb/tools/lldb-dap/Protocol/ProtocolEvents.h | 4 +-
5 files changed, 128 insertions(+), 4 deletions(-)
create mode 100644 lldb/test/API/tools/lldb-dap/stop-events/Makefile
create mode 100644 lldb/test/API/tools/lldb-dap/stop-events/TestDAP_stop_events.py
create mode 100644 lldb/test/API/tools/lldb-dap/stop-events/main.cpp
diff --git a/lldb/test/API/tools/lldb-dap/stop-events/Makefile b/lldb/test/API/tools/lldb-dap/stop-events/Makefile
new file mode 100644
index 0000000000000..99998b20bcb05
--- /dev/null
+++ b/lldb/test/API/tools/lldb-dap/stop-events/Makefile
@@ -0,0 +1,3 @@
+CXX_SOURCES := main.cpp
+
+include Makefile.rules
diff --git a/lldb/test/API/tools/lldb-dap/stop-events/TestDAP_stop_events.py b/lldb/test/API/tools/lldb-dap/stop-events/TestDAP_stop_events.py
new file mode 100644
index 0000000000000..e766db2e47f59
--- /dev/null
+++ b/lldb/test/API/tools/lldb-dap/stop-events/TestDAP_stop_events.py
@@ -0,0 +1,94 @@
+"""
+Test lldb-dap stop events.
+"""
+
+from lldbsuite.test.decorators import *
+from lldbsuite.test.lldbtest import *
+import lldbdap_testcase
+
+
+class TestDAP_stop_events(lldbdap_testcase.DAPTestCaseBase):
+ """
+ Test validates different operations that produce 'stopped' events.
+ """
+
+ def evaluate(self, command: str) -> str:
+ result = self.dap_server.request_evaluate(command, context="repl")
+ self.assertTrue(result["success"])
+ return result["body"]["result"]
+
+ 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_1 = line_number("main.cpp", "breakpoint 1")
+ [bp1] = self.set_source_breakpoints("main.cpp", [line_1])
+
+ events = self.continue_to_next_stop()
+ self.assertEqual(len(events), 1, "Expected a single stopped event")
+ body = events[0]["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(bp1)])
+ self.assertEqual(body["allThreadsStopped"], True)
+ self.assertNotIn("preserveFocusHint", body)
+ 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
+ # thread #3: tid = 0x03, 0x0c a.out`add(a=4, b=5) at main.cpp:10:32, stop reason = breakpoint 1.1
+ result = self.evaluate("thread list")
+
+ # Ensure we have 2 threads stopped at the same breakpoint.
+ threads_with_stop_reason = [
+ l for l in result.split("\n") if "stop reason = breakpoint" in l
+ ]
+ self.assertTrue(
+ len(threads_with_stop_reason) == 2,
+ f"Failed to stop at the same breakpoint: {result}",
+ )
+
+ self.continue_to_exit()
+
+ 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 1")
+ [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), 1, "Expected a single stopped event")
+ body = events[0]["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.assertEqual(body["allThreadsStopped"], True)
+ self.assertNotIn("preserveFocusHint", body)
+ 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
+ result = self.evaluate("thread list")
+
+ # Ensure we have 2 threads at the same location with overlapping breakpoints.
+ threads_with_stop_reason = [
+ l for l in result.split("\n") if "stop reason = breakpoint" in l
+ ]
+ self.assertTrue(
+ len(threads_with_stop_reason) == 2,
+ f"Failed to stop at the same breakpoint: {result}",
+ )
+ self.continue_to_exit()
diff --git a/lldb/test/API/tools/lldb-dap/stop-events/main.cpp b/lldb/test/API/tools/lldb-dap/stop-events/main.cpp
new file mode 100644
index 0000000000000..195dbeda5c2f1
--- /dev/null
+++ b/lldb/test/API/tools/lldb-dap/stop-events/main.cpp
@@ -0,0 +1,27 @@
+#include <condition_variable>
+#include <mutex>
+#include <thread>
+
+std::mutex mux;
+std::condition_variable cv;
+bool ready = false;
+
+static int my_add(int a, int b) { // breakpoint 1
+ std::unique_lock<std::mutex> lk(mux);
+ cv.wait(lk, [] { return ready; });
+ return a + b;
+}
+
+int main(int argc, char const *argv[]) {
+ std::thread t1(my_add, 1, 2);
+ std::thread t2(my_add, 4, 5);
+
+ {
+ std::lock_guard<std::mutex> lk(mux);
+ ready = true;
+ cv.notify_all();
+ }
+ t1.join();
+ t2.join();
+ return 0;
+}
diff --git a/lldb/tools/lldb-dap/Protocol/ProtocolEvents.cpp b/lldb/tools/lldb-dap/Protocol/ProtocolEvents.cpp
index 1bc656b0458b2..70722bc4ea0ca 100644
--- a/lldb/tools/lldb-dap/Protocol/ProtocolEvents.cpp
+++ b/lldb/tools/lldb-dap/Protocol/ProtocolEvents.cpp
@@ -68,6 +68,8 @@ llvm::json::Value toJSON(const MemoryEventBody &MEB) {
[[maybe_unused]] static llvm::json::Value toJSON(const StopReason &SR) {
switch (SR) {
+ case eStopReasonEmpty:
+ return "";
case eStopReasonStep:
return "step";
case eStopReasonBreakpoint:
@@ -86,8 +88,6 @@ llvm::json::Value toJSON(const MemoryEventBody &MEB) {
return "data breakpoint";
case eStopReasonInstructionBreakpoint:
return "instruction breakpoint";
- case eStopReasonInvalid:
- return "";
}
}
diff --git a/lldb/tools/lldb-dap/Protocol/ProtocolEvents.h b/lldb/tools/lldb-dap/Protocol/ProtocolEvents.h
index 230d28f7e2810..c305499084021 100644
--- a/lldb/tools/lldb-dap/Protocol/ProtocolEvents.h
+++ b/lldb/tools/lldb-dap/Protocol/ProtocolEvents.h
@@ -118,7 +118,7 @@ struct MemoryEventBody {
llvm::json::Value toJSON(const MemoryEventBody &);
enum StopReason : unsigned {
- eStopReasonInvalid,
+ eStopReasonEmpty,
eStopReasonStep,
eStopReasonBreakpoint,
eStopReasonException,
@@ -140,7 +140,7 @@ struct StoppedEventBody {
///
/// For backward compatibility this string is shown in the UI if the
/// `description` attribute is missing (but it must not be translated).
- StopReason reason = eStopReasonInvalid;
+ StopReason reason = eStopReasonEmpty;
/// The full reason for the event, e.g. 'Paused on exception'. This string is
/// shown in the UI as is and can be translated.
>From 0eab16ea51c3ba3fc96e37c875e280ca3fca501f Mon Sep 17 00:00:00 2001
From: John Harrison <harjohn at google.com>
Date: Fri, 16 Jan 2026 16:52:56 -0800
Subject: [PATCH 4/7] Moved creating the event body into its own function
again.
---
lldb/tools/lldb-dap/EventHelper.cpp | 81 +++++++++++++++--------------
1 file changed, 43 insertions(+), 38 deletions(-)
diff --git a/lldb/tools/lldb-dap/EventHelper.cpp b/lldb/tools/lldb-dap/EventHelper.cpp
index 2bc64fe8d2582..888c959414fef 100644
--- a/lldb/tools/lldb-dap/EventHelper.cpp
+++ b/lldb/tools/lldb-dap/EventHelper.cpp
@@ -175,39 +175,7 @@ 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.
-llvm::Error SendThreadStoppedEvent(DAP &dap, bool on_entry) {
- lldb::SBMutex lock = dap.GetAPIMutex();
- std::lock_guard<lldb::SBMutex> guard(lock);
-
- lldb::SBProcess process = dap.target.GetProcess();
- if (!process.IsValid())
- return make_error<DAPError>("invalid process");
-
- lldb::StateType state = process.GetState();
- if (!lldb::SBDebugger::StateIsStoppedState(state))
- return make_error<NotStoppedError>();
-
- llvm::DenseSet<lldb::tid_t> old_thread_ids;
- old_thread_ids.swap(dap.thread_ids);
- const uint32_t num_threads = process.GetNumThreads();
-
- lldb::tid_t stopped_thread_idx = 0;
- 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 (stopped_thread_idx || !ThreadHasStopReason(thread))
- continue;
-
- // Stop at the first thread with a stop reason.
- stopped_thread_idx = thread_idx;
- }
-
- lldb::SBThread thread = process.GetThreadAtIndex(stopped_thread_idx);
- assert(thread.IsValid() && "no valid thread found, process not stopped");
-
+static void SendStoppedEvent(DAP &dap, lldb::SBThread &thread, bool on_entry) {
protocol::StoppedEventBody body;
if (on_entry) {
body.reason = protocol::eStopReasonEntry;
@@ -267,8 +235,7 @@ llvm::Error SendThreadStoppedEvent(DAP &dap, bool on_entry) {
case lldb::eStopReasonThreadExiting:
case lldb::eStopReasonInvalid:
case lldb::eStopReasonNone:
- llvm_unreachable("invalid stop reason, thread is not stopped");
- break;
+ return;
}
}
lldb::tid_t tid = thread.GetThreadID();
@@ -279,10 +246,48 @@ llvm::Error SendThreadStoppedEvent(DAP &dap, bool on_entry) {
body.preserveFocusHint = tid == dap.focus_tid;
body.allThreadsStopped = true;
- // Update focused thread.
- dap.focus_tid = tid;
-
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);
+
+ lldb::SBProcess process = dap.target.GetProcess();
+ if (!process.IsValid())
+ return make_error<DAPError>("invalid process");
+
+ lldb::StateType state = process.GetState();
+ if (!lldb::SBDebugger::StateIsStoppedState(state))
+ return make_error<NotStoppedError>();
+
+ llvm::DenseSet<lldb::tid_t> old_thread_ids;
+ old_thread_ids.swap(dap.thread_ids);
+ const uint32_t num_threads = process.GetNumThreads();
+
+ lldb::SBThread stopped_thread;
+ for (uint32_t thread_idx = 0; thread_idx < num_threads; ++thread_idx) {
+ lldb::SBThread thread = process.GetThreadAtIndex(thread_idx);
+ // Collect all known thread ids for sending thread events.
+ dap.thread_ids.insert(thread.GetThreadID());
+
+ if (stopped_thread || !ThreadHasStopReason(thread))
+ continue;
+
+ // Stop at the first thread with a stop reason.
+ stopped_thread = thread;
+ }
+
+ if (!stopped_thread)
+ return make_error<DAPError>(
+ "no valid thread found, cannot determine why the process is stopped");
+
+ SendStoppedEvent(dap, stopped_thread, on_entry);
+
+ // Update focused thread.
+ dap.focus_tid = stopped_thread.GetThreadID();
for (const auto &tid : old_thread_ids) {
auto end = dap.thread_ids.end();
>From 63d9c6081e886bcd76f546e27d530a6aa5618bb1 Mon Sep 17 00:00:00 2001
From: John Harrison <harjohn at google.com>
Date: Fri, 16 Jan 2026 17:03:55 -0800
Subject: [PATCH 5/7] Addressing feedback.
---
lldb/tools/lldb-dap/EventHelper.cpp | 8 ++------
1 file changed, 2 insertions(+), 6 deletions(-)
diff --git a/lldb/tools/lldb-dap/EventHelper.cpp b/lldb/tools/lldb-dap/EventHelper.cpp
index 888c959414fef..bd8fb9767f57b 100644
--- a/lldb/tools/lldb-dap/EventHelper.cpp
+++ b/lldb/tools/lldb-dap/EventHelper.cpp
@@ -183,6 +183,8 @@ static void SendStoppedEvent(DAP &dap, lldb::SBThread &thread, bool on_entry) {
switch (thread.GetStopReason()) {
case lldb::eStopReasonTrace:
case lldb::eStopReasonPlanComplete:
+ case lldb::eStopReasonProcessorTrace:
+ case lldb::eStopReasonHistoryBoundary:
body.reason = protocol::eStopReasonStep;
break;
case lldb::eStopReasonBreakpoint: {
@@ -212,12 +214,6 @@ static void SendStoppedEvent(DAP &dap, lldb::SBThread &thread, bool on_entry) {
body.hitBreakpointIds.push_back(bp_id);
body.text = llvm::formatv("data breakpoint {0}", bp_id).str();
} break;
- case lldb::eStopReasonProcessorTrace:
- body.reason = protocol::eStopReasonStep; // fallback reason
- break;
- case lldb::eStopReasonHistoryBoundary:
- body.reason = protocol::eStopReasonStep; // fallback reason
- break;
case lldb::eStopReasonSignal:
case lldb::eStopReasonException:
case lldb::eStopReasonInstrumentation:
>From 4784f64d2a3ae4462be4bcc84246fa0955588d89 Mon Sep 17 00:00:00 2001
From: John Harrison <harjohn at google.com>
Date: Fri, 16 Jan 2026 17:27:24 -0800
Subject: [PATCH 6/7] Adjusting the multiple thread stop event to more closely
match other tests stopping at the same location and marking the test with the
same skipped decorators.
---
.../stop-events/TestDAP_stop_events.py | 28 +++++++++++++---
.../API/tools/lldb-dap/stop-events/main.cpp | 32 ++++++++++---------
.../lldb-dap/Protocol/ProtocolEvents.cpp | 2 +-
3 files changed, 42 insertions(+), 20 deletions(-)
diff --git a/lldb/test/API/tools/lldb-dap/stop-events/TestDAP_stop_events.py b/lldb/test/API/tools/lldb-dap/stop-events/TestDAP_stop_events.py
index e766db2e47f59..8fa57fab4cf1c 100644
--- a/lldb/test/API/tools/lldb-dap/stop-events/TestDAP_stop_events.py
+++ b/lldb/test/API/tools/lldb-dap/stop-events/TestDAP_stop_events.py
@@ -17,14 +17,24 @@ def evaluate(self, command: str) -> str:
self.assertTrue(result["success"])
return result["body"]["result"]
+ @expectedFailureAll(
+ oslist=["linux"],
+ bugnumber="llvm.org/pr15824 thread states not properly maintained",
+ )
+ @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_1 = line_number("main.cpp", "breakpoint 1")
- [bp1] = self.set_source_breakpoints("main.cpp", [line_1])
+ line = line_number("main.cpp", "breakpoint")
+ [bp] = self.set_source_breakpoints("main.cpp", [line])
events = self.continue_to_next_stop()
self.assertEqual(len(events), 1, "Expected a single stopped event")
@@ -32,7 +42,7 @@ def test_multiple_threads_sample_breakpoint(self):
self.assertEqual(body["reason"], "breakpoint")
self.assertEqual(body["text"], "breakpoint 1.1")
self.assertEqual(body["description"], "breakpoint 1.1")
- self.assertEqual(body["hitBreakpointIds"], [int(bp1)])
+ self.assertEqual(body["hitBreakpointIds"], [int(bp)])
self.assertEqual(body["allThreadsStopped"], True)
self.assertNotIn("preserveFocusHint", body)
self.assertIsNotNone(body["threadId"])
@@ -55,13 +65,23 @@ def test_multiple_threads_sample_breakpoint(self):
self.continue_to_exit()
+ @expectedFailureAll(
+ oslist=["linux"],
+ bugnumber="llvm.org/pr15824 thread states not properly maintained",
+ )
+ @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 1")
+ line_1 = line_number("main.cpp", "breakpoint")
[bp1] = self.set_source_breakpoints("main.cpp", [line_1])
[bp2] = self.set_function_breakpoints(["my_add"])
diff --git a/lldb/test/API/tools/lldb-dap/stop-events/main.cpp b/lldb/test/API/tools/lldb-dap/stop-events/main.cpp
index 195dbeda5c2f1..4ad66cac33b08 100644
--- a/lldb/test/API/tools/lldb-dap/stop-events/main.cpp
+++ b/lldb/test/API/tools/lldb-dap/stop-events/main.cpp
@@ -1,27 +1,29 @@
-#include <condition_variable>
-#include <mutex>
+#include "pseudo_barrier.h"
#include <thread>
-std::mutex mux;
-std::condition_variable cv;
-bool ready = false;
+pseudo_barrier_t g_barrier;
-static int my_add(int a, int b) { // breakpoint 1
- std::unique_lock<std::mutex> lk(mux);
- cv.wait(lk, [] { return ready; });
+static int my_add(int a, int b) { // breakpoint
return a + b;
}
int main(int argc, char const *argv[]) {
- std::thread t1(my_add, 1, 2);
- std::thread t2(my_add, 4, 5);
+ // 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);
+ });
- {
- std::lock_guard<std::mutex> lk(mux);
- ready = true;
- cv.notify_all();
- }
t1.join();
t2.join();
+
return 0;
}
diff --git a/lldb/tools/lldb-dap/Protocol/ProtocolEvents.cpp b/lldb/tools/lldb-dap/Protocol/ProtocolEvents.cpp
index 70722bc4ea0ca..f2455098f5ccf 100644
--- a/lldb/tools/lldb-dap/Protocol/ProtocolEvents.cpp
+++ b/lldb/tools/lldb-dap/Protocol/ProtocolEvents.cpp
@@ -66,7 +66,7 @@ llvm::json::Value toJSON(const MemoryEventBody &MEB) {
{"count", MEB.count}};
}
-[[maybe_unused]] static llvm::json::Value toJSON(const StopReason &SR) {
+static llvm::json::Value toJSON(const StopReason &SR) {
switch (SR) {
case eStopReasonEmpty:
return "";
>From 9c6db170caac9606027e12338d244b439a84cf8a Mon Sep 17 00:00:00 2001
From: John Harrison <harjohn at google.com>
Date: Tue, 20 Jan 2026 15:51:01 -0800
Subject: [PATCH 7/7] When the process is stopped, sand a stopped event for
each stopped thread, but correctly mark 'preserveFocusedHint' and
'allThreadsStopped' to ensure the client correctly updates the event state.
---
.../test/tools/lldb-dap/dap_server.py | 6 +-
.../stop-events/TestDAP_stop_events.py | 114 --------------
.../{stop-events => stopped-events}/Makefile | 0
.../stopped-events/TestDAP_stopped_events.py | 149 ++++++++++++++++++
.../{stop-events => stopped-events}/main.cpp | 0
lldb/tools/lldb-dap/EventHelper.cpp | 33 ++--
6 files changed, 174 insertions(+), 128 deletions(-)
delete mode 100644 lldb/test/API/tools/lldb-dap/stop-events/TestDAP_stop_events.py
rename lldb/test/API/tools/lldb-dap/{stop-events => stopped-events}/Makefile (100%)
create mode 100644 lldb/test/API/tools/lldb-dap/stopped-events/TestDAP_stopped_events.py
rename lldb/test/API/tools/lldb-dap/{stop-events => stopped-events}/main.cpp (100%)
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 875cfb6c6b197..031e6a24e0fe4 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
@@ -278,6 +278,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 = None
self.frame_scopes: Dict[str, Any] = {}
# keyed by breakpoint id
self.resolved_breakpoints: dict[str, Breakpoint] = {}
@@ -507,6 +508,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
@@ -567,6 +570,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:
@@ -1530,7 +1534,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/test/API/tools/lldb-dap/stop-events/TestDAP_stop_events.py b/lldb/test/API/tools/lldb-dap/stop-events/TestDAP_stop_events.py
deleted file mode 100644
index 8fa57fab4cf1c..0000000000000
--- a/lldb/test/API/tools/lldb-dap/stop-events/TestDAP_stop_events.py
+++ /dev/null
@@ -1,114 +0,0 @@
-"""
-Test lldb-dap stop events.
-"""
-
-from lldbsuite.test.decorators import *
-from lldbsuite.test.lldbtest import *
-import lldbdap_testcase
-
-
-class TestDAP_stop_events(lldbdap_testcase.DAPTestCaseBase):
- """
- Test validates different operations that produce 'stopped' events.
- """
-
- def evaluate(self, command: str) -> str:
- result = self.dap_server.request_evaluate(command, context="repl")
- self.assertTrue(result["success"])
- return result["body"]["result"]
-
- @expectedFailureAll(
- oslist=["linux"],
- bugnumber="llvm.org/pr15824 thread states not properly maintained",
- )
- @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), 1, "Expected a single stopped event")
- body = events[0]["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.assertEqual(body["allThreadsStopped"], True)
- self.assertNotIn("preserveFocusHint", body)
- 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
- # thread #3: tid = 0x03, 0x0c a.out`add(a=4, b=5) at main.cpp:10:32, stop reason = breakpoint 1.1
- result = self.evaluate("thread list")
-
- # Ensure we have 2 threads stopped at the same breakpoint.
- threads_with_stop_reason = [
- l for l in result.split("\n") if "stop reason = breakpoint" in l
- ]
- self.assertTrue(
- len(threads_with_stop_reason) == 2,
- f"Failed to stop at the same breakpoint: {result}",
- )
-
- self.continue_to_exit()
-
- @expectedFailureAll(
- oslist=["linux"],
- bugnumber="llvm.org/pr15824 thread states not properly maintained",
- )
- @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), 1, "Expected a single stopped event")
- body = events[0]["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.assertEqual(body["allThreadsStopped"], True)
- self.assertNotIn("preserveFocusHint", body)
- 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
- result = self.evaluate("thread list")
-
- # Ensure we have 2 threads at the same location with overlapping breakpoints.
- threads_with_stop_reason = [
- l for l in result.split("\n") if "stop reason = breakpoint" in l
- ]
- self.assertTrue(
- len(threads_with_stop_reason) == 2,
- f"Failed to stop at the same breakpoint: {result}",
- )
- self.continue_to_exit()
diff --git a/lldb/test/API/tools/lldb-dap/stop-events/Makefile b/lldb/test/API/tools/lldb-dap/stopped-events/Makefile
similarity index 100%
rename from lldb/test/API/tools/lldb-dap/stop-events/Makefile
rename to lldb/test/API/tools/lldb-dap/stopped-events/Makefile
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..94dfa6f69c852
--- /dev/null
+++ b/lldb/test/API/tools/lldb-dap/stopped-events/TestDAP_stopped_events.py
@@ -0,0 +1,149 @@
+"""
+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 different 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=["linux"],
+ bugnumber="llvm.org/pr15824 thread states not properly maintained",
+ )
+ @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=["linux"],
+ bugnumber="llvm.org/pr15824 thread states not properly maintained",
+ )
+ @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/stop-events/main.cpp b/lldb/test/API/tools/lldb-dap/stopped-events/main.cpp
similarity index 100%
rename from lldb/test/API/tools/lldb-dap/stop-events/main.cpp
rename to lldb/test/API/tools/lldb-dap/stopped-events/main.cpp
diff --git a/lldb/tools/lldb-dap/EventHelper.cpp b/lldb/tools/lldb-dap/EventHelper.cpp
index bd8fb9767f57b..166536125b1c3 100644
--- a/lldb/tools/lldb-dap/EventHelper.cpp
+++ b/lldb/tools/lldb-dap/EventHelper.cpp
@@ -25,6 +25,8 @@
#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"
@@ -175,7 +177,8 @@ void SendProcessEvent(DAP &dap, LaunchMethod launch_method) {
dap.SendJSON(llvm::json::Value(std::move(event)));
}
-static void SendStoppedEvent(DAP &dap, lldb::SBThread &thread, bool on_entry) {
+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::eStopReasonEntry;
@@ -239,8 +242,8 @@ static void SendStoppedEvent(DAP &dap, lldb::SBThread &thread, bool on_entry) {
thread.GetStopDescription(description);
body.description = {description.GetData(), description.GetSize()};
body.threadId = tid;
- body.preserveFocusHint = tid == dap.focus_tid;
- body.allThreadsStopped = true;
+ body.allThreadsStopped = all_threads_stopped;
+ body.preserveFocusHint = preserve_focus;
dap.Send(protocol::Event{"stopped", std::move(body)});
}
@@ -263,27 +266,31 @@ llvm::Error SendThreadStoppedEvent(DAP &dap, bool on_entry) {
old_thread_ids.swap(dap.thread_ids);
const uint32_t num_threads = process.GetNumThreads();
- lldb::SBThread stopped_thread;
+ lldb::tid_t focused_tid = LLDB_INVALID_THREAD_ID;
for (uint32_t thread_idx = 0; thread_idx < num_threads; ++thread_idx) {
lldb::SBThread thread = process.GetThreadAtIndex(thread_idx);
// Collect all known thread ids for sending thread events.
dap.thread_ids.insert(thread.GetThreadID());
- if (stopped_thread || !ThreadHasStopReason(thread))
+ if (!ThreadHasStopReason(thread))
continue;
- // Stop at the first thread with a stop reason.
- stopped_thread = thread;
- }
+ // 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);
- if (!stopped_thread)
- return make_error<DAPError>(
- "no valid thread found, cannot determine why the process is stopped");
+ // Default focus to the first stopped thread.
+ if (focused_tid == LLDB_INVALID_THREAD_ID)
+ focused_tid = thread.GetThreadID();
+ }
- SendStoppedEvent(dap, stopped_thread, on_entry);
+ if (focused_tid == LLDB_INVALID_THREAD_ID)
+ return make_error<DAPError>("no stopped threads");
// Update focused thread.
- dap.focus_tid = stopped_thread.GetThreadID();
+ dap.focus_tid = focused_tid;
for (const auto &tid : old_thread_ids) {
auto end = dap.thread_ids.end();
More information about the lldb-commits
mailing list