[Lldb-commits] [lldb] [lldb] Transport accelerator actions in stop replies (PR #237448)
satyanarayana reddy janga via lldb-commits
lldb-commits at lists.llvm.org
Fri Oct 30 18:00:39 PDT 2026
https://github.com/satyajanga created https://github.com/llvm/llvm-project/pull/237448
This adds the vendor-neutral transport needed for accelerator plugins and
accelerator processes to request native-process work from stop replies.
- let native-connection accelerator plugins react to native stops
- let accelerator processes attach one or more hex-encoded JSON action fields
to a stop reply
- parse the complete stop reply before routing its actions to the CPU process
- preserve the initial `vRun` stop reply instead of losing one-shot actions
- deduplicate the full `(plugin name, action ID)` identity with at-most-once
semantics
LLGS asks for actions while constructing the stop reply, matching the existing
server-plugin design. The hooks may run on a process state-notification thread
and are explicitly required to be thread-safe. Actions are currently emitted
only in all-stop mode; non-stop replies do not consume action providers. This
patch contains only common infrastructure; it adds no AMD- or NVIDIA-specific
implementation.
Validation:
- `ninja lldb-server ProcessGdbRemoteTests LLDBServerTests`
- `ProcessGdbRemoteTests` (51/51)
- `LLDBServerTests` (3/3)
- `TestMockAcceleratorPackets.py` (8/8)
- `TestMockAcceleratorActions.py` (6/6)
- `TestAcceleratorActionsStopReply.py` (1/1)
- LLVM C++ formatting check
- Black, Darker, pylint `E0102`, and Python byte-compilation checks
>From 20083e8280922a1f3e4e34478afcb9ba89b0e4ff Mon Sep 17 00:00:00 2001
From: satya janga <satyajanga at fb.com>
Date: Fri, 30 Oct 2026 16:46:24 -0700
Subject: [PATCH] [lldb] Transport accelerator actions in stop replies
---
lldb/docs/resources/lldbgdbremote.md | 16 +++-
.../lldb/Host/common/NativeProcessProtocol.h | 11 +++
.../Utility/AcceleratorGDBRemotePackets.h | 12 +--
.../gdb-server/PlatformRemoteGDBServer.cpp | 6 +-
.../GDBRemoteCommunicationClient.cpp | 16 ++--
.../gdb-remote/GDBRemoteCommunicationClient.h | 4 +-
.../GDBRemoteCommunicationServerLLGS.cpp | 27 ++++++
.../gdb-remote/LLDBServerAcceleratorPlugin.h | 25 ++++-
.../Process/gdb-remote/ProcessGDBRemote.cpp | 85 ++++++++++++++---
.../Process/gdb-remote/ProcessGDBRemote.h | 19 ++--
.../mock/TestMockAcceleratorActions.py | 43 +++++++++
.../mock/TestMockAcceleratorPackets.py | 94 +++++++++++++++++++
lldb/test/API/accelerator/mock/main.c | 6 ++
.../TestAcceleratorActionsStopReply.py | 79 ++++++++++++++++
.../Mock/LLDBServerMockAcceleratorPlugin.cpp | 17 ++++
.../Mock/LLDBServerMockAcceleratorPlugin.h | 7 ++
.../Mock/ProcessMockAccelerator.cpp | 16 ++++
.../Accelerator/Mock/ProcessMockAccelerator.h | 10 ++
18 files changed, 449 insertions(+), 44 deletions(-)
create mode 100644 lldb/test/API/functionalities/gdb_remote_client/TestAcceleratorActionsStopReply.py
diff --git a/lldb/docs/resources/lldbgdbremote.md b/lldb/docs/resources/lldbgdbremote.md
index 53a36646444bfe..8a4fcd85601a9b 100644
--- a/lldb/docs/resources/lldbgdbremote.md
+++ b/lldb/docs/resources/lldbgdbremote.md
@@ -2918,17 +2918,27 @@ in lldb-server.
An accelerator plugin drives the client by returning a list of
`AcceleratorActions` (currently just breakpoints to set in the native
-process). The client can receive `AcceleratorActions` at two points:
+process). The client can receive `AcceleratorActions` at three points:
1. Once, in response to the `jAcceleratorPluginInitialize` packet sent when
the native process is launched or attached.
2. In response to a `jAcceleratorPluginBreakpointHit` packet, when a
breakpoint the plugin previously requested is hit. The hit response may
carry a further set of `AcceleratorActions`.
+3. In an `accelerator-actions` field of a stop reply. Plugins installed on the
+ native connection can return actions when the native process stops, and an
+ accelerator process can return actions for the client to perform in its
+ associated native process. The field value is hex-encoded JSON, and the
+ field can occur more than once when several plugins return actions. These
+ fields are currently emitted only in all-stop mode; action providers are not
+ called while the connection is in non-stop mode.
Each set of `AcceleratorActions` is tagged with a `plugin_name` and an
-`identifier` that is unique within that plugin, so the client can ignore a
-set it has already processed if the same actions are delivered again.
+`identifier` that is not reused by any source or connection for that plugin
+during the debugging session, so the client can ignore a set it has already
+processed if the same actions are delivered again. The client records the
+identifier before applying the actions, so delivery is at most once: replaying
+an action after a partial failure does not retry it.
### jAcceleratorPluginInitialize
diff --git a/lldb/include/lldb/Host/common/NativeProcessProtocol.h b/lldb/include/lldb/Host/common/NativeProcessProtocol.h
index da1fa26abefa9e..40a90e5b816a75 100644
--- a/lldb/include/lldb/Host/common/NativeProcessProtocol.h
+++ b/lldb/include/lldb/Host/common/NativeProcessProtocol.h
@@ -14,6 +14,7 @@
#include "NativeWatchpointList.h"
#include "lldb/Host/Host.h"
#include "lldb/Host/MainLoop.h"
+#include "lldb/Utility/AcceleratorGDBRemotePackets.h"
#include "lldb/Utility/AddressSpace.h"
#include "lldb/Utility/ArchSpec.h"
#include "lldb/Utility/Iterable.h"
@@ -101,6 +102,16 @@ class NativeProcessProtocol {
/// Served over the "jAddressSpacesInfo" packet.
virtual std::vector<AddressSpaceInfo> GetAddressSpaces() { return {}; }
+ /// Return accelerator actions to include in this process's next stop reply.
+ /// This lets an accelerator process request work in its associated native
+ /// process. Returning std::nullopt adds no actions. LLGS calls this only for
+ /// all-stop replies. The call can occur on a process state-notification
+ /// thread, so implementations must be thread-safe. If an action is returned
+ /// more than once for the same event, it must reuse its identifier.
+ virtual std::optional<AcceleratorActions> GetAcceleratorActions() {
+ return std::nullopt;
+ }
+
/// Plugins without address spaces should error on a non-default one.
virtual Status ReadMemory(const ProcessAddress &addr, void *buf, size_t size,
size_t &bytes_read) = 0;
diff --git a/lldb/include/lldb/Utility/AcceleratorGDBRemotePackets.h b/lldb/include/lldb/Utility/AcceleratorGDBRemotePackets.h
index 6a2ea74ff2bc37..b626d8b5c5dc5d 100644
--- a/lldb/include/lldb/Utility/AcceleratorGDBRemotePackets.h
+++ b/lldb/include/lldb/Utility/AcceleratorGDBRemotePackets.h
@@ -123,11 +123,9 @@ llvm::json::Value toJSON(const AcceleratorConnectionInfo &data);
/// the AcceleratorBreakpointHitResponse contains an AcceleratorActions that
/// can request additional breakpoints or other actions.
///
-/// In future patches, AcceleratorActions will also be returned:
-/// - When the native process stops (via NativeProcessIsStopping), allowing
-/// plugins to react to arbitrary stop events.
-/// - Via accelerator stop reply packets, enabling plugins to inject actions
-/// into the native process asynchronously.
+/// - Stop replies: NativeProcessIsStopping lets plugins react to native stops,
+/// while an accelerator process can inject actions into its stop replies for
+/// the client to perform in the native process.
struct AcceleratorActions {
AcceleratorActions() = default;
AcceleratorActions(llvm::StringRef plugin_name, int64_t action_id)
@@ -137,7 +135,9 @@ struct AcceleratorActions {
std::string plugin_name;
/// Human-readable label for the accelerator target.
std::string session_name;
- /// Unique identifier for this action within the plugin.
+ /// Unique identifier for this action within the plugin. It must not be
+ /// reused by any action source or connection for the plugin during the
+ /// debugging session.
int64_t identifier = 0;
/// New breakpoints to set. Nothing to set if this is empty.
std::vector<AcceleratorBreakpointInfo> breakpoints;
diff --git a/lldb/source/Plugins/Platform/gdb-server/PlatformRemoteGDBServer.cpp b/lldb/source/Plugins/Platform/gdb-server/PlatformRemoteGDBServer.cpp
index f1858deb7fec6f..30126e9fa09443 100644
--- a/lldb/source/Plugins/Platform/gdb-server/PlatformRemoteGDBServer.cpp
+++ b/lldb/source/Plugins/Platform/gdb-server/PlatformRemoteGDBServer.cpp
@@ -364,10 +364,12 @@ Status PlatformRemoteGDBServer::LaunchProcess(ProcessLaunchInfo &launch_info) {
Args args = launch_info.GetArguments();
if (FileSpec exe_file = launch_info.GetExecutableFile())
args.ReplaceArgumentAtIndex(0, exe_file.GetPath(false));
- if (llvm::Error err = m_gdb_client_up->LaunchProcess(args)) {
+ llvm::Expected<StringExtractorGDBRemote> launch_response =
+ m_gdb_client_up->LaunchProcess(args);
+ if (!launch_response) {
error = Status::FromErrorStringWithFormatv(
"Cannot launch '{0}': {1}", args.GetArgumentAtIndex(0),
- llvm::fmt_consume(std::move(err)));
+ llvm::fmt_consume(launch_response.takeError()));
return error;
}
}
diff --git a/lldb/source/Plugins/Process/gdb-remote/GDBRemoteCommunicationClient.cpp b/lldb/source/Plugins/Process/gdb-remote/GDBRemoteCommunicationClient.cpp
index 1a5b5a2c534d84..30daa2d099b15a 100644
--- a/lldb/source/Plugins/Process/gdb-remote/GDBRemoteCommunicationClient.cpp
+++ b/lldb/source/Plugins/Process/gdb-remote/GDBRemoteCommunicationClient.cpp
@@ -951,10 +951,13 @@ lldb::pid_t GDBRemoteCommunicationClient::GetCurrentProcessID(bool allow_lazy) {
return LLDB_INVALID_PROCESS_ID;
}
-llvm::Error GDBRemoteCommunicationClient::LaunchProcess(const Args &args) {
+llvm::Expected<StringExtractorGDBRemote>
+GDBRemoteCommunicationClient::LaunchProcess(const Args &args) {
if (!args.GetArgumentAtIndex(0))
return llvm::createStringError(llvm::inconvertibleErrorCode(),
"Nothing to launch");
+ StringExtractorGDBRemote response;
+
// try vRun first
if (m_supports_vRun) {
StreamString packet;
@@ -964,7 +967,6 @@ llvm::Error GDBRemoteCommunicationClient::LaunchProcess(const Args &args) {
packet.PutStringAsRawHex8(arg.ref());
}
- StringExtractorGDBRemote response;
if (SendPacketAndWaitForResponse(packet.GetString(), response) !=
PacketResult::Success)
return llvm::createStringError(llvm::inconvertibleErrorCode(),
@@ -973,11 +975,10 @@ llvm::Error GDBRemoteCommunicationClient::LaunchProcess(const Args &args) {
if (response.IsErrorResponse())
return response.GetStatus().ToError();
- // vRun replies with a stop reason packet
- // FIXME: right now we just discard the packet and LLDB queries
- // for stop reason again
+ // Preserve the vRun stop reply so fields that occur only on this first
+ // response, such as accelerator actions, are not lost.
if (!response.IsUnsupportedResponse())
- return llvm::Error::success();
+ return response;
m_supports_vRun = false;
}
@@ -992,7 +993,6 @@ llvm::Error GDBRemoteCommunicationClient::LaunchProcess(const Args &args) {
packet.PutStringAsRawHex8(arg.value().ref());
}
- StringExtractorGDBRemote response;
if (SendPacketAndWaitForResponse(packet.GetString(), response) !=
PacketResult::Success) {
return llvm::createStringError(llvm::inconvertibleErrorCode(),
@@ -1007,7 +1007,7 @@ llvm::Error GDBRemoteCommunicationClient::LaunchProcess(const Args &args) {
"Sending qLaunchSuccess packet failed");
}
if (response.IsOKResponse())
- return llvm::Error::success();
+ return response;
if (response.GetChar() == 'E') {
return llvm::createStringError(llvm::inconvertibleErrorCode(),
response.GetStringRef().substr(1));
diff --git a/lldb/source/Plugins/Process/gdb-remote/GDBRemoteCommunicationClient.h b/lldb/source/Plugins/Process/gdb-remote/GDBRemoteCommunicationClient.h
index e38eb03f7593e0..0490c8a17a4741 100644
--- a/lldb/source/Plugins/Process/gdb-remote/GDBRemoteCommunicationClient.h
+++ b/lldb/source/Plugins/Process/gdb-remote/GDBRemoteCommunicationClient.h
@@ -95,7 +95,9 @@ class GDBRemoteCommunicationClient : public GDBRemoteClientBase {
///
/// \param[in] args
/// A list of program arguments. The first entry is the program being run.
- llvm::Error LaunchProcess(const Args &args);
+ /// Launch a process and return the launch response. With vRun this can be
+ /// the process's initial stop reply rather than an OK packet.
+ llvm::Expected<StringExtractorGDBRemote> LaunchProcess(const Args &args);
/// Sends a "QEnvironment:NAME=VALUE" packet that will build up the
/// environment that will get used when launching an application
diff --git a/lldb/source/Plugins/Process/gdb-remote/GDBRemoteCommunicationServerLLGS.cpp b/lldb/source/Plugins/Process/gdb-remote/GDBRemoteCommunicationServerLLGS.cpp
index db62be06d60ff9..7e5716091bb9e4 100644
--- a/lldb/source/Plugins/Process/gdb-remote/GDBRemoteCommunicationServerLLGS.cpp
+++ b/lldb/source/Plugins/Process/gdb-remote/GDBRemoteCommunicationServerLLGS.cpp
@@ -1105,6 +1105,33 @@ GDBRemoteCommunicationServerLLGS::SendStopReplyPacketForThread(
if (response.Empty())
return SendErrorResponse(42);
+ // Non-stop mode can discard and rebuild queued stop notifications. Do not
+ // consume one-shot action providers until an all-stop reply can deliver the
+ // actions directly.
+ if (!m_non_stop) {
+ auto append_actions = [&](const AcceleratorActions &actions) {
+ response.PutCString("accelerator-actions:");
+ StreamGDBRemote encoded_actions;
+ encoded_actions.PutAsJSON(actions, /*hex_ascii=*/true);
+ response.PutCString(encoded_actions.GetString());
+ response.PutChar(';');
+ };
+
+ // Plugins installed on the native connection can react to native stops.
+ for (const std::unique_ptr<LLDBServerAcceleratorPlugin> &plugin_up :
+ m_accelerator_plugins) {
+ if (std::optional<AcceleratorActions> actions =
+ plugin_up->NativeProcessIsStopping(process.GetID()))
+ append_actions(*actions);
+ }
+
+ // An accelerator process can request actions in its associated native
+ // process through the accelerator connection's stop reply.
+ if (std::optional<AcceleratorActions> actions =
+ process.GetAcceleratorActions())
+ append_actions(*actions);
+ }
+
if (m_non_stop && !force_synchronous) {
PacketResult ret = SendNotificationPacketNoLock(
"Stop", m_stop_notification_queue, response.GetString());
diff --git a/lldb/source/Plugins/Process/gdb-remote/LLDBServerAcceleratorPlugin.h b/lldb/source/Plugins/Process/gdb-remote/LLDBServerAcceleratorPlugin.h
index 2e585d9a64c6aa..e4fa90324e4a09 100644
--- a/lldb/source/Plugins/Process/gdb-remote/LLDBServerAcceleratorPlugin.h
+++ b/lldb/source/Plugins/Process/gdb-remote/LLDBServerAcceleratorPlugin.h
@@ -13,6 +13,7 @@
#include "lldb/Host/common/NativeProcessProtocol.h"
#include "lldb/Utility/AcceleratorGDBRemotePackets.h"
#include "llvm/ADT/StringRef.h"
+#include <atomic>
#include <memory>
#include <optional>
@@ -37,14 +38,28 @@ class LLDBServerAcceleratorPlugin {
virtual std::optional<AcceleratorActions> GetInitializeActions() = 0;
+ /// Return accelerator actions to include when native process \p pid stops.
+ /// Returning std::nullopt adds no actions to the stop reply. Servers can
+ /// debug multiple processes, so plugins should ignore unrelated PIDs. LLGS
+ /// calls this only for all-stop replies. The call can occur on a process
+ /// state-notification thread, so implementations must be thread-safe. If an
+ /// action is returned more than once for the same event, it must reuse its
+ /// identifier.
+ virtual std::optional<AcceleratorActions>
+ NativeProcessIsStopping(lldb::pid_t pid) {
+ return std::nullopt;
+ }
+
virtual llvm::Expected<AcceleratorBreakpointHitResponse>
BreakpointWasHit(AcceleratorBreakpointHitArgs &args) = 0;
- /// Create an AcceleratorActions with an identifier unique within this plugin,
- /// so identifiers from different actions don't collide.
+ /// Create an AcceleratorActions with an identifier unique within this plugin.
+ /// Other action sources and connections for the plugin must coordinate with
+ /// this sequence so no identifier is reused during a debugging session.
AcceleratorActions GetNewAcceleratorAction() {
return AcceleratorActions(GetPluginName(),
- ++m_accelerator_action_identifier);
+ m_next_accelerator_action_identifier.fetch_add(
+ 1, std::memory_order_relaxed));
}
virtual std::optional<AcceleratorDynamicLoaderResponse>
@@ -57,7 +72,9 @@ class LLDBServerAcceleratorPlugin {
MainLoop &m_native_main_loop;
std::unique_ptr<Manager> m_process_manager_up;
std::unique_ptr<GDBServer> m_accelerator_gdb_server;
- int64_t m_accelerator_action_identifier = 0;
+ // Atomic signed fetch_add has defined two's-complement wrapping. The full
+ // int64_t identifier space is used before an identifier can be reused.
+ std::atomic<int64_t> m_next_accelerator_action_identifier{1};
};
} // namespace lldb_server
diff --git a/lldb/source/Plugins/Process/gdb-remote/ProcessGDBRemote.cpp b/lldb/source/Plugins/Process/gdb-remote/ProcessGDBRemote.cpp
index 540f620be4c957..7377de9886cd60 100644
--- a/lldb/source/Plugins/Process/gdb-remote/ProcessGDBRemote.cpp
+++ b/lldb/source/Plugins/Process/gdb-remote/ProcessGDBRemote.cpp
@@ -72,6 +72,7 @@
#include "lldb/Target/TargetGroupList.h"
#include "lldb/Target/TargetList.h"
#include "lldb/Target/ThreadPlanCallFunction.h"
+#include "lldb/Utility/AcceleratorGDBRemotePackets.h"
#include "lldb/Utility/Args.h"
#include "lldb/Utility/Baton.h"
#include "lldb/Utility/FileSpec.h"
@@ -747,6 +748,8 @@ Status ProcessGDBRemote::DoConnectRemote(llvm::StringRef remote_url) {
Status ProcessGDBRemote::WillLaunchOrAttach() {
Status error;
m_stdio_communication.Clear();
+ std::lock_guard<std::recursive_mutex> guard(m_accelerator_actions_mutex);
+ m_processed_accelerator_actions.clear();
return error;
}
@@ -893,6 +896,7 @@ Status ProcessGDBRemote::DoLaunch(lldb_private::Module *exe_module,
// Send the environment and the program + arguments after we connect
m_gdb_comm.SendEnvironment(launch_info.GetEnvironment());
+ StringExtractorGDBRemote response;
{
// Scope for the scoped timeout object
GDBRemoteCommunication::ScopedTimeout timeout(m_gdb_comm,
@@ -914,11 +918,14 @@ Status ProcessGDBRemote::DoLaunch(lldb_private::Module *exe_module,
exe_file.GetPath(/*denormalize=*/true));
}
}
- if (llvm::Error err = m_gdb_comm.LaunchProcess(args)) {
+ llvm::Expected<StringExtractorGDBRemote> launch_response =
+ m_gdb_comm.LaunchProcess(args);
+ if (!launch_response) {
error = Status::FromErrorStringWithFormatv(
"Cannot launch '{0}': {1}", args.GetArgumentAtIndex(0),
- llvm::fmt_consume(std::move(err)));
+ llvm::fmt_consume(launch_response.takeError()));
} else {
+ response = std::move(*launch_response);
SetID(m_gdb_comm.GetCurrentProcessID());
}
}
@@ -930,8 +937,11 @@ Status ProcessGDBRemote::DoLaunch(lldb_private::Module *exe_module,
return error;
}
- StringExtractorGDBRemote response;
- if (m_gdb_comm.GetStopReply(response)) {
+ llvm::StringRef launch_reply = response.GetStringRef();
+ bool launch_reply_is_stop =
+ !launch_reply.empty() &&
+ (launch_reply.front() == 'T' || launch_reply.front() == 'S');
+ if (launch_reply_is_stop || m_gdb_comm.GetStopReply(response)) {
SetLastStopPacket(response);
const ArchSpec &process_arch = m_gdb_comm.GetProcessArchitecture();
@@ -2473,6 +2483,7 @@ StateType ProcessGDBRemote::SetThreadStopInfo(StringExtractor &stop_packet) {
uint64_t queue_serial_number = 0;
ExpeditedRegisterMap expedited_register_map;
AddressableBits addressable_bits;
+ std::vector<AcceleratorActions> accelerator_actions;
while (stop_packet.GetNameColonValue(key, value)) {
if (key.compare("metype") == 0) {
// exception type in big endian hex
@@ -2631,6 +2642,19 @@ StateType ProcessGDBRemote::SetThreadStopInfo(StringExtractor &stop_packet) {
if (!value.getAsInteger(BASE_10, addressing_bits)) {
addressable_bits.SetHighmemAddressableBits(addressing_bits);
}
+ } else if (key == "accelerator-actions") {
+ StringExtractor json_extractor(value);
+ std::string json;
+ json_extractor.GetHexByteString(json);
+ llvm::Expected<AcceleratorActions> actions =
+ llvm::json::parse<AcceleratorActions>(json, "AcceleratorActions");
+ if (!actions) {
+ Debugger::ReportError(llvm::formatv(
+ "failed to parse accelerator actions from stop reply: {0}",
+ llvm::fmt_consume(actions.takeError())));
+ } else {
+ accelerator_actions.push_back(std::move(*actions));
+ }
} else if (key == "added-binaries") {
// A comma separated list of all threads in the current
// process that includes the thread for this stop reply packet
@@ -2691,6 +2715,16 @@ StateType ProcessGDBRemote::SetThreadStopInfo(StringExtractor &stop_packet) {
queue_kind, queue_serial_number, added_binaries,
detailed_binaries_info);
+ // Apply the complete stop reply before dispatching actions. Later
+ // synchronization fields in the packet must take effect before an action
+ // can wait on that state.
+ for (const AcceleratorActions &actions : accelerator_actions) {
+ if (llvm::Error error = HandleAcceleratorActions(actions))
+ Debugger::ReportError(llvm::formatv(
+ "failed to handle accelerator actions: {0}\nActions:\n{1}",
+ llvm::fmt_consume(std::move(error)), toJSON(actions)));
+ }
+
return eStateStopped;
} break;
@@ -4336,22 +4370,45 @@ llvm::Error
ProcessGDBRemote::HandleAcceleratorActions(const AcceleratorActions &actions) {
Log *log = GetLog(GDBRLog::Process);
- // The same set of actions can be delivered to the client more than once: a
- // plugin may keep reporting the same actions (with the same identifier) on
- // subsequent native stops until its state advances. The identifier uniquely
- // names a set of actions for a plugin, so skip any set we have already
- // processed to avoid re-running its side effects (e.g. setting the same
- // breakpoints again).
- auto it = m_processed_accelerator_actions.find(actions.plugin_name);
- if (it != m_processed_accelerator_actions.end() &&
- it->second == actions.identifier) {
+ // Actions always apply to the native process. Route actions received on an
+ // accelerator connection through the CPU target in the shared group.
+ std::shared_ptr<TargetGroupCPUAndAccelerators> group_sp =
+ GetTarget().GetFirstTargetGroupOfType<TargetGroupCPUAndAccelerators>();
+ if (group_sp && group_sp->GetRole(GetTarget().shared_from_this()) ==
+ TargetGroup::Role::Accelerator) {
+ TargetSP cpu_target_sp = group_sp->GetCPUTarget();
+ if (!cpu_target_sp)
+ return llvm::createStringError(
+ "accelerator target group does not contain a CPU target");
+
+ ProcessSP cpu_process_sp = cpu_target_sp->GetProcessSP();
+ if (!cpu_process_sp)
+ return llvm::createStringError("CPU target does not have a process");
+ if (cpu_process_sp->GetPluginName() != GetPluginNameStatic())
+ return llvm::createStringError(
+ "CPU target is not using the gdb-remote process plugin");
+
+ return std::static_pointer_cast<ProcessGDBRemote>(cpu_process_sp)
+ ->HandleAcceleratorActions(actions);
+ }
+
+ // Native and accelerator communication threads can both deliver actions to
+ // this CPU process. Serialize the deduplication check and all action side
+ // effects as one operation.
+ std::lock_guard<std::recursive_mutex> guard(m_accelerator_actions_mutex);
+
+ // Record before executing for deliberate at-most-once delivery: retrying an
+ // action that failed partway through could repeat an earlier side effect.
+ bool inserted = m_processed_accelerator_actions
+ .emplace(actions.plugin_name, actions.identifier)
+ .second;
+ if (!inserted) {
LLDB_LOG(log,
"ProcessGDBRemote::HandleAcceleratorActions skipping already "
"processed actions for plugin '{0}' with identifier {1}",
actions.plugin_name, actions.identifier);
return llvm::Error::success();
}
- m_processed_accelerator_actions[actions.plugin_name] = actions.identifier;
// Handle each kind of action. More action kinds will be handled here in the
// future, so only return early on error; otherwise fall through so the next
diff --git a/lldb/source/Plugins/Process/gdb-remote/ProcessGDBRemote.h b/lldb/source/Plugins/Process/gdb-remote/ProcessGDBRemote.h
index 018d0699ef1586..316df6523ed072 100644
--- a/lldb/source/Plugins/Process/gdb-remote/ProcessGDBRemote.h
+++ b/lldb/source/Plugins/Process/gdb-remote/ProcessGDBRemote.h
@@ -14,6 +14,7 @@
#include <memory>
#include <mutex>
#include <optional>
+#include <set>
#include <string>
#include <vector>
@@ -520,12 +521,18 @@ class ProcessGDBRemote : public Process,
lldb::user_id_t break_id,
lldb::user_id_t break_loc_id);
- /// Tracks the last action identifier handled per accelerator plugin so the
- /// same actions are not processed more than once. Accelerator actions can
- /// arrive in a stop reply packet, and if the debugger re-requests the stop
- /// reply with a '?' packet we can be handed the same actions again; this
- /// guards against handling them (e.g. setting the same breakpoints) twice.
- std::map<std::string, int64_t> m_processed_accelerator_actions;
+ /// Tracks every action identifier accepted for each accelerator plugin so
+ /// replaying older actions cannot repeat their side effects. An action is
+ /// recorded before it is executed, giving action delivery at-most-once
+ /// semantics even when handling fails partway through. The set is cleared
+ /// before a new process connection, so it is bounded by that connection.
+ std::set<std::pair<std::string, int64_t>> m_processed_accelerator_actions;
+
+ /// Serializes deduplication and all target mutations performed by actions
+ /// arriving from the native and accelerator communication threads. This is
+ /// recursive because a synchronous accelerator connection can route its
+ /// initial actions back to the CPU process on the same thread.
+ std::recursive_mutex m_accelerator_actions_mutex;
// ContinueDelegate interface
void HandleAsyncStdout(llvm::StringRef out) override;
diff --git a/lldb/test/API/accelerator/mock/TestMockAcceleratorActions.py b/lldb/test/API/accelerator/mock/TestMockAcceleratorActions.py
index 58c9f2e193ed9b..2dc218fc307766 100644
--- a/lldb/test/API/accelerator/mock/TestMockAcceleratorActions.py
+++ b/lldb/test/API/accelerator/mock/TestMockAcceleratorActions.py
@@ -179,6 +179,49 @@ def test_accelerator_actions(self):
self.assertState(process.GetState(), lldb.eStateExited)
self.assertEqual(process.GetExitStatus(), 0)
+ @skipIfRemote
+ @add_test_categories(["llgs"])
+ def test_actions_from_native_stop_reply(self):
+ """Actions in the initial native vRun stop reply are not lost."""
+ self.set_mock_env("LLDB_MOCK_ACCELERATOR_NATIVE_STOP_ACTIONS", "1")
+ self.build()
+ target = self.dbg.CreateTarget(self.getBuildArtifact("a.out"))
+ self.assertTrue(target, VALID_TARGET)
+
+ process = target.LaunchSimple(None, None, self.get_process_working_directory())
+ self.assertTrue(process, PROCESS_IS_VALID)
+ self.check_accelerator_breakpoint_stop(
+ process, "mock_gpu_accelerator_native_stop_action", hit_count=1
+ )
+
+ @skipIfRemote
+ @add_test_categories(["llgs"])
+ def test_actions_from_accelerator_stop_reply(self):
+ """Actions on the accelerator connection run in the CPU process."""
+ self.set_mock_env("LLDB_MOCK_ACCELERATOR_PROCESS_STOP_ACTIONS", "1")
+ self.build()
+ target = self.dbg.CreateTarget(self.getBuildArtifact("a.out"))
+ self.assertTrue(target, VALID_TARGET)
+
+ process = target.LaunchSimple(None, None, self.get_process_working_directory())
+ self.assertTrue(process, PROCESS_IS_VALID)
+ self.check_accelerator_breakpoint_stop(
+ process, "mock_gpu_accelerator_initialize", hit_count=1
+ )
+
+ # Connecting the accelerator process parses its stop reply and routes
+ # the requested breakpoint through the group's CPU target.
+ process.Continue()
+ self.check_accelerator_breakpoint_stop(
+ process, "mock_gpu_accelerator_connect", hit_count=1
+ )
+ self.assertEqual(self.dbg.GetNumTargets(), 2)
+
+ process.Continue()
+ self.check_accelerator_breakpoint_stop(
+ process, "mock_gpu_accelerator_process_stop_action", hit_count=1
+ )
+
def run_expecting_failed_connection(self):
"""Run the inferior through its breakpoints, asserting the connection is
attempted at the connection hook but no accelerator target is created
diff --git a/lldb/test/API/accelerator/mock/TestMockAcceleratorPackets.py b/lldb/test/API/accelerator/mock/TestMockAcceleratorPackets.py
index e40e6491e59a05..1f6e2dbf7b24b5 100644
--- a/lldb/test/API/accelerator/mock/TestMockAcceleratorPackets.py
+++ b/lldb/test/API/accelerator/mock/TestMockAcceleratorPackets.py
@@ -7,8 +7,11 @@
"""
import json
+import os
import gdbremote_testcase
+from lldbsuite.support import seven
+import lldbsuite.test.lldbutil as lldbutil
from lldbsuite.test.decorators import *
from lldbsuite.test.gdbclientutils import escape_binary
from lldbsuite.test.lldbtest import *
@@ -28,6 +31,18 @@ def setUp(self):
if "mock-accelerator" not in configuration.enabled_plugins:
self.skipTest("mock-accelerator plugin is not enabled")
+ def set_mock_env(self, name, value):
+ previous = os.environ.get(name)
+ os.environ[name] = value
+
+ def restore():
+ if previous is None:
+ os.environ.pop(name, None)
+ else:
+ os.environ[name] = previous
+
+ self.addTearDownHook(restore)
+
def send_and_decode_json(self, packet):
"""Send a packet and return the decoded JSON response."""
self.test_sequence.add_log_lines(
@@ -59,6 +74,85 @@ def test_qSupported_reports_accelerator_plugins(self):
self.assertIn("accelerator-plugins", features)
self.assertEqual(features["accelerator-plugins"], "+")
+ @add_test_categories(["llgs"])
+ def test_legacy_launch_stop_reply_actions(self):
+ """The A-packet launch path reports actions in the following stop."""
+ self.set_mock_env("LLDB_MOCK_ACCELERATOR_NATIVE_STOP_ACTIONS", "1")
+ self.build()
+ server = self.connect_to_debug_monitor()
+ self.assertIsNotNone(server)
+ self.do_handshake()
+
+ exe_path = lldbutil.install_to_target(self, self.getBuildArtifact("a.out"))
+ hex_path = seven.hexlify(exe_path)
+ self.test_sequence.add_log_lines(
+ [
+ "read packet: $A %d,0,%s#00" % (len(hex_path), hex_path),
+ "send packet: $OK#00",
+ "read packet: $qLaunchSuccess#00",
+ "send packet: $OK#00",
+ "read packet: $?#00",
+ {
+ "direction": "send",
+ "regex": (
+ r"^\$T[0-9a-fA-F]+.*accelerator-actions:" r"([0-9a-fA-F]+);"
+ ),
+ "capture": {1: "actions"},
+ },
+ ],
+ True,
+ )
+ context = self.expect_gdbremote_sequence()
+ actions = json.loads(seven.unhexlify(context["actions"]))
+ self.assertEqual(actions["plugin_name"], "mock")
+ self.assertEqual(
+ actions["breakpoints"][0]["by_name"]["function_name"],
+ "mock_gpu_accelerator_native_stop_action",
+ )
+
+ @add_test_categories(["llgs"])
+ def test_stop_reply_actions_are_all_stop_only(self):
+ """Non-stop replies do not consume actions needed by all-stop."""
+ self.set_mock_env("LLDB_MOCK_ACCELERATOR_NATIVE_STOP_ACTIONS", "1")
+ self.build()
+ server = self.connect_to_debug_monitor()
+ self.assertIsNotNone(server)
+ self.do_handshake()
+
+ exe_path = lldbutil.install_to_target(self, self.getBuildArtifact("a.out"))
+ hex_path = seven.hexlify(exe_path)
+ no_action_reply = r"^\$T(?!.*accelerator-actions:).*$"
+ self.test_sequence.add_log_lines(
+ [
+ "read packet: $QNonStop:1#00",
+ "send packet: $OK#00",
+ "read packet: $A %d,0,%s#00" % (len(hex_path), hex_path),
+ "send packet: $OK#00",
+ "read packet: $qLaunchSuccess#00",
+ "send packet: $OK#00",
+ "read packet: $?#00",
+ {"direction": "send", "regex": no_action_reply},
+ "read packet: $QNonStop:0#00",
+ "send packet: $OK#00",
+ "read packet: $?#00",
+ {
+ "direction": "send",
+ "regex": (
+ r"^\$T[0-9a-fA-F]+.*accelerator-actions:" r"([0-9a-fA-F]+);"
+ ),
+ "capture": {1: "actions"},
+ },
+ ],
+ True,
+ )
+ context = self.expect_gdbremote_sequence()
+ actions = json.loads(seven.unhexlify(context["actions"]))
+ self.assertEqual(actions["plugin_name"], "mock")
+ self.assertEqual(
+ actions["breakpoints"][0]["by_name"]["function_name"],
+ "mock_gpu_accelerator_native_stop_action",
+ )
+
@add_test_categories(["llgs"])
def test_jAcceleratorPluginInitialize_returns_breakpoints(self):
self.build()
diff --git a/lldb/test/API/accelerator/mock/main.c b/lldb/test/API/accelerator/mock/main.c
index 18aed944b8d8fb..5fa2a91eaa748b 100644
--- a/lldb/test/API/accelerator/mock/main.c
+++ b/lldb/test/API/accelerator/mock/main.c
@@ -2,6 +2,8 @@
// named functions.
void mock_gpu_accelerator_initialize(void) {}
+void mock_gpu_accelerator_native_stop_action(void) {}
+
int mock_gpu_accelerator_compute(int x) { return x * 2; }
int mock_gpu_accelerator_finish(void) { return 0; }
@@ -11,9 +13,13 @@ int mock_gpu_accelerator_finish(void) { return 0; }
// GDB server.
void mock_gpu_accelerator_connect(void) {}
+void mock_gpu_accelerator_process_stop_action(void) {}
+
int main(void) {
+ mock_gpu_accelerator_native_stop_action();
mock_gpu_accelerator_initialize();
mock_gpu_accelerator_connect();
+ mock_gpu_accelerator_process_stop_action();
int result = mock_gpu_accelerator_compute(21);
mock_gpu_accelerator_finish();
return result == 42 ? 0 : 1;
diff --git a/lldb/test/API/functionalities/gdb_remote_client/TestAcceleratorActionsStopReply.py b/lldb/test/API/functionalities/gdb_remote_client/TestAcceleratorActionsStopReply.py
new file mode 100644
index 00000000000000..0dcbadefd39899
--- /dev/null
+++ b/lldb/test/API/functionalities/gdb_remote_client/TestAcceleratorActionsStopReply.py
@@ -0,0 +1,79 @@
+import json
+
+from lldbsuite.test.gdbclientutils import MockGDBServerResponder, escape_binary
+from lldbsuite.test.lldbgdbclient import GDBRemoteTestBase
+
+
+def hex_json(value):
+ return json.dumps(value, separators=(",", ":")).encode().hex()
+
+
+class TestAcceleratorActionsStopReply(GDBRemoteTestBase):
+ def test_actions_are_parsed_and_deduplicated_from_stop_reply(self):
+ actions = []
+ for plugin_name, identifier, address in (
+ ("mock", 1, 0x1000),
+ ("mock", 2, 0x2000),
+ ("mock", 1, 0x1000),
+ ):
+ actions.append(
+ {
+ "plugin_name": plugin_name,
+ "session_name": "",
+ "identifier": identifier,
+ "breakpoints": [
+ {
+ "identifier": identifier,
+ "by_address": {"load_address": address},
+ "symbol_names": [],
+ }
+ ],
+ }
+ )
+
+ class Responder(MockGDBServerResponder):
+ def __init__(self):
+ super().__init__()
+ self.breakpoint_packets = []
+ self.clear_breakpoint_packets = []
+
+ def qSupported(self, client_supported):
+ return "PacketSize=3fff;QStartNoAckMode+;accelerator-plugins+"
+
+ def haltReason(self):
+ fields = "".join(
+ "accelerator-actions:%s;" % hex_json(action) for action in actions
+ )
+ return "T05thread:1;%s" % fields
+
+ def setBreakpoint(self, packet):
+ self.breakpoint_packets.append(packet)
+ return "OK"
+
+ def clearBreakpoint(self, packet):
+ self.clear_breakpoint_packets.append(packet)
+ return "OK"
+
+ def other(self, packet):
+ if packet == "jAcceleratorPluginInitialize":
+ return escape_binary("[]")
+ return ""
+
+ self.server.responder = Responder()
+ target = self.dbg.CreateTargetWithFileAndTargetTriple("", "x86_64-pc-linux")
+ process = self.connect(target)
+ self.assertTrue(process.IsValid())
+
+ self.assertEqual(len(self.server.responder.breakpoint_packets), 2)
+ self.assertTrue(
+ any(
+ packet.startswith("Z0,1000,")
+ for packet in self.server.responder.breakpoint_packets
+ )
+ )
+ self.assertTrue(
+ any(
+ packet.startswith("Z0,2000,")
+ for packet in self.server.responder.breakpoint_packets
+ )
+ )
diff --git a/lldb/tools/lldb-server/Plugins/Accelerator/Mock/LLDBServerMockAcceleratorPlugin.cpp b/lldb/tools/lldb-server/Plugins/Accelerator/Mock/LLDBServerMockAcceleratorPlugin.cpp
index b525f8e3592a7b..d5b2ddb7bdb835 100644
--- a/lldb/tools/lldb-server/Plugins/Accelerator/Mock/LLDBServerMockAcceleratorPlugin.cpp
+++ b/lldb/tools/lldb-server/Plugins/Accelerator/Mock/LLDBServerMockAcceleratorPlugin.cpp
@@ -80,6 +80,21 @@ LLDBServerMockAcceleratorPlugin::GetInitializeActions() {
return actions;
}
+std::optional<AcceleratorActions>
+LLDBServerMockAcceleratorPlugin::NativeProcessIsStopping(lldb::pid_t) {
+ if (!::getenv("LLDB_MOCK_ACCELERATOR_NATIVE_STOP_ACTIONS") ||
+ m_sent_native_stop_actions.exchange(true))
+ return std::nullopt;
+
+ AcceleratorActions actions = GetNewAcceleratorAction();
+ AcceleratorBreakpointInfo bp;
+ bp.identifier = kBreakpointIDNativeStopAction;
+ bp.by_name = AcceleratorBreakpointByName{
+ std::nullopt, "mock_gpu_accelerator_native_stop_action"};
+ actions.breakpoints.push_back(std::move(bp));
+ return actions;
+}
+
llvm::Expected<AcceleratorBreakpointHitResponse>
LLDBServerMockAcceleratorPlugin::BreakpointWasHit(
AcceleratorBreakpointHitArgs &args) {
@@ -125,6 +140,8 @@ LLDBServerMockAcceleratorPlugin::BreakpointWasHit(
}
case kBreakpointIDByAddress:
case kBreakpointIDByNameShlib:
+ case kBreakpointIDNativeStopAction:
+ case ProcessMockAccelerator::kStopReplyBreakpointID:
// Disable and stop the native process so the hit is observable.
response.disable_bp = true;
response.auto_resume_native = false;
diff --git a/lldb/tools/lldb-server/Plugins/Accelerator/Mock/LLDBServerMockAcceleratorPlugin.h b/lldb/tools/lldb-server/Plugins/Accelerator/Mock/LLDBServerMockAcceleratorPlugin.h
index 36f6aa2500ba80..4aab0d8b136642 100644
--- a/lldb/tools/lldb-server/Plugins/Accelerator/Mock/LLDBServerMockAcceleratorPlugin.h
+++ b/lldb/tools/lldb-server/Plugins/Accelerator/Mock/LLDBServerMockAcceleratorPlugin.h
@@ -14,6 +14,7 @@
#include "lldb/Host/MainLoop.h"
#include "lldb/Host/MainLoopBase.h"
+#include <atomic>
#include <memory>
#include <vector>
@@ -31,6 +32,8 @@ class LLDBServerMockAcceleratorPlugin : public LLDBServerAcceleratorPlugin {
llvm::StringRef GetPluginName() override;
std::optional<AcceleratorActions> GetInitializeActions() override;
+ std::optional<AcceleratorActions>
+ NativeProcessIsStopping(lldb::pid_t pid) override;
llvm::Expected<AcceleratorBreakpointHitResponse>
BreakpointWasHit(AcceleratorBreakpointHitArgs &args) override;
@@ -54,6 +57,10 @@ class LLDBServerMockAcceleratorPlugin : public LLDBServerAcceleratorPlugin {
// Breakpoint on the dedicated "mock_gpu_accelerator_connect" hook; only the
// connection test defines that function. See BreakpointWasHit for the hit.
static constexpr int64_t kBreakpointIDConnect = 4;
+ // Breakpoint requested through the native process's first stop reply.
+ static constexpr int64_t kBreakpointIDNativeStopAction = 5;
+
+ std::atomic<bool> m_sent_native_stop_actions{false};
// The mock accelerator server runs on its own main loop and thread so its
// packets don't contend with the native process. Populated by
diff --git a/lldb/tools/lldb-server/Plugins/Accelerator/Mock/ProcessMockAccelerator.cpp b/lldb/tools/lldb-server/Plugins/Accelerator/Mock/ProcessMockAccelerator.cpp
index 6ae74d290d7150..627a589383076c 100644
--- a/lldb/tools/lldb-server/Plugins/Accelerator/Mock/ProcessMockAccelerator.cpp
+++ b/lldb/tools/lldb-server/Plugins/Accelerator/Mock/ProcessMockAccelerator.cpp
@@ -13,6 +13,7 @@
#include "lldb/Host/ProcessLaunchInfo.h"
#include "llvm/Support/Error.h"
+#include <cstdlib>
#include <cstring>
using namespace lldb;
@@ -129,3 +130,18 @@ std::vector<AddressSpaceInfo> ProcessMockAccelerator::GetAddressSpaces() {
return {{"global", 1, /*is_thread_specific=*/false},
{"local", 2, /*is_thread_specific=*/true}};
}
+
+std::optional<AcceleratorActions>
+ProcessMockAccelerator::GetAcceleratorActions() {
+ if (!::getenv("LLDB_MOCK_ACCELERATOR_PROCESS_STOP_ACTIONS") ||
+ m_sent_accelerator_stop_actions.exchange(true))
+ return std::nullopt;
+
+ AcceleratorActions actions("mock", kStopReplyActionID);
+ AcceleratorBreakpointInfo bp;
+ bp.identifier = kStopReplyBreakpointID;
+ bp.by_name = AcceleratorBreakpointByName{
+ std::nullopt, "mock_gpu_accelerator_process_stop_action"};
+ actions.breakpoints.push_back(std::move(bp));
+ return actions;
+}
diff --git a/lldb/tools/lldb-server/Plugins/Accelerator/Mock/ProcessMockAccelerator.h b/lldb/tools/lldb-server/Plugins/Accelerator/Mock/ProcessMockAccelerator.h
index af1631ebc46084..478e288828e93e 100644
--- a/lldb/tools/lldb-server/Plugins/Accelerator/Mock/ProcessMockAccelerator.h
+++ b/lldb/tools/lldb-server/Plugins/Accelerator/Mock/ProcessMockAccelerator.h
@@ -10,7 +10,9 @@
#define LLDB_TOOLS_LLDB_SERVER_PLUGINS_ACCELERATOR_MOCK_PROCESSMOCKACCELERATOR_H
#include "lldb/Host/common/NativeProcessProtocol.h"
+
#include "lldb/Utility/ArchSpec.h"
+#include <atomic>
namespace lldb_private {
namespace lldb_server {
@@ -20,6 +22,11 @@ namespace lldb_server {
/// (accelerator) target without any real hardware.
class ProcessMockAccelerator : public NativeProcessProtocol {
public:
+ // Use an action identifier outside the native mock plugin's sequence. Action
+ // identifiers must be unique across all connections for a plugin.
+ static constexpr int64_t kStopReplyActionID = 1'000'000;
+ static constexpr int64_t kStopReplyBreakpointID = 6;
+
class Manager : public NativeProcessProtocol::Manager {
public:
using NativeProcessProtocol::Manager::Manager;
@@ -66,8 +73,11 @@ class ProcessMockAccelerator : public NativeProcessProtocol {
std::vector<AddressSpaceInfo> GetAddressSpaces() override;
+ std::optional<AcceleratorActions> GetAcceleratorActions() override;
+
private:
mutable ArchSpec m_arch;
+ std::atomic<bool> m_sent_accelerator_stop_actions{false};
};
} // namespace lldb_server
More information about the lldb-commits
mailing list