https://github.com/DrSergei created https://github.com/llvm/llvm-project/pull/138160
Added `runInTerminal` support for Windows based on Windows Named Pipes. Adapted existed `FifoFile` class to Windows client-server pipes model. When server side owns the assosieted filesystem handle and client side only provide read-write acces to it. Also, fixed small typo in `JSONUtill.cpp` related to `runInTerminal` functionality. >From c60a556561b70f8eab0781e5de9374aac87529d4 Mon Sep 17 00:00:00 2001 From: Druzhkov Sergei <serzhdruz...@gmail.com> Date: Thu, 1 May 2025 18:46:22 +0300 Subject: [PATCH] [lldb-dap] Add runInTerminal support for Windows --- .../runInTerminal/TestDAP_runInTerminal.py | 3 - lldb/tools/lldb-dap/FifoFiles.cpp | 85 +++++++++++++++++-- lldb/tools/lldb-dap/FifoFiles.h | 36 ++++++-- .../tools/lldb-dap/Handler/RequestHandler.cpp | 12 ++- lldb/tools/lldb-dap/JSONUtils.cpp | 2 +- lldb/tools/lldb-dap/RunInTerminal.cpp | 34 ++++++-- lldb/tools/lldb-dap/RunInTerminal.h | 29 ++++++- lldb/tools/lldb-dap/lldb-dap.cpp | 72 ++++++++++++++-- 8 files changed, 237 insertions(+), 36 deletions(-) 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 9aab7ca3293db..d5355f3bacd9c 100644 --- a/lldb/test/API/tools/lldb-dap/runInTerminal/TestDAP_runInTerminal.py +++ b/lldb/test/API/tools/lldb-dap/runInTerminal/TestDAP_runInTerminal.py @@ -43,7 +43,6 @@ def isTestSupported(self): except: return False - @skipIfWindows @skipIf(oslist=["linux"], archs=no_match(["x86_64"])) def test_runInTerminal(self): if not self.isTestSupported(): @@ -113,7 +112,6 @@ def test_runInTerminalWithObjectEnv(self): self.assertIn("FOO", request_envs) self.assertEqual("BAR", request_envs["FOO"]) - @skipIfWindows @skipIf(oslist=["linux"], archs=no_match(["x86_64"])) def test_runInTerminalInvalidTarget(self): if not self.isTestSupported(): @@ -132,7 +130,6 @@ def test_runInTerminalInvalidTarget(self): response["message"], ) - @skipIfWindows @skipIf(oslist=["linux"], archs=no_match(["x86_64"])) def test_missingArgInRunInTerminalLauncher(self): if not self.isTestSupported(): diff --git a/lldb/tools/lldb-dap/FifoFiles.cpp b/lldb/tools/lldb-dap/FifoFiles.cpp index 1f1bba80bd3b1..43eb4679b592f 100644 --- a/lldb/tools/lldb-dap/FifoFiles.cpp +++ b/lldb/tools/lldb-dap/FifoFiles.cpp @@ -24,26 +24,53 @@ using namespace llvm; namespace lldb_dap { -FifoFile::FifoFile(StringRef path) : m_path(path) {} +#if defined(_WIN32) +FifoFile::FifoFile(StringRef path, HANDLE handle, bool is_server) + : m_path(path), m_is_server(is_server), m_pipe_fd(handle) {} +#else +FifoFile::FifoFile(StringRef path, bool is_server) + : m_path(path), m_is_server(is_server) {} +#endif FifoFile::~FifoFile() { -#if !defined(_WIN32) - unlink(m_path.c_str()); +#if defined(_WIN32) + if (m_pipe_fd == INVALID_HANDLE_VALUE) + return; + if (m_is_server) + DisconnectNamedPipe(m_pipe_fd); + CloseHandle(m_pipe_fd); +#else + if (m_is_server) + unlink(m_path.c_str()); #endif } -Expected<std::shared_ptr<FifoFile>> CreateFifoFile(StringRef path) { +Expected<std::shared_ptr<FifoFile>> CreateFifoFile(StringRef path, + bool is_server) { #if defined(_WIN32) - return createStringError(inconvertibleErrorCode(), "Unimplemented"); + if (!is_server) + return std::make_shared<FifoFile>(path, INVALID_HANDLE_VALUE, is_server); + HANDLE handle = + CreateNamedPipeA(path.data(), PIPE_ACCESS_DUPLEX, + PIPE_TYPE_BYTE | PIPE_READMODE_BYTE | PIPE_WAIT, 1, + 1024 * 16, 1024 * 16, 0, NULL); + if (handle == INVALID_HANDLE_VALUE) + return createStringError( + std::error_code(GetLastError(), std::generic_category()), + "Couldn't create fifo file: %s", path.data()); + return std::make_shared<FifoFile>(path, handle, is_server); #else + if (!is_server) + return std::make_shared<FifoFile>(path, is_server); 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); + return std::make_shared<FifoFile>(path, is_server); #endif } -FifoFileIO::FifoFileIO(StringRef fifo_file, StringRef other_endpoint_name) +FifoFileIO::FifoFileIO(std::shared_ptr<FifoFile> 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) { @@ -52,11 +79,27 @@ Expected<json::Value> FifoFileIO::ReadJSON(std::chrono::milliseconds timeout) { 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); +#if defined(_WIN32) + std::string buffer; + buffer.reserve(4096); + char ch; + DWORD bytes_read = 0; + while (ReadFile(m_fifo_file->m_pipe_fd, &ch, 1, &bytes_read, NULL) && + (bytes_read == 1)) { + buffer.push_back(ch); + if (ch == '\n') { + break; + } + } + if (!buffer.empty()) + line = std::move(buffer); +#else + std::ifstream reader(m_fifo_file->m_path, std::ifstream::in); std::string buffer; std::getline(reader, buffer); if (!buffer.empty()) line = buffer; +#endif })); if (future->wait_for(timeout) == std::future_status::timeout || !line) // Indeed this is a leak, but it's intentional. "future" obj destructor @@ -78,9 +121,19 @@ Error FifoFileIO::SendJSON(const json::Value &json, bool done = false; std::future<void> *future = new std::future<void>(std::async(std::launch::async, [&]() { +#if defined(_WIN32) + std::string buffer = JSONToString(json); + buffer.append("\n"); + DWORD bytes_write = 0; + WriteFile(m_fifo_file->m_pipe_fd, buffer.c_str(), buffer.size(), + &bytes_write, NULL); + done = bytes_write == buffer.size(); +#else + std::ofstream writer(m_fifo_file->m_path, std::ofstream::out); std::ofstream writer(m_fifo_file, std::ofstream::out); writer << JSONToString(json) << std::endl; done = true; +#endif })); if (future->wait_for(timeout) == std::future_status::timeout || !done) { // Indeed this is a leak, but it's intentional. "future" obj destructor will @@ -98,4 +151,20 @@ Error FifoFileIO::SendJSON(const json::Value &json, return Error::success(); } +#if defined(_WIN32) +bool FifoFileIO::Connect() { + if (m_fifo_file->m_is_server) { + return ConnectNamedPipe(m_fifo_file->m_pipe_fd, NULL); + } + if (!WaitNamedPipeA(m_fifo_file->m_path.c_str(), NMPWAIT_WAIT_FOREVER)) + return false; + m_fifo_file->m_pipe_fd = + CreateFileA(m_fifo_file->m_path.c_str(), GENERIC_READ | GENERIC_WRITE, 0, + NULL, OPEN_EXISTING, 0, NULL); + if (m_fifo_file->m_pipe_fd == INVALID_HANDLE_VALUE) + return false; + return true; +} +#endif + } // namespace lldb_dap diff --git a/lldb/tools/lldb-dap/FifoFiles.h b/lldb/tools/lldb-dap/FifoFiles.h index 633ebeb2aedd4..335c7561040dd 100644 --- a/lldb/tools/lldb-dap/FifoFiles.h +++ b/lldb/tools/lldb-dap/FifoFiles.h @@ -12,19 +12,35 @@ #include "llvm/Support/Error.h" #include "llvm/Support/JSON.h" +#if defined(_WIN32) +#include "lldb/Host/windows/windows.h" +#endif + #include <chrono> namespace lldb_dap { +class FifoFileIO; + /// 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); +#if defined(_WIN32) + FifoFile(llvm::StringRef path, HANDLE handle, bool is_server); +#else + FifoFile(llvm::StringRef path, bool is_server); +#endif ~FifoFile(); std::string m_path; + bool m_is_server; +#if defined(_WIN32) + HANDLE m_pipe_fd = INVALID_HANDLE_VALUE; +#endif + + friend FifoFileIO; }; /// Create a fifo file in the filesystem. @@ -32,20 +48,26 @@ struct FifoFile { /// \param[in] path /// The path for the fifo file. /// +/// \param[in] is_server +/// If \a is_server is true, then created instance of FifoFile will own +/// created 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); +llvm::Expected<std::shared_ptr<FifoFile>> CreateFifoFile(llvm::StringRef path, + bool is_server); class FifoFileIO { public: /// \param[in] fifo_file - /// The path to an input fifo file that exists in the file system. + /// The std::shared_ptr<FifoFile> to an existing fifo file. /// /// \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); + FifoFileIO(std::shared_ptr<FifoFile> fifo_file, + llvm::StringRef other_endpoint_name); /// Read the next JSON object from the underlying input fifo file. /// @@ -75,8 +97,12 @@ class FifoFileIO { const llvm::json::Value &json, std::chrono::milliseconds timeout = std::chrono::milliseconds(20000)); +#if defined(_WIN32) + bool Connect(); +#endif + private: - std::string m_fifo_file; + std::shared_ptr<FifoFile> m_fifo_file; std::string m_other_endpoint_name; }; diff --git a/lldb/tools/lldb-dap/Handler/RequestHandler.cpp b/lldb/tools/lldb-dap/Handler/RequestHandler.cpp index 7a75cd93abc19..e68597194c296 100644 --- a/lldb/tools/lldb-dap/Handler/RequestHandler.cpp +++ b/lldb/tools/lldb-dap/Handler/RequestHandler.cpp @@ -119,9 +119,9 @@ RunInTerminal(DAP &dap, const protocol::LaunchRequestArguments &arguments) { CreateRunInTerminalCommFile(); if (!comm_file_or_err) return comm_file_or_err.takeError(); - FifoFile &comm_file = *comm_file_or_err.get(); + std::shared_ptr<FifoFile> comm_file = *comm_file_or_err; - RunInTerminalDebugAdapterCommChannel comm_channel(comm_file.m_path); + RunInTerminalDebugAdapterCommChannel comm_channel(comm_file); lldb::pid_t debugger_pid = LLDB_INVALID_PROCESS_ID; #if !defined(_WIN32) @@ -130,10 +130,16 @@ RunInTerminal(DAP &dap, const protocol::LaunchRequestArguments &arguments) { llvm::json::Object reverse_request = CreateRunInTerminalReverseRequest( *arguments.configuration.program, arguments.args, arguments.env, - arguments.cwd.value_or(""), comm_file.m_path, debugger_pid); + arguments.cwd.value_or(""), comm_file->m_path, debugger_pid); dap.SendReverseRequest<LogFailureResponseHandler>("runInTerminal", std::move(reverse_request)); +#if defined(_WIN32) + if (!comm_channel.Connect()) + return llvm::createStringError(llvm::inconvertibleErrorCode(), + "Failed to connect to the named pipe."); +#endif + if (llvm::Expected<lldb::pid_t> pid = comm_channel.GetLauncherPid()) attach_info.SetProcessID(*pid); else diff --git a/lldb/tools/lldb-dap/JSONUtils.cpp b/lldb/tools/lldb-dap/JSONUtils.cpp index 4409cf5b27e5b..f29ab09e4feb9 100644 --- a/lldb/tools/lldb-dap/JSONUtils.cpp +++ b/lldb/tools/lldb-dap/JSONUtils.cpp @@ -1463,7 +1463,7 @@ llvm::json::Object CreateRunInTerminalReverseRequest( req_args.push_back("--launch-target"); req_args.push_back(program.str()); req_args.insert(req_args.end(), args.begin(), args.end()); - run_in_terminal_args.try_emplace("args", args); + run_in_terminal_args.try_emplace("args", req_args); if (!cwd.empty()) run_in_terminal_args.try_emplace("cwd", cwd); diff --git a/lldb/tools/lldb-dap/RunInTerminal.cpp b/lldb/tools/lldb-dap/RunInTerminal.cpp index 9f309dd78221a..8afe6e8907710 100644 --- a/lldb/tools/lldb-dap/RunInTerminal.cpp +++ b/lldb/tools/lldb-dap/RunInTerminal.cpp @@ -9,7 +9,9 @@ #include "RunInTerminal.h" #include "JSONUtils.h" -#if !defined(_WIN32) +#if defined(_WIN32) +#include "lldb/Host/windows/windows.h" +#else #include <sys/stat.h> #include <sys/types.h> #include <unistd.h> @@ -96,7 +98,7 @@ static Error ToError(const RunInTerminalMessage &message) { } RunInTerminalLauncherCommChannel::RunInTerminalLauncherCommChannel( - StringRef comm_file) + std::shared_ptr<FifoFile> comm_file) : m_io(comm_file, "debug adapter") {} Error RunInTerminalLauncherCommChannel::WaitUntilDebugAdapterAttaches( @@ -111,8 +113,8 @@ Error RunInTerminalLauncherCommChannel::WaitUntilDebugAdapterAttaches( return message.takeError(); } -Error RunInTerminalLauncherCommChannel::NotifyPid() { - return m_io.SendJSON(RunInTerminalMessagePid(getpid()).ToJSON()); +Error RunInTerminalLauncherCommChannel::NotifyPid(lldb::pid_t pid) { + return m_io.SendJSON(RunInTerminalMessagePid(pid).ToJSON()); } void RunInTerminalLauncherCommChannel::NotifyError(StringRef error) { @@ -121,8 +123,12 @@ void RunInTerminalLauncherCommChannel::NotifyError(StringRef error) { llvm::errs() << llvm::toString(std::move(err)) << "\n"; } +#if defined(_WIN32) +bool RunInTerminalLauncherCommChannel::Connect() { return m_io.Connect(); } +#endif + RunInTerminalDebugAdapterCommChannel::RunInTerminalDebugAdapterCommChannel( - StringRef comm_file) + std::shared_ptr<FifoFile> comm_file) : m_io(comm_file, "runInTerminal launcher") {} // Can't use \a std::future<llvm::Error> because it doesn't compile on Windows @@ -148,6 +154,10 @@ Expected<lldb::pid_t> RunInTerminalDebugAdapterCommChannel::GetLauncherPid() { } } +#if defined(_WIN32) +bool RunInTerminalDebugAdapterCommChannel::Connect() { return m_io.Connect(); } +#endif + std::string RunInTerminalDebugAdapterCommChannel::GetLauncherError() { // We know there's been an error, so a small timeout is enough. if (Expected<RunInTerminalMessageUP> message = @@ -158,13 +168,25 @@ std::string RunInTerminalDebugAdapterCommChannel::GetLauncherError() { } Expected<std::shared_ptr<FifoFile>> CreateRunInTerminalCommFile() { +#if defined(_WIN32) + static constexpr llvm::StringLiteral g_pipe_name_prefix = "\\\\.\\Pipe\\"; + SmallString<256> comm_file; + sys::fs::createUniquePath("lldb-dap-run-in-terminal-comm-%%%%%%", comm_file, + +false); + return CreateFifoFile((g_pipe_name_prefix + comm_file.str()).str(), true); +#else SmallString<256> comm_file; if (std::error_code EC = sys::fs::getPotentiallyUniqueTempFileName( "lldb-dap-run-in-terminal-comm", "", comm_file)) return createStringError(EC, "Error making unique file name for " "runInTerminal communication files"); + return CreateFifoFile(comm_file.str(), true); +#endif +} - return CreateFifoFile(comm_file.str()); +Expected<std::shared_ptr<FifoFile>> +OpenRunInTerminalCommFile(llvm::StringRef fifo_file) { + return CreateFifoFile(fifo_file, false); } } // namespace lldb_dap diff --git a/lldb/tools/lldb-dap/RunInTerminal.h b/lldb/tools/lldb-dap/RunInTerminal.h index 457850c8ea538..10e715d1d12e1 100644 --- a/lldb/tools/lldb-dap/RunInTerminal.h +++ b/lldb/tools/lldb-dap/RunInTerminal.h @@ -70,7 +70,7 @@ struct RunInTerminalMessageDidAttach : RunInTerminalMessage { class RunInTerminalLauncherCommChannel { public: - RunInTerminalLauncherCommChannel(llvm::StringRef comm_file); + RunInTerminalLauncherCommChannel(std::shared_ptr<FifoFile> comm_file); /// Wait until the debug adapter attaches. /// @@ -82,23 +82,31 @@ class RunInTerminalLauncherCommChannel { /// out. llvm::Error WaitUntilDebugAdapterAttaches(std::chrono::milliseconds timeout); - /// Notify the debug adapter this process' pid. + /// Notify the debug adaptor debuggee's pid. + /// + /// \param[in] pid + /// The process ID to be attached. /// /// \return /// An \a llvm::Error object in case of errors or if this operation times /// out. - llvm::Error NotifyPid(); + llvm::Error NotifyPid(lldb::pid_t pid); /// Notify the debug adapter that there's been an error. void NotifyError(llvm::StringRef error); +#if defined(_WIN32) + /// Connect to RunInTerminalDebugAdapterCommChannel instance. + bool Connect(); +#endif + private: FifoFileIO m_io; }; class RunInTerminalDebugAdapterCommChannel { public: - RunInTerminalDebugAdapterCommChannel(llvm::StringRef comm_file); + RunInTerminalDebugAdapterCommChannel(std::shared_ptr<FifoFile> comm_file); /// Notify the runInTerminal launcher that it was attached. /// @@ -118,6 +126,11 @@ class RunInTerminalDebugAdapterCommChannel { /// default error message if a certain timeout if reached. std::string GetLauncherError(); +#if defined(_WIN32) + /// Connect to RunInTerminalLauncherCommChannel instance. + bool Connect(); +#endif + private: FifoFileIO m_io; }; @@ -126,6 +139,14 @@ class RunInTerminalDebugAdapterCommChannel { /// the runInTerminal launcher. llvm::Expected<std::shared_ptr<FifoFile>> CreateRunInTerminalCommFile(); +/// Open a fifo file used to communicate the debug adaptor with +/// the runInTerminal launcher. +/// +/// \param[in] fifo_file +/// The path to a fifo file that exists in the file system. +llvm::Expected<std::shared_ptr<FifoFile>> +OpenRunInTerminalCommFile(llvm::StringRef fifo_file); + } // namespace lldb_dap #endif // LLDB_TOOLS_LLDB_DAP_RUNINTERMINAL_H diff --git a/lldb/tools/lldb-dap/lldb-dap.cpp b/lldb/tools/lldb-dap/lldb-dap.cpp index 6e17b13cc9e33..13fdd0e57ad1a 100644 --- a/lldb/tools/lldb-dap/lldb-dap.cpp +++ b/lldb/tools/lldb-dap/lldb-dap.cpp @@ -44,9 +44,11 @@ #include <cstdio> #include <cstdlib> #include <fcntl.h> +#include <iomanip> #include <map> #include <memory> #include <mutex> +#include <sstream> #include <string> #include <system_error> #include <thread> @@ -204,14 +206,73 @@ static void PrintVersion() { // In case of errors launching the target, a suitable error message will be // emitted to the debug adapter. static llvm::Error LaunchRunInTerminalTarget(llvm::opt::Arg &target_arg, - llvm::StringRef comm_file, + llvm::StringRef fifo_file, lldb::pid_t debugger_pid, char *argv[]) { + llvm::Expected<std::shared_ptr<FifoFile>> comm_file_or_err = + OpenRunInTerminalCommFile(fifo_file); + if (!comm_file_or_err) { + llvm::errs() << llvm::toString(comm_file_or_err.takeError()) << "\n"; + exit(EXIT_FAILURE); + } + std::shared_ptr<FifoFile> comm_file = *comm_file_or_err; + RunInTerminalLauncherCommChannel comm_channel(comm_file); + #if defined(_WIN32) - return llvm::createStringError( - "runInTerminal is only supported on POSIX systems"); -#else + if (!comm_channel.Connect()) { + llvm::errs() << "Failed to connect to the named pipe.\n"; + exit(EXIT_FAILURE); + } + + STARTUPINFOA si; + PROCESS_INFORMATION pi; + ZeroMemory(&si, sizeof(si)); + si.cb = sizeof(si); + ZeroMemory(&pi, sizeof(pi)); + + std::string cmd; + while (*argv != nullptr) { + std::stringstream ss; + ss << std::quoted(*(argv++)); + cmd += ss.str(); + cmd += " "; + } + cmd.pop_back(); + + if (!CreateProcessA(NULL, cmd.data(), NULL, NULL, FALSE, CREATE_SUSPENDED, + NULL, NULL, &si, &pi)) { + llvm::errs() << "Create process failed: " << GetLastError() << "\n"; + exit(EXIT_FAILURE); + } + if (llvm::Error err = comm_channel.NotifyPid(pi.dwProcessId)) { + llvm::errs() << llvm::toString(std::move(err)) << "\n"; + exit(EXIT_FAILURE); + } + + // We will wait to be attached with a timeout. We don't wait indefinitely + // using a signal to prevent being paused forever. + + // This env var should be used only for tests. + const char *timeout_env_var = getenv("LLDB_DAP_RIT_TIMEOUT_IN_MS"); + int timeout_in_ms = + timeout_env_var != nullptr ? atoi(timeout_env_var) : 20000; + if (llvm::Error err = comm_channel.WaitUntilDebugAdapterAttaches( + std::chrono::milliseconds(timeout_in_ms))) { + llvm::errs() << llvm::toString(std::move(err)) << "\n"; + exit(EXIT_FAILURE); + } + + if (ResumeThread(pi.hThread) == (DWORD)-1) { + llvm::errs() << "Resume process failed: " << GetLastError() << "\n"; + exit(EXIT_FAILURE); + } + + WaitForSingleObject(pi.hProcess, INFINITE); + CloseHandle(pi.hProcess); + CloseHandle(pi.hThread); + exit(EXIT_SUCCESS); +#else // On Linux with the Yama security module enabled, a process can only attach // to its descendants by default. In the runInTerminal case the target // process is launched by the client so we need to allow tracing explicitly. @@ -220,8 +281,7 @@ static llvm::Error LaunchRunInTerminalTarget(llvm::opt::Arg &target_arg, (void)prctl(PR_SET_PTRACER, debugger_pid, 0, 0, 0); #endif - RunInTerminalLauncherCommChannel comm_channel(comm_file); - if (llvm::Error err = comm_channel.NotifyPid()) + if (llvm::Error err = comm_channel.NotifyPid(getpid())) return err; // We will wait to be attached with a timeout. We don't wait indefinitely _______________________________________________ lldb-commits mailing list lldb-commits@lists.llvm.org https://lists.llvm.org/cgi-bin/mailman/listinfo/lldb-commits