[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