Skip to content

[lldb-dap] Reimplement runInTerminal with signals #142374

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 1 commit into
base: main
Choose a base branch
from

Conversation

SuibianP
Copy link
Contributor

@SuibianP SuibianP commented Jun 2, 2025

runInTerminal is currently implemented using JSON messages over FIFOs, which feels too clunky for the simple job of passing a PID.

Instead, take advantage of si_pid available from sa_sigaction handler, and send a user signal instead. Both synchronisation and timeout are preserved with the protocol below,

  1. Debugger waits for SIGUSR1 under timeout with pselect
  2. Launcher forks into child, sends SIGUSR1 to debugger and suspends
  3. Debugger receives SIGUSR1, captures the sender (launcher child) PID, attaches to and resumes it
  4. Launcher child resumes and exec's into target
  5. Launcher parent waitpid's and kills irresponsive child after timeout

With this, the entirety of FifoFiles and RunInTerminal can be dropped.

Refs: #121269 (comment)

@llvmbot
Copy link
Member

llvmbot commented Jun 2, 2025

@llvm/pr-subscribers-lldb

Author: Hu Jialun (SuibianP)

Changes

runInTerminal is currently implemented using JSON messages over FIFOs, which feels too clunky for the simple job of passing a PID.

Instead, take advantage of si_pid available from sa_sigaction handler, and send a user signal instead. Both synchronisation and timeout are preserved with the protocol below,

  1. Debugger waits for SIGUSR1 under timeout with pselect
  2. Launcher forks into child, sends SIGUSR1 to debugger and suspends
  3. Debugger receives SIGUSR1, captures the sender (launcher child) PID, attaches to and resumes it
  4. Launcher child resumes and exec's into target
  5. Launcher parent waitpid's and kills irresponsive child after timeout

With this, the entirety of FifoFiles and RunInTerminal can be dropped.

Refs: #121269 (comment)


Patch is 36.38 KiB, truncated to 20.00 KiB below, full version: https://github.com/llvm/llvm-project/pull/142374.diff

11 Files Affected:

  • (modified) lldb/test/API/tools/lldb-dap/runInTerminal/TestDAP_runInTerminal.py (-117)
  • (modified) lldb/tools/lldb-dap/CMakeLists.txt (-2)
  • (removed) lldb/tools/lldb-dap/FifoFiles.cpp (-101)
  • (removed) lldb/tools/lldb-dap/FifoFiles.h (-85)
  • (modified) lldb/tools/lldb-dap/Handler/RequestHandler.cpp (+72-27)
  • (modified) lldb/tools/lldb-dap/JSONUtils.cpp (+2-3)
  • (modified) lldb/tools/lldb-dap/JSONUtils.h (+1-4)
  • (modified) lldb/tools/lldb-dap/Options.td (+5-10)
  • (removed) lldb/tools/lldb-dap/RunInTerminal.cpp (-170)
  • (removed) lldb/tools/lldb-dap/RunInTerminal.h (-131)
  • (modified) lldb/tools/lldb-dap/tool/lldb-dap.cpp (+43-44)
diff --git a/lldb/test/API/tools/lldb-dap/runInTerminal/TestDAP_runInTerminal.py b/lldb/test/API/tools/lldb-dap/runInTerminal/TestDAP_runInTerminal.py
index 65c931210d400..3b6571621e541 100644
--- a/lldb/test/API/tools/lldb-dap/runInTerminal/TestDAP_runInTerminal.py
+++ b/lldb/test/API/tools/lldb-dap/runInTerminal/TestDAP_runInTerminal.py
@@ -131,120 +131,3 @@ def test_runInTerminalInvalidTarget(self):
             "'INVALIDPROGRAM' does not exist",
             response["body"]["error"]["format"],
         )
-
-    @skipIfWindows
-    @skipIf(oslist=["linux"], archs=no_match(["x86_64"]))
-    def test_missingArgInRunInTerminalLauncher(self):
-        if not self.isTestSupported():
-            return
-        proc = subprocess.run(
-            [self.lldbDAPExec, "--launch-target", "INVALIDPROGRAM"],
-            capture_output=True,
-            universal_newlines=True,
-        )
-        self.assertNotEqual(proc.returncode, 0)
-        self.assertIn(
-            '"--launch-target" requires "--comm-file" to be specified', proc.stderr
-        )
-
-    @skipIfWindows
-    @skipIf(oslist=["linux"], archs=no_match(["x86_64"]))
-    def test_FakeAttachedRunInTerminalLauncherWithInvalidProgram(self):
-        if not self.isTestSupported():
-            return
-        comm_file = os.path.join(self.getBuildDir(), "comm-file")
-        os.mkfifo(comm_file)
-
-        proc = subprocess.Popen(
-            [
-                self.lldbDAPExec,
-                "--comm-file",
-                comm_file,
-                "--launch-target",
-                "INVALIDPROGRAM",
-            ],
-            universal_newlines=True,
-            stderr=subprocess.PIPE,
-        )
-
-        self.readPidMessage(comm_file)
-        self.sendDidAttachMessage(comm_file)
-        self.assertIn("No such file or directory", self.readErrorMessage(comm_file))
-
-        _, stderr = proc.communicate()
-        self.assertIn("No such file or directory", stderr)
-
-    @skipIfWindows
-    @skipIf(oslist=["linux"], archs=no_match(["x86_64"]))
-    def test_FakeAttachedRunInTerminalLauncherWithValidProgram(self):
-        if not self.isTestSupported():
-            return
-        comm_file = os.path.join(self.getBuildDir(), "comm-file")
-        os.mkfifo(comm_file)
-
-        proc = subprocess.Popen(
-            [
-                self.lldbDAPExec,
-                "--comm-file",
-                comm_file,
-                "--launch-target",
-                "echo",
-                "foo",
-            ],
-            universal_newlines=True,
-            stdout=subprocess.PIPE,
-        )
-
-        self.readPidMessage(comm_file)
-        self.sendDidAttachMessage(comm_file)
-
-        stdout, _ = proc.communicate()
-        self.assertIn("foo", stdout)
-
-    @skipIfWindows
-    @skipIf(oslist=["linux"], archs=no_match(["x86_64"]))
-    def test_FakeAttachedRunInTerminalLauncherAndCheckEnvironment(self):
-        if not self.isTestSupported():
-            return
-        comm_file = os.path.join(self.getBuildDir(), "comm-file")
-        os.mkfifo(comm_file)
-
-        proc = subprocess.Popen(
-            [self.lldbDAPExec, "--comm-file", comm_file, "--launch-target", "env"],
-            universal_newlines=True,
-            stdout=subprocess.PIPE,
-            env={**os.environ, "FOO": "BAR"},
-        )
-
-        self.readPidMessage(comm_file)
-        self.sendDidAttachMessage(comm_file)
-
-        stdout, _ = proc.communicate()
-        self.assertIn("FOO=BAR", stdout)
-
-    @skipIfWindows
-    @skipIf(oslist=["linux"], archs=no_match(["x86_64"]))
-    def test_NonAttachedRunInTerminalLauncher(self):
-        if not self.isTestSupported():
-            return
-        comm_file = os.path.join(self.getBuildDir(), "comm-file")
-        os.mkfifo(comm_file)
-
-        proc = subprocess.Popen(
-            [
-                self.lldbDAPExec,
-                "--comm-file",
-                comm_file,
-                "--launch-target",
-                "echo",
-                "foo",
-            ],
-            universal_newlines=True,
-            stderr=subprocess.PIPE,
-            env={**os.environ, "LLDB_DAP_RIT_TIMEOUT_IN_MS": "1000"},
-        )
-
-        self.readPidMessage(comm_file)
-
-        _, stderr = proc.communicate()
-        self.assertIn("Timed out trying to get messages from the debug adapter", stderr)
diff --git a/lldb/tools/lldb-dap/CMakeLists.txt b/lldb/tools/lldb-dap/CMakeLists.txt
index 40cba16c141f0..3e7e88afa9ced 100644
--- a/lldb/tools/lldb-dap/CMakeLists.txt
+++ b/lldb/tools/lldb-dap/CMakeLists.txt
@@ -14,7 +14,6 @@ add_lldb_library(lldbDAP
   DAPLog.cpp
   EventHelper.cpp
   ExceptionBreakpoint.cpp
-  FifoFiles.cpp
   FunctionBreakpoint.cpp
   InstructionBreakpoint.cpp
   JSONUtils.cpp
@@ -22,7 +21,6 @@ add_lldb_library(lldbDAP
   OutputRedirector.cpp
   ProgressEvent.cpp
   ProtocolUtils.cpp
-  RunInTerminal.cpp
   SourceBreakpoint.cpp
   Transport.cpp
   Variables.cpp
diff --git a/lldb/tools/lldb-dap/FifoFiles.cpp b/lldb/tools/lldb-dap/FifoFiles.cpp
deleted file mode 100644
index 1f1bba80bd3b1..0000000000000
--- a/lldb/tools/lldb-dap/FifoFiles.cpp
+++ /dev/null
@@ -1,101 +0,0 @@
-//===-- FifoFiles.cpp -------------------------------------------*- C++ -*-===//
-//
-// 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 "FifoFiles.h"
-#include "JSONUtils.h"
-
-#if !defined(_WIN32)
-#include <sys/stat.h>
-#include <sys/types.h>
-#include <unistd.h>
-#endif
-
-#include <chrono>
-#include <fstream>
-#include <future>
-#include <optional>
-
-using namespace llvm;
-
-namespace lldb_dap {
-
-FifoFile::FifoFile(StringRef path) : m_path(path) {}
-
-FifoFile::~FifoFile() {
-#if !defined(_WIN32)
-  unlink(m_path.c_str());
-#endif
-}
-
-Expected<std::shared_ptr<FifoFile>> CreateFifoFile(StringRef path) {
-#if defined(_WIN32)
-  return createStringError(inconvertibleErrorCode(), "Unimplemented");
-#else
-  if (int err = mkfifo(path.data(), 0600))
-    return createStringError(std::error_code(err, std::generic_category()),
-                             "Couldn't create fifo file: %s", path.data());
-  return std::make_shared<FifoFile>(path);
-#endif
-}
-
-FifoFileIO::FifoFileIO(StringRef fifo_file, StringRef other_endpoint_name)
-    : m_fifo_file(fifo_file), m_other_endpoint_name(other_endpoint_name) {}
-
-Expected<json::Value> FifoFileIO::ReadJSON(std::chrono::milliseconds timeout) {
-  // We use a pointer for this future, because otherwise its normal destructor
-  // would wait for the getline to end, rendering the timeout useless.
-  std::optional<std::string> line;
-  std::future<void> *future =
-      new std::future<void>(std::async(std::launch::async, [&]() {
-        std::ifstream reader(m_fifo_file, std::ifstream::in);
-        std::string buffer;
-        std::getline(reader, buffer);
-        if (!buffer.empty())
-          line = buffer;
-      }));
-  if (future->wait_for(timeout) == std::future_status::timeout || !line)
-    // Indeed this is a leak, but it's intentional. "future" obj destructor
-    //  will block on waiting for the worker thread to join. And the worker
-    //  thread might be stuck in blocking I/O. Intentionally leaking the  obj
-    //  as a hack to avoid blocking main thread, and adding annotation to
-    //  supress static code inspection warnings
-
-    // coverity[leaked_storage]
-    return createStringError(inconvertibleErrorCode(),
-                             "Timed out trying to get messages from the " +
-                                 m_other_endpoint_name);
-  delete future;
-  return json::parse(*line);
-}
-
-Error FifoFileIO::SendJSON(const json::Value &json,
-                           std::chrono::milliseconds timeout) {
-  bool done = false;
-  std::future<void> *future =
-      new std::future<void>(std::async(std::launch::async, [&]() {
-        std::ofstream writer(m_fifo_file, std::ofstream::out);
-        writer << JSONToString(json) << std::endl;
-        done = true;
-      }));
-  if (future->wait_for(timeout) == std::future_status::timeout || !done) {
-    // Indeed this is a leak, but it's intentional. "future" obj destructor will
-    // block on waiting for the worker thread to join. And the worker thread
-    // might be stuck in blocking I/O. Intentionally leaking the  obj as a hack
-    // to avoid blocking main thread, and adding annotation to supress static
-    // code inspection warnings"
-
-    // coverity[leaked_storage]
-    return createStringError(inconvertibleErrorCode(),
-                             "Timed out trying to send messages to the " +
-                                 m_other_endpoint_name);
-  }
-  delete future;
-  return Error::success();
-}
-
-} // namespace lldb_dap
diff --git a/lldb/tools/lldb-dap/FifoFiles.h b/lldb/tools/lldb-dap/FifoFiles.h
deleted file mode 100644
index 633ebeb2aedd4..0000000000000
--- a/lldb/tools/lldb-dap/FifoFiles.h
+++ /dev/null
@@ -1,85 +0,0 @@
-//===-- FifoFiles.h ---------------------------------------------*- C++ -*-===//
-//
-// 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
-//
-//===----------------------------------------------------------------------===//
-
-#ifndef LLDB_TOOLS_LLDB_DAP_FIFOFILES_H
-#define LLDB_TOOLS_LLDB_DAP_FIFOFILES_H
-
-#include "llvm/Support/Error.h"
-#include "llvm/Support/JSON.h"
-
-#include <chrono>
-
-namespace lldb_dap {
-
-/// Struct that controls the life of a fifo file in the filesystem.
-///
-/// The file is destroyed when the destructor is invoked.
-struct FifoFile {
-  FifoFile(llvm::StringRef path);
-
-  ~FifoFile();
-
-  std::string m_path;
-};
-
-/// Create a fifo file in the filesystem.
-///
-/// \param[in] path
-///     The path for the fifo file.
-///
-/// \return
-///     A \a std::shared_ptr<FifoFile> if the file could be created, or an
-///     \a llvm::Error in case of failures.
-llvm::Expected<std::shared_ptr<FifoFile>> CreateFifoFile(llvm::StringRef path);
-
-class FifoFileIO {
-public:
-  /// \param[in] fifo_file
-  ///     The path to an input fifo file that exists in the file system.
-  ///
-  /// \param[in] other_endpoint_name
-  ///     A human readable name for the other endpoint that will communicate
-  ///     using this file. This is used for error messages.
-  FifoFileIO(llvm::StringRef fifo_file, llvm::StringRef other_endpoint_name);
-
-  /// Read the next JSON object from the underlying input fifo file.
-  ///
-  /// The JSON object is expected to be a single line delimited with \a
-  /// std::endl.
-  ///
-  /// \return
-  ///     An \a llvm::Error object indicating the success or failure of this
-  ///     operation. Failures arise if the timeout is hit, the next line of text
-  ///     from the fifo file is not a valid JSON object, or is it impossible to
-  ///     read from the file.
-  llvm::Expected<llvm::json::Value> ReadJSON(std::chrono::milliseconds timeout);
-
-  /// Serialize a JSON object and write it to the underlying output fifo file.
-  ///
-  /// \param[in] json
-  ///     The JSON object to send. It will be printed as a single line delimited
-  ///     with \a std::endl.
-  ///
-  /// \param[in] timeout
-  ///     A timeout for how long we should until for the data to be consumed.
-  ///
-  /// \return
-  ///     An \a llvm::Error object indicating whether the data was consumed by
-  ///     a reader or not.
-  llvm::Error SendJSON(
-      const llvm::json::Value &json,
-      std::chrono::milliseconds timeout = std::chrono::milliseconds(20000));
-
-private:
-  std::string m_fifo_file;
-  std::string m_other_endpoint_name;
-};
-
-} // namespace lldb_dap
-
-#endif // LLDB_TOOLS_LLDB_DAP_FIFOFILES_H
diff --git a/lldb/tools/lldb-dap/Handler/RequestHandler.cpp b/lldb/tools/lldb-dap/Handler/RequestHandler.cpp
index 93bc80a38e29d..cfcbabbb16a93 100644
--- a/lldb/tools/lldb-dap/Handler/RequestHandler.cpp
+++ b/lldb/tools/lldb-dap/Handler/RequestHandler.cpp
@@ -14,10 +14,12 @@
 #include "LLDBUtils.h"
 #include "Protocol/ProtocolBase.h"
 #include "Protocol/ProtocolRequests.h"
-#include "RunInTerminal.h"
 #include "lldb/API/SBDefines.h"
 #include "lldb/API/SBEnvironment.h"
 #include "llvm/Support/Error.h"
+#include <cerrno>
+#include <csignal>
+#include <limits>
 #include <mutex>
 
 #if !defined(_WIN32)
@@ -51,6 +53,70 @@ static uint32_t SetLaunchFlag(uint32_t flags, bool flag,
   return flags;
 }
 
+// Assume single thread
+class PidReceiver {
+private:
+  inline static volatile std::sig_atomic_t pid;
+  sigset_t oldmask;
+  struct sigaction oldaction;
+
+  static_assert(std::numeric_limits<volatile sig_atomic_t>::max() >=
+                    std::numeric_limits<pid_t>::max(),
+                "sig_atomic_t must be able to hold a pid_t value");
+
+  static void sig_handler(int, siginfo_t *info, void *) {
+    // Store the PID from the signal info
+    pid = info->si_pid;
+  }
+
+  PidReceiver(const PidReceiver &) = delete;
+  PidReceiver &operator=(const PidReceiver &) = delete;
+  PidReceiver(PidReceiver &&) = delete;
+
+public:
+  PidReceiver() {
+    pid = LLDB_INVALID_PROCESS_ID;
+    // sigprocmask and sigaction can only fail by programmer error
+    // Defer SIGUSR1, otherwise we might receive it out of pselect and hang
+    sigset_t mask;
+    sigemptyset(&mask);
+    sigaddset(&mask, SIGUSR1);
+    assert(!sigprocmask(SIG_BLOCK, &mask, &oldmask));
+
+    // Set up sigaction for SIGUSR1 with SA_SIGINFO
+    struct sigaction sa;
+    std::memset(&sa, 0, sizeof(sa));
+    sa.sa_sigaction = sig_handler;
+    sa.sa_flags = SA_SIGINFO;
+    sigemptyset(&sa.sa_mask);
+    assert(!sigaction(SIGUSR1, &sa, &oldaction));
+  }
+
+  ~PidReceiver() {
+    // Restore the old signal mask
+    assert(!sigprocmask(SIG_SETMASK, &oldmask, nullptr));
+    assert(!sigaction(SIGUSR1, &oldaction, nullptr));
+  }
+
+  llvm::Expected<lldb::pid_t> WaitForPid() {
+    // Wait for the signal to be received
+    while (pid == LLDB_INVALID_PROCESS_ID) {
+      struct timespec timeout;
+      timeout.tv_sec = 5;
+      timeout.tv_nsec = 0;
+      auto ret = pselect(0, nullptr, nullptr, nullptr, &timeout, &oldmask);
+      if (ret == -1 && errno != EINTR) {
+        return llvm::createStringError(
+            std::error_code(errno, std::generic_category()), "pselect failed");
+      } else if (ret == 0) {
+        return llvm::createStringError(llvm::inconvertibleErrorCode(),
+                                       "Timed out waiting for signal");
+      }
+    }
+    return pid;
+  }
+};
+
 static llvm::Error
 RunInTerminal(DAP &dap, const protocol::LaunchRequestArguments &arguments) {
   if (!dap.clientFeatures.contains(
@@ -65,26 +131,19 @@ RunInTerminal(DAP &dap, const protocol::LaunchRequestArguments &arguments) {
   dap.is_attach = true;
   lldb::SBAttachInfo attach_info;
 
-  llvm::Expected<std::shared_ptr<FifoFile>> comm_file_or_err =
-      CreateRunInTerminalCommFile();
-  if (!comm_file_or_err)
-    return comm_file_or_err.takeError();
-  FifoFile &comm_file = *comm_file_or_err.get();
-
-  RunInTerminalDebugAdapterCommChannel comm_channel(comm_file.m_path);
-
   lldb::pid_t debugger_pid = LLDB_INVALID_PROCESS_ID;
 #if !defined(_WIN32)
   debugger_pid = getpid();
 #endif
 
+  auto pid_receiver = PidReceiver();
   llvm::json::Object reverse_request = CreateRunInTerminalReverseRequest(
       arguments.configuration.program, arguments.args, arguments.env,
-      arguments.cwd, comm_file.m_path, debugger_pid);
+      arguments.cwd, debugger_pid);
   dap.SendReverseRequest<LogFailureResponseHandler>("runInTerminal",
                                                     std::move(reverse_request));
 
-  if (llvm::Expected<lldb::pid_t> pid = comm_channel.GetLauncherPid())
+  if (llvm::Expected<lldb::pid_t> pid = pid_receiver.WaitForPid())
     attach_info.SetProcessID(*pid);
   else
     return pid.takeError();
@@ -95,13 +154,7 @@ RunInTerminal(DAP &dap, const protocol::LaunchRequestArguments &arguments) {
 
   if (error.Fail())
     return llvm::createStringError(llvm::inconvertibleErrorCode(),
-                                   "Failed to attach to the target process. %s",
-                                   comm_channel.GetLauncherError().c_str());
-  // This will notify the runInTerminal launcher that we attached.
-  // We have to make this async, as the function won't return until the launcher
-  // resumes and reads the data.
-  std::future<lldb::SBError> did_attach_message_success =
-      comm_channel.NotifyDidAttach();
+                                   "Failed to attach to the target process.");
 
   // We just attached to the runInTerminal launcher, which was waiting to be
   // attached. We now resume it, so it can receive the didAttach notification
@@ -115,15 +168,7 @@ RunInTerminal(DAP &dap, const protocol::LaunchRequestArguments &arguments) {
   // we return the debugger to its sync state.
   scope_sync_mode.reset();
 
-  // If sending the notification failed, the launcher should be dead by now and
-  // the async didAttach notification should have an error message, so we
-  // return it. Otherwise, everything was a success.
-  did_attach_message_success.wait();
-  error = did_attach_message_success.get();
-  if (error.Success())
-    return llvm::Error::success();
-  return llvm::createStringError(llvm::inconvertibleErrorCode(),
-                                 error.GetCString());
+  return llvm::Error::success();
 }
 
 void BaseRequestHandler::Run(const Request &request) {
diff --git a/lldb/tools/lldb-dap/JSONUtils.cpp b/lldb/tools/lldb-dap/JSONUtils.cpp
index 573f3eba00f62..bf10742e18bb8 100644
--- a/lldb/tools/lldb-dap/JSONUtils.cpp
+++ b/lldb/tools/lldb-dap/JSONUtils.cpp
@@ -1238,15 +1238,14 @@ llvm::json::Value CreateCompileUnit(lldb::SBCompileUnit &unit) {
 llvm::json::Object CreateRunInTerminalReverseRequest(
     llvm::StringRef program, const std::vector<std::string> &args,
     const llvm::StringMap<std::string> &env, llvm::StringRef cwd,
-    llvm::StringRef comm_file, lldb::pid_t debugger_pid) {
+    lldb::pid_t debugger_pid) {
   llvm::json::Object run_in_terminal_args;
   // This indicates the IDE to open an embedded terminal, instead of opening
   // the terminal in a new window.
   run_in_terminal_args.try_emplace("kind", "integrated");
 
   // The program path must be the first entry in the "args" field
-  std::vector<std::string> req_args = {DAP::debug_adapter_path.str(),
-                                       "--comm-file", comm_file.str()};
+  std::vector<std::string> req_args = {DAP::debug_adapter_path.str()};
   if (debugger_pid != LLDB_INVALID_PROCESS_ID) {
     req_args.push_back("--debugger-pid");
     req_args.push_back(std::to_string(debugger_pid));
diff --git a/lldb/tools/lldb-dap/JSONUtils.h b/lldb/tools/lldb-dap/JSONUtils.h
index 08699a94bbd87..4ed66e6992be2 100644
--- a/lldb/tools/lldb-dap/JSONUtils.h
+++ b/lldb/tools/lldb-dap/JSONUtils.h
@@ -466,9 +466,6 @@ llvm::json::Value CreateCompileUnit(lldb::SBCompileUnit &unit);
 /// \param[in] cwd
 ///     The working directory for the run in terminal request.
 ///
-/// \param[in] comm_file
-///     The fifo file used to communicate the with the target launcher.
-///
 /// \param[in] debugger_pid
 ///     The PID of the lldb-dap instance that will attach to the target. The
 ///     launcher uses it on Linux tell the kernel that it should allow the
@@ -480,7 +477,7 @@ llvm::json::Value CreateCompileUnit(lldb::SBCompileUnit &unit);
 llvm::json::Object CreateRunInTerminalReverseRequest(
     llvm::StringRef program, const std::vector<std::string> &args,
     const llvm::StringMap<std::string> &env, llvm::StringRef cwd,
-    llvm::StringRef comm_file, lldb::pid_t debugger_pid);
+    lldb::pid_t debugger_pid);
 
 /// Create a "Terminated" JSON object that contains statistics
 ///
diff --git a/lldb/tools/lldb-dap/Options.td b/lldb/tools/lldb-dap/Options.td
index aecf91797ac70..669d6a7c0df05 100644
--- a/lldb/tools/lldb-dap/Options.td
+++ b/lldb/tools/lldb-dap/Options.td
@@ -31,16 +31,11 @@ def connection
           "Connections are specified like 'tcp://[host]:port' or "
           "'unix:///path'.">;
 
-def launch_target: S<"laun...
[truncated]

Copy link

github-actions bot commented Jun 2, 2025

✅ With the latest revision this PR passed the C/C++ code formatter.

@SuibianP SuibianP force-pushed the lldb-dap-runinterminal-signal branch 3 times, most recently from b71032a to 7555bcf Compare June 3, 2025 02:19
@JDevlieghere JDevlieghere requested review from ashgti and labath June 3, 2025 22:21
runInTerminal is currently implemented using JSON messages over FIFOs,
which feels too clunky for the simple job of passing a PID.

Instead, take advantage of si_pid available from sa_sigaction handler,
and send a user signal instead. Both synchronisation and timeout are
preserved with the protocol below,

1. Debugger waits for SIGUSR1 under timeout with pselect
2. Launcher forks into child, sends SIGUSR1 to debugger and suspends
3. Debugger receives SIGUSR1, captures the sender (launcher child) PID,
   attaches to and resumes it
4. Launcher child resumes and exec's into target
5. Launcher parent waitpid's and kills irresponsive child after timeout

With this, the entirety of FifoFiles and RunInTerminal can be dropped.

Refs: llvm#121269 (comment)
@SuibianP SuibianP force-pushed the lldb-dap-runinterminal-signal branch from 7555bcf to 098fb7f Compare June 6, 2025 11:19
@labath
Copy link
Collaborator

labath commented Jun 10, 2025

Sorry about the delay. I have very mixed feelings about this PR. I too found the existing method of synchronization extremely clunky, so I definitely wouldn't be sad to see it go. However, I also have some reservations about this implementation:

  • you say you're assuming a single thread, but is that actually true. IIUC the signal receiving code runs inside the main lldb-dap process, which creates threads left and right. Your method of receiving a signal may not work reliably in a multithreaded process. We already have the MainLoop class, which knows how to wait for a signal in the presence of multiple threads. Unfortunately, it doesn't actually preserve the information about the sender. That should be relatively easy to fix though -- just have the signal handler save off the siginfo_t and then pass it to the callback. I would like to be able to reuse/extend that instead of building a new signal catching primitive.
  • in the launcher process, I see that you're doing a fork -- and then exiting from the parent. This sounds like it could confuse some terminals, which may close the window once the process they launched exits. I think you're doing that because you want to be sure the child exits, but I don't think that's really necessary -- one can imagine synchronization protocols which don't require waiting in a third process. You just need to avoid SIGSTOPping the child process, which is another potential source of problems here.
  • if the debugger attaches to the child process before it executes raise(SIGSTOP), the debugger will see one more stop by the child process than it might expect. Are you sure that's handled correctly here?

At the end of the day, I'm not really sure that signals are the best solution to this problem, as they interact strongly with the debugging APIs. The existing implementation was very.. baroque, but I think the choice of unix sockets was actually quite reasonable. With that in mind, let me propose an alternative protocol using (named) pipes:

  • child gets the socket name, writes the pid to it, and waits for EOF, if EOF doesn't come, it exits
  • parent listens on the socket, reads the pid, attaches to the process and then closes it

This doesn't require any signals, nor iostreams, and is mostly portable (we have a named pipe implementation for windows, and I believe we already use it in some situations).

@SuibianP
Copy link
Contributor Author

Thanks for the response!

Your method of receiving a signal may not work reliably in a multithreaded process.

It will not. Signal mask is thread local and the signal may be delivered to other threads instead of being deferred until pselect. sigtimedwait avoids the pitfall but is not supported on macOS and OpenBSD. Good to know there are existing parts to reuse.

I think you're doing that because you want to be sure the child exits, but I don't think that's really necessary

It turned out that that does not even work reliably. Seems that on BSD and macOS, ptrace completely reparents the process and the real parent cannot waitpid any more before tracing ends.

which may close the window once the process they launched exits

Noted. This also means that on Windows (with no real exec) the launcher will have to stay around until the target exits.

Are you sure that's handled correctly here?

Not yet. I have been trying things like retrying if not eStopReasonExec , but on macOS somehow the SBProcess had no thread on the second stop.

let me propose an alternative protocol using (named) pipes
we have a named pipe implementation for windows, and I believe we already use it in some situations

The current implementation is using named pipes, which I tried to extend to Windows in #121269. The named pipe semantics, however, was rather different between Windows and POSIX. #121269 (comment)


I will see if the suggested socket abstraction has fewer lurking surprises across platforms. Failing that, I will try out SysV message queues.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants