https://github.com/ashgti updated https://github.com/llvm/llvm-project/pull/116392
>From 88a8522f1b29b2ff392974322acdb722b7e00b70 Mon Sep 17 00:00:00 2001 From: John Harrison <harj...@google.com> Date: Tue, 28 Jan 2025 12:39:38 -0800 Subject: [PATCH 1/5] [lldb-dap] Refactoring lldb-dap port listening mode to allow multiple connections. This adjusts the lldb-dap listening mode to accept multiple clients. Each client initializes a new instance of DAP and an associated `lldb::SBDebugger` instance. The listening mode is configured with the `--connection` option and supports listening on a port or a unix socket on supported platforms. --- .../test/tools/lldb-dap/dap_server.py | 74 ++++- .../test/tools/lldb-dap/lldbdap_testcase.py | 6 +- lldb/test/API/tools/lldb-dap/server/Makefile | 3 + .../tools/lldb-dap/server/TestDAP_server.py | 77 +++++ lldb/test/API/tools/lldb-dap/server/main.c | 10 + lldb/test/Shell/DAP/TestOptions.test | 4 +- lldb/tools/lldb-dap/DAP.cpp | 18 +- lldb/tools/lldb-dap/DAP.h | 6 +- lldb/tools/lldb-dap/DAPForward.h | 2 + lldb/tools/lldb-dap/Options.td | 22 +- lldb/tools/lldb-dap/lldb-dap.cpp | 283 +++++++++++------- 11 files changed, 358 insertions(+), 147 deletions(-) create mode 100644 lldb/test/API/tools/lldb-dap/server/Makefile create mode 100644 lldb/test/API/tools/lldb-dap/server/TestDAP_server.py create mode 100644 lldb/test/API/tools/lldb-dap/server/main.c diff --git a/lldb/packages/Python/lldbsuite/test/tools/lldb-dap/dap_server.py b/lldb/packages/Python/lldbsuite/test/tools/lldb-dap/dap_server.py index c29992ce9c7848e..6d765e10236733a 100644 --- a/lldb/packages/Python/lldbsuite/test/tools/lldb-dap/dap_server.py +++ b/lldb/packages/Python/lldbsuite/test/tools/lldb-dap/dap_server.py @@ -903,7 +903,7 @@ def request_setBreakpoints(self, file_path, line_array, data=None): "sourceModified": False, } if line_array is not None: - args_dict["lines"] = "%s" % line_array + args_dict["lines"] = line_array breakpoints = [] for i, line in enumerate(line_array): breakpoint_data = None @@ -1150,11 +1150,12 @@ def request_setInstructionBreakpoints(self, memory_reference=[]): } return self.send_recv(command_dict) + class DebugAdaptorServer(DebugCommunication): def __init__( self, executable=None, - port=None, + connection=None, init_commands=[], log_file=None, env=None, @@ -1167,21 +1168,62 @@ def __init__( if log_file: adaptor_env["LLDBDAP_LOG"] = log_file + args = [executable] + + if connection is not None: + args.append("--connection") + args.append(connection) + self.process = subprocess.Popen( - [executable], + args, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE, env=adaptor_env, ) + + if connection is not None: + # If the process was also launched, parse the connection from the + # resolved connection. For example, if the connection + # `connection://localhost:0` was specified then the OS would pick a + # random port for listening and lldb-dap would print the listening + # port to stdout. + if self.process is not None: + # lldb-dap will print the listening address once the listener is + # made to stdout. The listener is formatted like + # `connection://host:port` or `unix-connection:///path`. + expected_prefix = "Listening for: " + out = self.process.stdout.readline().decode() + if not out.startswith(expected_prefix): + self.process.kill() + raise ValueError( + "lldb-dap failed to print listening address, expected '{}', got '{}'".format( + expected_prefix, out + ) + ) + + # If the listener expanded into multiple addresses, use the first. + connection = ( + out.removeprefix(expected_prefix).rstrip("\r\n").split(",", 1)[0] + ) + + scheme, address = connection.split("://") + if scheme == "unix-connect": # unix-connect:///path + s = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) + s.connect(address) + elif scheme == "connection": # connection://[host]:port + host, port = address.rsplit(":", 1) + # create_connection with try both ipv4 and ipv6. + s = socket.create_connection((host.strip("[]"), int(port))) + else: + raise ValueError("invalid connection: {}".format(connection)) DebugCommunication.__init__( - self, self.process.stdout, self.process.stdin, init_commands, log_file + self, s.makefile("rb"), s.makefile("wb"), init_commands, log_file ) - elif port is not None: - s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) - s.connect(("127.0.0.1", port)) + self.connection = connection + else: DebugCommunication.__init__( - self, s.makefile("r"), s.makefile("w"), init_commands + self, self.process.stdout, self.process.stdin, init_commands, log_file ) def get_pid(self): @@ -1349,10 +1391,9 @@ def main(): ) parser.add_option( - "--port", - type="int", - dest="port", - help="Attach a socket to a port instead of using STDIN for VSCode", + "--connection", + dest="connection", + help="Attach a socket connection of using STDIN for VSCode", default=None, ) @@ -1498,15 +1539,16 @@ def main(): (options, args) = parser.parse_args(sys.argv[1:]) - if options.vscode_path is None and options.port is None: + if options.vscode_path is None and options.connection is None: print( "error: must either specify a path to a Visual Studio Code " "Debug Adaptor vscode executable path using the --vscode " - "option, or a port to attach to for an existing lldb-dap " - "using the --port option" + "option, or using the --connection option" ) return - dbg = DebugAdaptorServer(executable=options.vscode_path, port=options.port) + dbg = DebugAdaptorServer( + executable=options.vscode_path, connection=options.connection + ) if options.debug: raw_input('Waiting for debugger to attach pid "%i"' % (dbg.get_pid())) if options.replay: diff --git a/lldb/packages/Python/lldbsuite/test/tools/lldb-dap/lldbdap_testcase.py b/lldb/packages/Python/lldbsuite/test/tools/lldb-dap/lldbdap_testcase.py index a25466f07fa557f..015c613956d86b1 100644 --- a/lldb/packages/Python/lldbsuite/test/tools/lldb-dap/lldbdap_testcase.py +++ b/lldb/packages/Python/lldbsuite/test/tools/lldb-dap/lldbdap_testcase.py @@ -1,5 +1,6 @@ import os import time +import subprocess import dap_server from lldbsuite.test.lldbtest import * @@ -10,10 +11,10 @@ class DAPTestCaseBase(TestBase): # set timeout based on whether ASAN was enabled or not. Increase # timeout by a factor of 10 if ASAN is enabled. - timeoutval = 10 * (10 if ('ASAN_OPTIONS' in os.environ) else 1) + timeoutval = 10 * (10 if ("ASAN_OPTIONS" in os.environ) else 1) NO_DEBUG_INFO_TESTCASE = True - def create_debug_adaptor(self, lldbDAPEnv=None): + def create_debug_adaptor(self, lldbDAPEnv=None, connection=None): """Create the Visual Studio Code debug adaptor""" self.assertTrue( is_exe(self.lldbDAPExec), "lldb-dap must exist and be executable" @@ -21,6 +22,7 @@ def create_debug_adaptor(self, lldbDAPEnv=None): log_file_path = self.getBuildArtifact("dap.txt") self.dap_server = dap_server.DebugAdaptorServer( executable=self.lldbDAPExec, + connection=connection, init_commands=self.setUpCommands(), log_file=log_file_path, env=lldbDAPEnv, diff --git a/lldb/test/API/tools/lldb-dap/server/Makefile b/lldb/test/API/tools/lldb-dap/server/Makefile new file mode 100644 index 000000000000000..451278a0946ef2b --- /dev/null +++ b/lldb/test/API/tools/lldb-dap/server/Makefile @@ -0,0 +1,3 @@ +C_SOURCES := main.c + +include Makefile.rules \ No newline at end of file diff --git a/lldb/test/API/tools/lldb-dap/server/TestDAP_server.py b/lldb/test/API/tools/lldb-dap/server/TestDAP_server.py new file mode 100644 index 000000000000000..9597f73b0659bee --- /dev/null +++ b/lldb/test/API/tools/lldb-dap/server/TestDAP_server.py @@ -0,0 +1,77 @@ +""" +Test lldb-dap server integration. +""" + +import os +import signal +import tempfile + +import dap_server +from lldbsuite.test.decorators import * +from lldbsuite.test.lldbtest import * +import lldbdap_testcase + + +class TestDAP_server(lldbdap_testcase.DAPTestCaseBase): + def start_server(self, connection): + log_file_path = self.getBuildArtifact("dap.txt") + server = dap_server.DebugAdaptorServer( + executable=self.lldbDAPExec, + connection=connection, + init_commands=self.setUpCommands(), + log_file=log_file_path, + ) + + def cleanup(): + server.terminate() + + self.addTearDownHook(cleanup) + + return server + + def run_debug_session(self, connection, name): + self.dap_server = dap_server.DebugAdaptorServer( + connection=connection, + ) + program = self.getBuildArtifact("a.out") + source = "main.c" + breakpoint_line = line_number(source, "// breakpoint") + + self.launch( + program, + args=[name], + disconnectAutomatically=False, + ) + self.set_source_breakpoints(source, [breakpoint_line]) + self.continue_to_next_stop() + self.continue_to_exit() + output = self.get_stdout() + self.assertEqual(output, f"Hello {name}!\r\n") + self.dap_server.request_disconnect() + + def test_server_port(self): + """ + Test launching a binary with a lldb-dap in server mode on a specific port. + """ + self.build() + server = self.start_server(connection="tcp://localhost:0") + self.run_debug_session(server.connection, "Alice") + self.run_debug_session(server.connection, "Bob") + + @skipIfWindows + def test_server_unix_socket(self): + """ + Test launching a binary with a lldb-dap in server mode on a unix socket. + """ + dir = tempfile.gettempdir() + name = dir + "/dap-connection-" + str(os.getpid()) + + def cleanup(): + os.unlink(name) + + self.addTearDownHook(cleanup) + + self.build() + server = self.start_server(connection="unix://" + name) + self.run_debug_session(server.connection, "Alice") + self.run_debug_session(server.connection, "Bob") diff --git a/lldb/test/API/tools/lldb-dap/server/main.c b/lldb/test/API/tools/lldb-dap/server/main.c new file mode 100644 index 000000000000000..446ae82532af57e --- /dev/null +++ b/lldb/test/API/tools/lldb-dap/server/main.c @@ -0,0 +1,10 @@ +#include <stdio.h> + +int main(int argc, char const *argv[]) { + if (argc == 2) { // breakpoint 1 + printf("Hello %s!\n", argv[1]); + } else { + printf("Hello World!\n"); + } + return 0; +} diff --git a/lldb/test/Shell/DAP/TestOptions.test b/lldb/test/Shell/DAP/TestOptions.test index e37e9116e3cddb5..d290cdae590fd67 100644 --- a/lldb/test/Shell/DAP/TestOptions.test +++ b/lldb/test/Shell/DAP/TestOptions.test @@ -1,8 +1,8 @@ # RUN: lldb-dap --help | FileCheck %s +# CHECK: --connection # CHECK: -g # CHECK: --help # CHECK: -h -# CHECK: --port -# CHECK: -p +# CHECK: --repl-mode # CHECK: --wait-for-debugger diff --git a/lldb/tools/lldb-dap/DAP.cpp b/lldb/tools/lldb-dap/DAP.cpp index a67abe582abd40c..11a4db5c7f0f48a 100644 --- a/lldb/tools/lldb-dap/DAP.cpp +++ b/lldb/tools/lldb-dap/DAP.cpp @@ -17,7 +17,6 @@ #include "lldb/API/SBListener.h" #include "lldb/API/SBProcess.h" #include "lldb/API/SBStream.h" -#include "lldb/Host/FileSystem.h" #include "lldb/Utility/Status.h" #include "lldb/lldb-defines.h" #include "lldb/lldb-enumerations.h" @@ -58,12 +57,13 @@ const char DEV_NULL[] = "/dev/null"; namespace lldb_dap { -DAP::DAP(llvm::StringRef path, std::ofstream *log, ReplMode repl_mode, - StreamDescriptor input, StreamDescriptor output) - : debug_adaptor_path(path), log(log), input(std::move(input)), +DAP::DAP(std::string name, llvm::StringRef path, std::ofstream *log, + StreamDescriptor input, StreamDescriptor output, ReplMode repl_mode, + std::vector<std::string> pre_init_commands) + : name(name), debug_adaptor_path(path), log(log), input(std::move(input)), output(std::move(output)), broadcaster("lldb-dap"), - exception_breakpoints(), focus_tid(LLDB_INVALID_THREAD_ID), - stop_at_entry(false), is_attach(false), + exception_breakpoints(), pre_init_commands(pre_init_commands), + focus_tid(LLDB_INVALID_THREAD_ID), stop_at_entry(false), is_attach(false), enable_auto_variable_summaries(false), enable_synthetic_child_debugging(false), display_extended_backtrace(false), @@ -249,7 +249,8 @@ void DAP::SendJSON(const llvm::json::Value &json) { if (log) { auto now = std::chrono::duration<double>( std::chrono::system_clock::now().time_since_epoch()); - *log << llvm::formatv("{0:f9} <-- ", now.count()).str() << std::endl + *log << llvm::formatv("{0:f9} {1} <-- ", now.count(), name).str() + << std::endl << "Content-Length: " << json_str.size() << "\r\n\r\n" << llvm::formatv("{0:2}", json).str() << std::endl; } @@ -279,7 +280,8 @@ std::string DAP::ReadJSON() { if (log) { auto now = std::chrono::duration<double>( std::chrono::system_clock::now().time_since_epoch()); - *log << llvm::formatv("{0:f9} --> ", now.count()).str() << std::endl + *log << llvm::formatv("{0:f9} {1} --> ", now.count(), name).str() + << std::endl << "Content-Length: " << length << "\r\n\r\n"; } return json_str; diff --git a/lldb/tools/lldb-dap/DAP.h b/lldb/tools/lldb-dap/DAP.h index 846300cb945b0d1..7c28dbf801179fa 100644 --- a/lldb/tools/lldb-dap/DAP.h +++ b/lldb/tools/lldb-dap/DAP.h @@ -139,6 +139,7 @@ struct SendEventRequestHandler : public lldb::SBCommandPluginInterface { }; struct DAP { + std::string name; llvm::StringRef debug_adaptor_path; std::ofstream *log; InputStream input; @@ -203,8 +204,9 @@ struct DAP { // will contain that expression. std::string last_nonempty_var_expression; - DAP(llvm::StringRef path, std::ofstream *log, ReplMode repl_mode, - StreamDescriptor input, StreamDescriptor output); + DAP(std::string name, llvm::StringRef path, std::ofstream *log, + StreamDescriptor input, StreamDescriptor output, ReplMode repl_mode, + std::vector<std::string> pre_init_commands); ~DAP(); DAP(const DAP &rhs) = delete; void operator=(const DAP &rhs) = delete; diff --git a/lldb/tools/lldb-dap/DAPForward.h b/lldb/tools/lldb-dap/DAPForward.h index 0196d83dcd6a91b..15e286f3d08dc1e 100644 --- a/lldb/tools/lldb-dap/DAPForward.h +++ b/lldb/tools/lldb-dap/DAPForward.h @@ -11,6 +11,8 @@ // IWYU pragma: begin_exports +#include "lldb/lldb-forward.h" + namespace lldb_dap { struct BreakpointBase; struct ExceptionBreakpoint; diff --git a/lldb/tools/lldb-dap/Options.td b/lldb/tools/lldb-dap/Options.td index d7b4a065abec010..97a6ec118c47b58 100644 --- a/lldb/tools/lldb-dap/Options.td +++ b/lldb/tools/lldb-dap/Options.td @@ -17,12 +17,13 @@ def: Flag<["-"], "g">, Alias<wait_for_debugger>, HelpText<"Alias for --wait-for-debugger">; -def port: S<"port">, - MetaVarName<"<port>">, - HelpText<"Communicate with the lldb-dap tool over the defined port.">; -def: Separate<["-"], "p">, - Alias<port>, - HelpText<"Alias for --port">; +def connection + : S<"connection">, + MetaVarName<"<connection>">, + HelpText< + "Communicate with the lldb-dap tool over the specified connection. " + "Connections are specified like 'tcp://[host]:port' or " + "'unix:///path'.">; def launch_target: S<"launch-target">, MetaVarName<"<target>">, @@ -40,9 +41,12 @@ def debugger_pid: S<"debugger-pid">, HelpText<"The PID of the lldb-dap instance that sent the launchInTerminal " "request when using --launch-target.">; -def repl_mode: S<"repl-mode">, - MetaVarName<"<mode>">, - HelpText<"The mode for handling repl evaluation requests, supported modes: variable, command, auto.">; +def repl_mode + : S<"repl-mode">, + MetaVarName<"<mode>">, + HelpText< + "The mode for handling repl evaluation requests, supported modes: " + "variable, command, auto.">; def pre_init_command: S<"pre-init-command">, MetaVarName<"<command>">, diff --git a/lldb/tools/lldb-dap/lldb-dap.cpp b/lldb/tools/lldb-dap/lldb-dap.cpp index 9e0e7f21ce4fc7a..2c7fdb0f0d3cfb2 100644 --- a/lldb/tools/lldb-dap/lldb-dap.cpp +++ b/lldb/tools/lldb-dap/lldb-dap.cpp @@ -14,45 +14,55 @@ #include "Watchpoint.h" #include "lldb/API/SBDeclaration.h" #include "lldb/API/SBEvent.h" +#include "lldb/API/SBFile.h" #include "lldb/API/SBInstruction.h" #include "lldb/API/SBListener.h" #include "lldb/API/SBMemoryRegionInfo.h" #include "lldb/API/SBStream.h" #include "lldb/API/SBStringList.h" #include "lldb/Host/Config.h" +#include "lldb/Host/Socket.h" +#include "lldb/Utility/Status.h" +#include "lldb/Utility/UriParser.h" +#include "lldb/lldb-forward.h" #include "llvm/ADT/ArrayRef.h" #include "llvm/ADT/DenseMap.h" #include "llvm/ADT/DenseSet.h" #include "llvm/ADT/ScopeExit.h" #include "llvm/ADT/StringExtras.h" +#include "llvm/ADT/StringRef.h" #include "llvm/Option/Arg.h" #include "llvm/Option/ArgList.h" #include "llvm/Option/OptTable.h" #include "llvm/Option/Option.h" #include "llvm/Support/Base64.h" -#include "llvm/Support/Errno.h" +#include "llvm/Support/Error.h" #include "llvm/Support/FileSystem.h" #include "llvm/Support/InitLLVM.h" #include "llvm/Support/Path.h" #include "llvm/Support/PrettyStackTrace.h" +#include "llvm/Support/Signals.h" +#include "llvm/Support/ThreadPool.h" #include "llvm/Support/raw_ostream.h" #include <algorithm> #include <array> -#include <cassert> -#include <climits> -#include <cstdarg> #include <cstdint> #include <cstdio> #include <cstdlib> #include <cstring> #include <fcntl.h> +#include <fstream> +#include <iostream> #include <map> #include <memory> #include <optional> +#include <ostream> #include <set> +#include <string> #include <sys/stat.h> #include <sys/types.h> #include <thread> +#include <utility> #include <vector> #if defined(_WIN32) @@ -68,6 +78,7 @@ #else #include <netinet/in.h> #include <sys/socket.h> +#include <sys/un.h> #include <unistd.h> #endif @@ -83,6 +94,9 @@ typedef int socklen_t; #endif using namespace lldb_dap; +using lldb_private::NativeSocket; +using lldb_private::Socket; +using lldb_private::Status; namespace { using namespace llvm::opt; @@ -142,43 +156,6 @@ lldb::SBValueList *GetTopLevelScope(DAP &dap, int64_t variablesReference) { } } -SOCKET AcceptConnection(std::ofstream *log, int portno) { - // Accept a socket connection from any host on "portno". - SOCKET newsockfd = -1; - struct sockaddr_in serv_addr, cli_addr; - SOCKET sockfd = socket(AF_INET, SOCK_STREAM, 0); - if (sockfd < 0) { - if (log) - *log << "error: opening socket (" << strerror(errno) << ")" << std::endl; - } else { - memset((char *)&serv_addr, 0, sizeof(serv_addr)); - serv_addr.sin_family = AF_INET; - // serv_addr.sin_addr.s_addr = htonl(INADDR_ANY); - serv_addr.sin_addr.s_addr = htonl(INADDR_LOOPBACK); - serv_addr.sin_port = htons(portno); - if (bind(sockfd, (struct sockaddr *)&serv_addr, sizeof(serv_addr)) < 0) { - if (log) - *log << "error: binding socket (" << strerror(errno) << ")" - << std::endl; - } else { - listen(sockfd, 5); - socklen_t clilen = sizeof(cli_addr); - newsockfd = - llvm::sys::RetryAfterSignal(static_cast<SOCKET>(-1), accept, sockfd, - (struct sockaddr *)&cli_addr, &clilen); - if (newsockfd < 0) - if (log) - *log << "error: accept (" << strerror(errno) << ")" << std::endl; - } -#if defined(_WIN32) - closesocket(sockfd); -#else - close(sockfd); -#endif - } - return newsockfd; -} - std::vector<const char *> MakeArgv(const llvm::ArrayRef<std::string> &strs) { // Create and return an array of "const char *", one for each C string in // "strs" and terminate the list with a NULL. This can be used for argument @@ -4857,10 +4834,10 @@ static void printHelp(LLDBDAPOptTable &table, llvm::StringRef tool_name) { The debug adapter can be started in two modes. Running lldb-dap without any arguments will start communicating with the - parent over stdio. Passing a port number causes lldb-dap to start listening - for connections on that port. + parent over stdio. Passing a --connection URI will cause lldb-dap to listen + for a connection in the specified mode. - lldb-dap -p <port> + lldb-dap --connection connection://localhost:<port> Passing --wait-for-debugger will pause the process at startup and wait for a debugger to attach to the process. @@ -4952,6 +4929,29 @@ static int DuplicateFileDescriptor(int fd) { #endif } +static llvm::Expected<std::pair<Socket::SocketProtocol, std::string>> +validateConnection(llvm::StringRef conn) { + auto uri = lldb_private::URI::Parse(conn); + + if (uri && (uri->scheme == "tcp" || uri->scheme == "connect" || + !uri->hostname.empty() || uri->port)) { + return std::make_pair( + Socket::ProtocolTcp, + formatv("[{0}]:{1}", uri->hostname.empty() ? "0.0.0.0" : uri->hostname, + uri->port.value_or(0))); + } + + if (uri && (uri->scheme == "unix" || uri->scheme == "unix-connect" || + uri->path != "/")) { + return std::make_pair(Socket::ProtocolUnixDomain, uri->path.str()); + } + + return llvm::createStringError( + "Unsupported connection specifier, expected 'unix-connect:///path' or " + "'connect://[host]:port', got '%s'.", + conn.str().c_str()); +} + int main(int argc, char *argv[]) { llvm::InitLLVM IL(argc, argv, /*InstallPipeSignalExitHandler=*/false); #if !defined(__APPLE__) @@ -4987,9 +4987,9 @@ int main(int argc, char *argv[]) { } else if (repl_mode_value == "command") { default_repl_mode = ReplMode::Command; } else { - llvm::errs() - << "'" << repl_mode_value - << "' is not a valid option, use 'variable', 'command' or 'auto'.\n"; + llvm::errs() << "'" << repl_mode_value + << "' is not a valid option, use 'variable', 'command' or " + "'auto'.\n"; return EXIT_FAILURE; } } @@ -5022,15 +5022,10 @@ int main(int argc, char *argv[]) { } } - int portno = -1; - if (auto *arg = input_args.getLastArg(OPT_port)) { - const auto *optarg = arg->getValue(); - char *remainder; - portno = strtol(optarg, &remainder, 0); - if (remainder == optarg || *remainder != '\0') { - fprintf(stderr, "'%s' is not a valid port number.\n", optarg); - return EXIT_FAILURE; - } + std::string connection; + if (auto *arg = input_args.getLastArg(OPT_connection)) { + const auto *path = arg->getValue(); + connection.assign(path); } #if !defined(_WIN32) @@ -5058,72 +5053,144 @@ int main(int argc, char *argv[]) { auto terminate_debugger = llvm::make_scope_exit([] { lldb::SBDebugger::Terminate(); }); - StreamDescriptor input; - StreamDescriptor output; - std::FILE *redirectOut = nullptr; - std::FILE *redirectErr = nullptr; - if (portno != -1) { - printf("Listening on port %i...\n", portno); - SOCKET socket_fd = AcceptConnection(log.get(), portno); - if (socket_fd < 0) - return EXIT_FAILURE; + std::vector<std::string> pre_init_commands; + for (const std::string &arg : + input_args.getAllArgValues(OPT_pre_init_command)) { + pre_init_commands.push_back(arg); + } - input = StreamDescriptor::from_socket(socket_fd, true); - output = StreamDescriptor::from_socket(socket_fd, false); - } else { -#if defined(_WIN32) - // Windows opens stdout and stdin in text mode which converts \n to 13,10 - // while the value is just 10 on Darwin/Linux. Setting the file mode to - // binary fixes this. - int result = _setmode(fileno(stdout), _O_BINARY); - assert(result); - result = _setmode(fileno(stdin), _O_BINARY); - UNUSED_IF_ASSERT_DISABLED(result); - assert(result); -#endif + auto RunDAP = [](llvm::StringRef program_path, ReplMode repl_mode, + std::vector<std::string> pre_init_commands, + std::ofstream *log, std::string name, StreamDescriptor input, + StreamDescriptor output, std::FILE *redirectOut = nullptr, + std::FILE *redirectErr = nullptr) -> bool { + DAP dap = DAP(name, program_path, log, std::move(input), std::move(output), + repl_mode, pre_init_commands); - int stdout_fd = DuplicateFileDescriptor(fileno(stdout)); - if (stdout_fd == -1) { + // stdout/stderr redirection to the IDE's console + if (auto Err = dap.ConfigureIO(redirectOut, redirectErr)) { llvm::logAllUnhandledErrors( - llvm::errorCodeToError(llvm::errnoAsErrorCode()), llvm::errs(), - "Failed to configure stdout redirect: "); + std::move(Err), llvm::errs(), + "Failed to configure lldb-dap IO operations: "); + return false; + } + + RegisterRequestCallbacks(dap); + + // used only by TestVSCode_redirection_to_console.py + if (getenv("LLDB_DAP_TEST_STDOUT_STDERR_REDIRECTION") != nullptr) + redirection_test(); + + if (auto Err = dap.Loop()) { + std::string errorMessage = llvm::toString(std::move(Err)); + if (log) + *log << "Transport Error: " << errorMessage << "\n"; + return false; + } + return true; + }; + + if (!connection.empty()) { + auto maybeProtoclAndName = validateConnection(connection); + if (auto Err = maybeProtoclAndName.takeError()) { + llvm::logAllUnhandledErrors(std::move(Err), llvm::errs(), + "Invalid connection: "); return EXIT_FAILURE; } - redirectOut = stdout; - redirectErr = stderr; + Socket::SocketProtocol protocol; + std::string name; + std::tie(protocol, name) = *maybeProtoclAndName; - input = StreamDescriptor::from_file(fileno(stdin), false); - output = StreamDescriptor::from_file(stdout_fd, false); - } + Status error; + std::unique_ptr<Socket> listener = Socket::Create(protocol, error); + if (error.Fail()) { + llvm::logAllUnhandledErrors(error.takeError(), llvm::errs(), + "Failed to create socket listener: "); + return EXIT_FAILURE; + } - DAP dap = DAP(program_path.str(), log.get(), default_repl_mode, - std::move(input), std::move(output)); + error = listener->Listen(name, /* backlog */ 5); + if (error.Fail()) { + llvm::logAllUnhandledErrors(error.takeError(), llvm::errs(), + "Failed to listen for connections: "); + return EXIT_FAILURE; + } - // stdout/stderr redirection to the IDE's console - if (auto Err = dap.ConfigureIO(redirectOut, redirectErr)) { - llvm::logAllUnhandledErrors(std::move(Err), llvm::errs(), - "Failed to configure lldb-dap IO operations: "); - return EXIT_FAILURE; - } + std::string address = + llvm::join(listener->GetListeningConnectionURI(), ", "); + if (log) + *log << "started with connection listeners " << address << "\n"; + + llvm::outs() << "Listening for: " << address << "\n"; + // Ensure listening address are flushed for calles to retrieve the resolve + // address. + llvm::outs().flush(); + + llvm::DefaultThreadPool pool(llvm::optimal_concurrency()); + unsigned int clientCount = 0; + + while (true) { + Socket *accepted_socket; + error = listener->Accept(/*timeout=*/std::nullopt, accepted_socket); + if (error.Fail()) { + llvm::logAllUnhandledErrors(error.takeError(), llvm::errs(), + "accept failed: "); + return EXIT_FAILURE; + } - RegisterRequestCallbacks(dap); + std::string name = llvm::formatv("client_{0}", clientCount++).str(); + if (log) { + auto now = std::chrono::duration<double>( + std::chrono::system_clock::now().time_since_epoch()); + *log << llvm::formatv("{0:f9}", now.count()).str() + << " client connected: " << name << "\n"; + } - for (const std::string &arg : - input_args.getAllArgValues(OPT_pre_init_command)) { - dap.pre_init_commands.push_back(arg); + // Move the client into the connection pool to unblock accepting the next + // client. + pool.async( + [=](Socket *accepted_socket, std::ofstream *log) { + StreamDescriptor input = StreamDescriptor::from_socket( + accepted_socket->GetNativeSocket(), false); + // Close the output last for the best chance at error reporting. + StreamDescriptor output = StreamDescriptor::from_socket( + accepted_socket->GetNativeSocket(), true); + RunDAP(program_path, default_repl_mode, pre_init_commands, log, + name, std::move(input), std::move(output)); + }, + accepted_socket, log.get()); + } + + pool.wait(); + + return EXIT_SUCCESS; } - // used only by TestVSCode_redirection_to_console.py - if (getenv("LLDB_DAP_TEST_STDOUT_STDERR_REDIRECTION") != nullptr) - redirection_test(); +#if defined(_WIN32) + // Windows opens stdout and stdin in text mode which converts \n to 13,10 + // while the value is just 10 on Darwin/Linux. Setting the file mode to + // binary fixes this. + int result = _setmode(fileno(stdout), _O_BINARY); + assert(result); + result = _setmode(fileno(stdin), _O_BINARY); + UNUSED_IF_ASSERT_DISABLED(result); + assert(result); +#endif - bool CleanExit = true; - if (auto Err = dap.Loop()) { - if (log) - *log << "Transport Error: " << llvm::toString(std::move(Err)) << "\n"; - CleanExit = false; + int stdout_fd = DuplicateFileDescriptor(fileno(stdout)); + if (stdout_fd == -1) { + llvm::logAllUnhandledErrors( + llvm::errorCodeToError(llvm::errnoAsErrorCode()), llvm::errs(), + "Failed to configure stdout redirect: "); + return EXIT_FAILURE; } - return CleanExit ? EXIT_SUCCESS : EXIT_FAILURE; + StreamDescriptor input = StreamDescriptor::from_file(fileno(stdin), false); + StreamDescriptor output = StreamDescriptor::from_file(stdout_fd, false); + + bool status = RunDAP(program_path, default_repl_mode, pre_init_commands, + log.get(), "stdin/stdout", std::move(input), + std::move(output), stdout, stderr); + return status ? EXIT_SUCCESS : EXIT_FAILURE; } >From 2fe4f85f7620be29c28452783a3d2cffafb91c31 Mon Sep 17 00:00:00 2001 From: John Harrison <harj...@google.com> Date: Tue, 4 Feb 2025 10:57:32 -0800 Subject: [PATCH 2/5] Applying review comments. --- lldb/test/API/tools/lldb-dap/server/Makefile | 2 +- lldb/tools/lldb-dap/DAP.cpp | 2 +- lldb/tools/lldb-dap/DAPForward.h | 2 -- lldb/tools/lldb-dap/lldb-dap.cpp | 5 ++--- 4 files changed, 4 insertions(+), 7 deletions(-) diff --git a/lldb/test/API/tools/lldb-dap/server/Makefile b/lldb/test/API/tools/lldb-dap/server/Makefile index 451278a0946ef2b..10495940055b63d 100644 --- a/lldb/test/API/tools/lldb-dap/server/Makefile +++ b/lldb/test/API/tools/lldb-dap/server/Makefile @@ -1,3 +1,3 @@ C_SOURCES := main.c -include Makefile.rules \ No newline at end of file +include Makefile.rules diff --git a/lldb/tools/lldb-dap/DAP.cpp b/lldb/tools/lldb-dap/DAP.cpp index 11a4db5c7f0f48a..cfaa8ad53c6340a 100644 --- a/lldb/tools/lldb-dap/DAP.cpp +++ b/lldb/tools/lldb-dap/DAP.cpp @@ -62,7 +62,7 @@ DAP::DAP(std::string name, llvm::StringRef path, std::ofstream *log, std::vector<std::string> pre_init_commands) : name(name), debug_adaptor_path(path), log(log), input(std::move(input)), output(std::move(output)), broadcaster("lldb-dap"), - exception_breakpoints(), pre_init_commands(pre_init_commands), + exception_breakpoints(), pre_init_commands(std::move(pre_init_commands)), focus_tid(LLDB_INVALID_THREAD_ID), stop_at_entry(false), is_attach(false), enable_auto_variable_summaries(false), enable_synthetic_child_debugging(false), diff --git a/lldb/tools/lldb-dap/DAPForward.h b/lldb/tools/lldb-dap/DAPForward.h index 15e286f3d08dc1e..0196d83dcd6a91b 100644 --- a/lldb/tools/lldb-dap/DAPForward.h +++ b/lldb/tools/lldb-dap/DAPForward.h @@ -11,8 +11,6 @@ // IWYU pragma: begin_exports -#include "lldb/lldb-forward.h" - namespace lldb_dap { struct BreakpointBase; struct ExceptionBreakpoint; diff --git a/lldb/tools/lldb-dap/lldb-dap.cpp b/lldb/tools/lldb-dap/lldb-dap.cpp index 2c7fdb0f0d3cfb2..fd86c8d8b3a9b56 100644 --- a/lldb/tools/lldb-dap/lldb-dap.cpp +++ b/lldb/tools/lldb-dap/lldb-dap.cpp @@ -5110,7 +5110,7 @@ int main(int argc, char *argv[]) { return EXIT_FAILURE; } - error = listener->Listen(name, /* backlog */ 5); + error = listener->Listen(name, /*backlog=*/5); if (error.Fail()) { llvm::logAllUnhandledErrors(error.takeError(), llvm::errs(), "Failed to listen for connections: "); @@ -5127,9 +5127,8 @@ int main(int argc, char *argv[]) { // address. llvm::outs().flush(); - llvm::DefaultThreadPool pool(llvm::optimal_concurrency()); + llvm::DefaultThreadPool pool; unsigned int clientCount = 0; - while (true) { Socket *accepted_socket; error = listener->Accept(/*timeout=*/std::nullopt, accepted_socket); >From 606decbf8ea180880e899625c73de11f0fecafa1 Mon Sep 17 00:00:00 2001 From: John Harrison <harj...@google.com> Date: Wed, 5 Feb 2025 10:06:05 -0800 Subject: [PATCH 3/5] Ensuring the server mode tracks active DAP sessions and adding an interrupt handler to disconnect active sessions. --- .../tools/lldb-dap/server/TestDAP_server.py | 31 +++ lldb/tools/lldb-dap/DAP.cpp | 51 ++++ lldb/tools/lldb-dap/DAP.h | 12 +- lldb/tools/lldb-dap/lldb-dap.cpp | 225 +++++++++--------- 4 files changed, 210 insertions(+), 109 deletions(-) diff --git a/lldb/test/API/tools/lldb-dap/server/TestDAP_server.py b/lldb/test/API/tools/lldb-dap/server/TestDAP_server.py index 9597f73b0659bee..595bb7a0d139212 100644 --- a/lldb/test/API/tools/lldb-dap/server/TestDAP_server.py +++ b/lldb/test/API/tools/lldb-dap/server/TestDAP_server.py @@ -75,3 +75,34 @@ def cleanup(): server = self.start_server(connection="unix://" + name) self.run_debug_session(server.connection, "Alice") self.run_debug_session(server.connection, "Bob") + + @skipIfWindows + def test_server_interrupt(self): + """ + Test launching a binary with lldb-dap in server mode and shutting down the server while the debug session is still active. + """ + self.build() + server = self.start_server(connection="tcp://localhost:0") + self.dap_server = dap_server.DebugAdaptorServer( + connection=server.connection, + ) + program = self.getBuildArtifact("a.out") + source = "main.c" + breakpoint_line = line_number(source, "// breakpoint") + + self.launch( + program, + args=["Alice"], + disconnectAutomatically=False, + ) + self.set_source_breakpoints(source, [breakpoint_line]) + self.continue_to_next_stop() + + # Interrupt the server which should disconnect all clients. + server.process.send_signal(signal.SIGINT) + + self.dap_server.wait_for_terminated() + self.assertIsNone( + self.dap_server.exit_status, + "Process exited before interrupting lldb-dap server", + ) diff --git a/lldb/tools/lldb-dap/DAP.cpp b/lldb/tools/lldb-dap/DAP.cpp index cfaa8ad53c6340a..4da077e857a4a2f 100644 --- a/lldb/tools/lldb-dap/DAP.cpp +++ b/lldb/tools/lldb-dap/DAP.cpp @@ -795,6 +795,57 @@ bool DAP::HandleObject(const llvm::json::Object &object) { return false; } +void DAP::SendTerminatedEvent() { + // Prevent races if the process exits while we're being asked to disconnect. + llvm::call_once(terminated_event_flag, [&] { + RunTerminateCommands(); + // Send a "terminated" event + llvm::json::Object event(CreateTerminatedEventObject(target)); + SendJSON(llvm::json::Value(std::move(event))); + }); +} + +lldb::SBError DAP::Disconnect() { return Disconnect(is_attach); } + +lldb::SBError DAP::Disconnect(bool terminateDebuggee) { + lldb::SBError error; + lldb::SBProcess process = target.GetProcess(); + auto state = process.GetState(); + switch (state) { + case lldb::eStateInvalid: + case lldb::eStateUnloaded: + case lldb::eStateDetached: + case lldb::eStateExited: + break; + case lldb::eStateConnected: + case lldb::eStateAttaching: + case lldb::eStateLaunching: + case lldb::eStateStepping: + case lldb::eStateCrashed: + case lldb::eStateSuspended: + case lldb::eStateStopped: + case lldb::eStateRunning: + debugger.SetAsync(false); + error = terminateDebuggee ? process.Kill() : process.Detach(); + debugger.SetAsync(true); + break; + } + SendTerminatedEvent(); + + if (event_thread.joinable()) { + broadcaster.BroadcastEventByType(eBroadcastBitStopEventThread); + event_thread.join(); + } + if (progress_event_thread.joinable()) { + broadcaster.BroadcastEventByType(eBroadcastBitStopProgressThread); + progress_event_thread.join(); + } + StopIO(); + disconnecting = true; + + return error; +} + llvm::Error DAP::Loop() { while (!disconnecting) { llvm::json::Object object; diff --git a/lldb/tools/lldb-dap/DAP.h b/lldb/tools/lldb-dap/DAP.h index 7c28dbf801179fa..6347dfb57890eb8 100644 --- a/lldb/tools/lldb-dap/DAP.h +++ b/lldb/tools/lldb-dap/DAP.h @@ -217,7 +217,8 @@ struct DAP { /// /// Errors in this operation will be printed to the log file and the IDE's /// console output as well. - llvm::Error ConfigureIO(std::FILE *overrideOut, std::FILE *overrideErr); + llvm::Error ConfigureIO(std::FILE *overrideOut = nullptr, + std::FILE *overrideErr = nullptr); /// Stop the redirected IO threads and associated pipes. void StopIO(); @@ -305,6 +306,15 @@ struct DAP { PacketStatus GetNextObject(llvm::json::Object &object); bool HandleObject(const llvm::json::Object &object); + /// Disconnect the DAP session. + lldb::SBError Disconnect(); + + /// Disconnect the DAP session and optionally terminate the debuggee. + lldb::SBError Disconnect(bool terminateDebuggee); + + /// Send a "terminated" event to indicate the process is done being debugged. + void SendTerminatedEvent(); + llvm::Error Loop(); /// Send a Debug Adapter Protocol reverse request to the IDE. diff --git a/lldb/tools/lldb-dap/lldb-dap.cpp b/lldb/tools/lldb-dap/lldb-dap.cpp index fd86c8d8b3a9b56..4b9b5fb4310ac58 100644 --- a/lldb/tools/lldb-dap/lldb-dap.cpp +++ b/lldb/tools/lldb-dap/lldb-dap.cpp @@ -21,6 +21,8 @@ #include "lldb/API/SBStream.h" #include "lldb/API/SBStringList.h" #include "lldb/Host/Config.h" +#include "lldb/Host/MainLoop.h" +#include "lldb/Host/MainLoopBase.h" #include "lldb/Host/Socket.h" #include "lldb/Utility/Status.h" #include "lldb/Utility/UriParser.h" @@ -42,7 +44,7 @@ #include "llvm/Support/Path.h" #include "llvm/Support/PrettyStackTrace.h" #include "llvm/Support/Signals.h" -#include "llvm/Support/ThreadPool.h" +#include "llvm/Support/Threading.h" #include "llvm/Support/raw_ostream.h" #include <algorithm> #include <array> @@ -55,6 +57,7 @@ #include <iostream> #include <map> #include <memory> +#include <mutex> #include <optional> #include <ostream> #include <set> @@ -207,18 +210,6 @@ void SendContinuedEvent(DAP &dap) { dap.SendJSON(llvm::json::Value(std::move(event))); } -// Send a "terminated" event to indicate the process is done being -// debugged. -void SendTerminatedEvent(DAP &dap) { - // Prevent races if the process exits while we're being asked to disconnect. - llvm::call_once(dap.terminated_event_flag, [&] { - dap.RunTerminateCommands(); - // Send a "terminated" event - llvm::json::Object event(CreateTerminatedEventObject(dap.target)); - dap.SendJSON(llvm::json::Value(std::move(event))); - }); -} - // Send a thread stopped event for all threads as long as the process // is stopped. void SendThreadStoppedEvent(DAP &dap) { @@ -390,6 +381,7 @@ void SendStdOutStdErr(DAP &dap, lldb::SBProcess &process) { } void ProgressEventThreadFunction(DAP &dap) { + llvm::set_thread_name(dap.name + ".progress_handler"); lldb::SBListener listener("lldb-dap.progress.listener"); dap.debugger.GetBroadcaster().AddListener( listener, lldb::SBDebugger::eBroadcastBitProgress | @@ -424,8 +416,10 @@ void ProgressEventThreadFunction(DAP &dap) { // them prevent multiple threads from writing simultaneously so no locking // is required. void EventThreadFunction(DAP &dap) { + llvm::set_thread_name(dap.name + ".event_handler"); lldb::SBEvent event; lldb::SBListener listener = dap.debugger.GetListener(); + dap.broadcaster.AddListener(listener, eBroadcastBitStopEventThread); bool done = false; while (!done) { if (listener.WaitForEvent(1, event)) { @@ -490,7 +484,7 @@ void EventThreadFunction(DAP &dap) { // launch.json dap.RunExitCommands(); SendProcessExitedEvent(dap, process); - SendTerminatedEvent(dap); + dap.SendTerminatedEvent(); done = true; } break; @@ -1048,41 +1042,12 @@ void request_disconnect(DAP &dap, const llvm::json::Object &request) { bool defaultTerminateDebuggee = dap.is_attach ? false : true; bool terminateDebuggee = GetBoolean(arguments, "terminateDebuggee", defaultTerminateDebuggee); - lldb::SBProcess process = dap.target.GetProcess(); - auto state = process.GetState(); - switch (state) { - case lldb::eStateInvalid: - case lldb::eStateUnloaded: - case lldb::eStateDetached: - case lldb::eStateExited: - break; - case lldb::eStateConnected: - case lldb::eStateAttaching: - case lldb::eStateLaunching: - case lldb::eStateStepping: - case lldb::eStateCrashed: - case lldb::eStateSuspended: - case lldb::eStateStopped: - case lldb::eStateRunning: - dap.debugger.SetAsync(false); - lldb::SBError error = terminateDebuggee ? process.Kill() : process.Detach(); - if (!error.Success()) - EmplaceSafeString(response, "error", error.GetCString()); - dap.debugger.SetAsync(true); - break; - } - SendTerminatedEvent(dap); + + lldb::SBError error = dap.Disconnect(terminateDebuggee); + if (error.Fail()) + EmplaceSafeString(response, "error", error.GetCString()); + dap.SendJSON(llvm::json::Value(std::move(response))); - if (dap.event_thread.joinable()) { - dap.broadcaster.BroadcastEventByType(eBroadcastBitStopEventThread); - dap.event_thread.join(); - } - if (dap.progress_event_thread.joinable()) { - dap.broadcaster.BroadcastEventByType(eBroadcastBitStopProgressThread); - dap.progress_event_thread.join(); - } - dap.StopIO(); - dap.disconnecting = true; } // "ExceptionInfoRequest": { @@ -5059,37 +5024,6 @@ int main(int argc, char *argv[]) { pre_init_commands.push_back(arg); } - auto RunDAP = [](llvm::StringRef program_path, ReplMode repl_mode, - std::vector<std::string> pre_init_commands, - std::ofstream *log, std::string name, StreamDescriptor input, - StreamDescriptor output, std::FILE *redirectOut = nullptr, - std::FILE *redirectErr = nullptr) -> bool { - DAP dap = DAP(name, program_path, log, std::move(input), std::move(output), - repl_mode, pre_init_commands); - - // stdout/stderr redirection to the IDE's console - if (auto Err = dap.ConfigureIO(redirectOut, redirectErr)) { - llvm::logAllUnhandledErrors( - std::move(Err), llvm::errs(), - "Failed to configure lldb-dap IO operations: "); - return false; - } - - RegisterRequestCallbacks(dap); - - // used only by TestVSCode_redirection_to_console.py - if (getenv("LLDB_DAP_TEST_STDOUT_STDERR_REDIRECTION") != nullptr) - redirection_test(); - - if (auto Err = dap.Loop()) { - std::string errorMessage = llvm::toString(std::move(Err)); - if (log) - *log << "Transport Error: " << errorMessage << "\n"; - return false; - } - return true; - }; - if (!connection.empty()) { auto maybeProtoclAndName = validateConnection(connection); if (auto Err = maybeProtoclAndName.takeError()) { @@ -5103,7 +5037,7 @@ int main(int argc, char *argv[]) { std::tie(protocol, name) = *maybeProtoclAndName; Status error; - std::unique_ptr<Socket> listener = Socket::Create(protocol, error); + static std::unique_ptr<Socket> listener = Socket::Create(protocol, error); if (error.Fail()) { llvm::logAllUnhandledErrors(error.takeError(), llvm::errs(), "Failed to create socket listener: "); @@ -5127,17 +5061,18 @@ int main(int argc, char *argv[]) { // address. llvm::outs().flush(); - llvm::DefaultThreadPool pool; + static lldb_private::MainLoop g_loop; + llvm::sys::SetInterruptFunction([]() { + g_loop.AddPendingCallback( + [](lldb_private::MainLoopBase &loop) { loop.RequestTermination(); }); + }); + std::mutex active_dap_sessions_mutext; + std::set<DAP *> active_dap_sessions; unsigned int clientCount = 0; - while (true) { - Socket *accepted_socket; - error = listener->Accept(/*timeout=*/std::nullopt, accepted_socket); - if (error.Fail()) { - llvm::logAllUnhandledErrors(error.takeError(), llvm::errs(), - "accept failed: "); - return EXIT_FAILURE; - } - + auto handle = listener->Accept(g_loop, [=, &active_dap_sessions_mutext, + &active_dap_sessions, &clientCount, + log = log.get()]( + std::unique_ptr<Socket> sock) { std::string name = llvm::formatv("client_{0}", clientCount++).str(); if (log) { auto now = std::chrono::duration<double>( @@ -5146,24 +5081,80 @@ int main(int argc, char *argv[]) { << " client connected: " << name << "\n"; } - // Move the client into the connection pool to unblock accepting the next + // Move the client into a background thread to unblock accepting the next // client. - pool.async( - [=](Socket *accepted_socket, std::ofstream *log) { - StreamDescriptor input = StreamDescriptor::from_socket( - accepted_socket->GetNativeSocket(), false); - // Close the output last for the best chance at error reporting. - StreamDescriptor output = StreamDescriptor::from_socket( - accepted_socket->GetNativeSocket(), true); - RunDAP(program_path, default_repl_mode, pre_init_commands, log, - name, std::move(input), std::move(output)); - }, - accepted_socket, log.get()); + std::thread client([=, &active_dap_sessions_mutext, &active_dap_sessions, + sock = std::move(sock)]() { + llvm::set_thread_name(name + ".runloop"); + StreamDescriptor input = + StreamDescriptor::from_socket(sock->GetNativeSocket(), false); + // Close the output last for the best chance at error reporting. + StreamDescriptor output = + StreamDescriptor::from_socket(sock->GetNativeSocket(), false); + DAP dap = DAP(name, program_path, log, std::move(input), + std::move(output), default_repl_mode, pre_init_commands); + + if (auto Err = dap.ConfigureIO()) { + llvm::logAllUnhandledErrors(std::move(Err), llvm::errs(), + "Failed to configure stdout redirect: "); + return; + } + + RegisterRequestCallbacks(dap); + + { + std::scoped_lock lock(active_dap_sessions_mutext); + active_dap_sessions.insert(&dap); + } + + if (auto Err = dap.Loop()) { + llvm::logAllUnhandledErrors(std::move(Err), llvm::errs(), + "DAP session error: "); + } + + { + std::scoped_lock lock(active_dap_sessions_mutext); + active_dap_sessions.erase(&dap); + } + + if (log) { + auto now = std::chrono::duration<double>( + std::chrono::system_clock::now().time_since_epoch()); + *log << llvm::formatv("{0:f9}", now.count()).str() + << " client closed: " << name << "\n"; + } + }); + client.detach(); + }); + if (auto Err = handle.takeError()) { + llvm::logAllUnhandledErrors(std::move(Err), llvm::errs(), + "Registering accept handler failed: "); + return EXIT_FAILURE; } - pool.wait(); + error = g_loop.Run(); + if (error.Fail()) { + llvm::logAllUnhandledErrors(error.takeError(), llvm::errs(), + "MainLoop failed: "); + return EXIT_FAILURE; + } - return EXIT_SUCCESS; + if (log) + *log << "lldb-dap server shutdown requested, disconnecting remaining " + "clients...\n"; + + bool client_failed = false; + std::scoped_lock lock(active_dap_sessions_mutext); + for (auto *dap : active_dap_sessions) { + auto error = dap->Disconnect(); + if (error.Fail()) { + client_failed = true; + llvm::errs() << "DAP client " << dap->name + << " disconnected failed: " << error.GetCString() << "\n"; + } + } + + return client_failed ? EXIT_FAILURE : EXIT_SUCCESS; } #if defined(_WIN32) @@ -5188,8 +5179,26 @@ int main(int argc, char *argv[]) { StreamDescriptor input = StreamDescriptor::from_file(fileno(stdin), false); StreamDescriptor output = StreamDescriptor::from_file(stdout_fd, false); - bool status = RunDAP(program_path, default_repl_mode, pre_init_commands, - log.get(), "stdin/stdout", std::move(input), - std::move(output), stdout, stderr); - return status ? EXIT_SUCCESS : EXIT_FAILURE; + DAP dap = DAP("stdin/stdout", program_path, log.get(), std::move(input), + std::move(output), default_repl_mode, pre_init_commands); + + // stdout/stderr redirection to the IDE's console + if (auto Err = dap.ConfigureIO(stdout, stderr)) { + llvm::logAllUnhandledErrors(std::move(Err), llvm::errs(), + "Failed to configure stdout redirect: "); + return EXIT_FAILURE; + } + + RegisterRequestCallbacks(dap); + + // used only by TestVSCode_redirection_to_console.py + if (getenv("LLDB_DAP_TEST_STDOUT_STDERR_REDIRECTION") != nullptr) + redirection_test(); + + if (auto Err = dap.Loop()) { + llvm::logAllUnhandledErrors(std::move(Err), llvm::errs(), + "DAP session error: "); + return EXIT_FAILURE; + } + return EXIT_SUCCESS; } >From 784cb25449a9f7dc25be70fe14289a13dd6f0e1d Mon Sep 17 00:00:00 2001 From: John Harrison <harj...@google.com> Date: Thu, 6 Feb 2025 13:16:19 -0800 Subject: [PATCH 4/5] Using a std::condition_variable to notify the main thread after all DAP sessions have disconnected to coordinate shutdowns. --- lldb/tools/lldb-dap/DAP.cpp | 21 ++++++---- lldb/tools/lldb-dap/IOStream.cpp | 5 +-- lldb/tools/lldb-dap/OutputRedirector.cpp | 3 ++ lldb/tools/lldb-dap/lldb-dap.cpp | 52 ++++++++++++++---------- 4 files changed, 48 insertions(+), 33 deletions(-) diff --git a/lldb/tools/lldb-dap/DAP.cpp b/lldb/tools/lldb-dap/DAP.cpp index 4da077e857a4a2f..e7904d9a9c4ac79 100644 --- a/lldb/tools/lldb-dap/DAP.cpp +++ b/lldb/tools/lldb-dap/DAP.cpp @@ -21,6 +21,7 @@ #include "lldb/lldb-defines.h" #include "lldb/lldb-enumerations.h" #include "llvm/ADT/ArrayRef.h" +#include "llvm/ADT/ScopeExit.h" #include "llvm/ADT/StringExtras.h" #include "llvm/ADT/Twine.h" #include "llvm/Support/Error.h" @@ -224,6 +225,15 @@ llvm::Error DAP::ConfigureIO(std::FILE *overrideOut, std::FILE *overrideErr) { void DAP::StopIO() { out.Stop(); err.Stop(); + + if (event_thread.joinable()) { + broadcaster.BroadcastEventByType(eBroadcastBitStopEventThread); + event_thread.join(); + } + if (progress_event_thread.joinable()) { + broadcaster.BroadcastEventByType(eBroadcastBitStopProgressThread); + progress_event_thread.join(); + } } // Send the JSON in "json_str" to the "out" stream. Correctly send the @@ -830,23 +840,16 @@ lldb::SBError DAP::Disconnect(bool terminateDebuggee) { debugger.SetAsync(true); break; } + SendTerminatedEvent(); - if (event_thread.joinable()) { - broadcaster.BroadcastEventByType(eBroadcastBitStopEventThread); - event_thread.join(); - } - if (progress_event_thread.joinable()) { - broadcaster.BroadcastEventByType(eBroadcastBitStopProgressThread); - progress_event_thread.join(); - } - StopIO(); disconnecting = true; return error; } llvm::Error DAP::Loop() { + auto stop_io = llvm::make_scope_exit([this]() { StopIO(); }); while (!disconnecting) { llvm::json::Object object; lldb_dap::PacketStatus status = GetNextObject(object); diff --git a/lldb/tools/lldb-dap/IOStream.cpp b/lldb/tools/lldb-dap/IOStream.cpp index d2e8ec40b0a7b85..7d0f363440f53d6 100644 --- a/lldb/tools/lldb-dap/IOStream.cpp +++ b/lldb/tools/lldb-dap/IOStream.cpp @@ -7,6 +7,8 @@ //===----------------------------------------------------------------------===// #include "IOStream.h" +#include <fstream> +#include <string> #if defined(_WIN32) #include <io.h> @@ -16,9 +18,6 @@ #include <unistd.h> #endif -#include <fstream> -#include <string> - using namespace lldb_dap; StreamDescriptor::StreamDescriptor() = default; diff --git a/lldb/tools/lldb-dap/OutputRedirector.cpp b/lldb/tools/lldb-dap/OutputRedirector.cpp index 7935e17a653bec3..84ee810f1bfa4cc 100644 --- a/lldb/tools/lldb-dap/OutputRedirector.cpp +++ b/lldb/tools/lldb-dap/OutputRedirector.cpp @@ -67,6 +67,9 @@ Error OutputRedirector::RedirectTo(std::function<void(StringRef)> callback) { continue; break; } + // Skip the null byte used to trigger a Stop. + if (bytes_count == 1 && buffer[0] == '\0') + continue; callback(StringRef(buffer, bytes_count)); } diff --git a/lldb/tools/lldb-dap/lldb-dap.cpp b/lldb/tools/lldb-dap/lldb-dap.cpp index 4b9b5fb4310ac58..8fa0bf007794118 100644 --- a/lldb/tools/lldb-dap/lldb-dap.cpp +++ b/lldb/tools/lldb-dap/lldb-dap.cpp @@ -48,6 +48,7 @@ #include "llvm/Support/raw_ostream.h" #include <algorithm> #include <array> +#include <condition_variable> #include <cstdint> #include <cstdio> #include <cstdlib> @@ -5066,12 +5067,13 @@ int main(int argc, char *argv[]) { g_loop.AddPendingCallback( [](lldb_private::MainLoopBase &loop) { loop.RequestTermination(); }); }); - std::mutex active_dap_sessions_mutext; - std::set<DAP *> active_dap_sessions; + std::condition_variable dap_sessions_condition; + std::mutex dap_sessions_mutex; + std::map<Socket *, DAP *> dap_sessions; unsigned int clientCount = 0; - auto handle = listener->Accept(g_loop, [=, &active_dap_sessions_mutext, - &active_dap_sessions, &clientCount, - log = log.get()]( + auto handle = listener->Accept(g_loop, [=, &dap_sessions_condition, + &dap_sessions_mutex, &dap_sessions, + &clientCount, log = log.get()]( std::unique_ptr<Socket> sock) { std::string name = llvm::formatv("client_{0}", clientCount++).str(); if (log) { @@ -5083,8 +5085,8 @@ int main(int argc, char *argv[]) { // Move the client into a background thread to unblock accepting the next // client. - std::thread client([=, &active_dap_sessions_mutext, &active_dap_sessions, - sock = std::move(sock)]() { + std::thread client([=, &dap_sessions_condition, &dap_sessions_mutex, + &dap_sessions, sock = std::move(sock)]() { llvm::set_thread_name(name + ".runloop"); StreamDescriptor input = StreamDescriptor::from_socket(sock->GetNativeSocket(), false); @@ -5103,8 +5105,8 @@ int main(int argc, char *argv[]) { RegisterRequestCallbacks(dap); { - std::scoped_lock lock(active_dap_sessions_mutext); - active_dap_sessions.insert(&dap); + std::scoped_lock lock(dap_sessions_mutex); + dap_sessions[sock.get()] = &dap; } if (auto Err = dap.Loop()) { @@ -5112,17 +5114,16 @@ int main(int argc, char *argv[]) { "DAP session error: "); } - { - std::scoped_lock lock(active_dap_sessions_mutext); - active_dap_sessions.erase(&dap); - } - if (log) { auto now = std::chrono::duration<double>( std::chrono::system_clock::now().time_since_epoch()); *log << llvm::formatv("{0:f9}", now.count()).str() << " client closed: " << name << "\n"; } + + std::unique_lock lock(dap_sessions_mutex); + dap_sessions.erase(sock.get()); + std::notify_all_at_thread_exit(dap_sessions_condition, std::move(lock)); }); client.detach(); }); @@ -5144,16 +5145,25 @@ int main(int argc, char *argv[]) { "clients...\n"; bool client_failed = false; - std::scoped_lock lock(active_dap_sessions_mutext); - for (auto *dap : active_dap_sessions) { - auto error = dap->Disconnect(); - if (error.Fail()) { - client_failed = true; - llvm::errs() << "DAP client " << dap->name - << " disconnected failed: " << error.GetCString() << "\n"; + { + std::scoped_lock lock(dap_sessions_mutex); + for (auto [sock, dap] : dap_sessions) { + auto error = dap->Disconnect(); + if (error.Fail()) { + client_failed = true; + llvm::errs() << "DAP client " << dap->name + << " disconnected failed: " << error.GetCString() + << "\n"; + } + // Close the socket to ensure the DAP::Loop read finishes. + sock->Close(); } } + // Wait for all clients to finish disconnecting. + std::unique_lock lock(dap_sessions_mutex); + dap_sessions_condition.wait(lock, [&] { return dap_sessions.empty(); }); + return client_failed ? EXIT_FAILURE : EXIT_SUCCESS; } >From 625083b9c9ea834f4c475d545afd7c627f5a299e Mon Sep 17 00:00:00 2001 From: John Harrison <harj...@google.com> Date: Fri, 7 Feb 2025 14:40:35 -0800 Subject: [PATCH 5/5] Refactoring to TestDAP_server tests to not create an extra client connection on startup of the binary. --- .../test/tools/lldb-dap/dap_server.py | 90 ++++++++++--------- .../tools/lldb-dap/server/TestDAP_server.py | 25 +++--- 2 files changed, 60 insertions(+), 55 deletions(-) diff --git a/lldb/packages/Python/lldbsuite/test/tools/lldb-dap/dap_server.py b/lldb/packages/Python/lldbsuite/test/tools/lldb-dap/dap_server.py index 6d765e10236733a..68ea2644476c95c 100644 --- a/lldb/packages/Python/lldbsuite/test/tools/lldb-dap/dap_server.py +++ b/lldb/packages/Python/lldbsuite/test/tools/lldb-dap/dap_server.py @@ -1161,52 +1161,15 @@ def __init__( env=None, ): self.process = None + self.connection = None if executable is not None: - adaptor_env = os.environ.copy() - if env is not None: - adaptor_env.update(env) - - if log_file: - adaptor_env["LLDBDAP_LOG"] = log_file - args = [executable] - - if connection is not None: - args.append("--connection") - args.append(connection) - - self.process = subprocess.Popen( - args, - stdin=subprocess.PIPE, - stdout=subprocess.PIPE, - stderr=subprocess.PIPE, - env=adaptor_env, + process, connection = DebugAdaptorServer.launch( + executable=executable, connection=connection, env=env, log_file=log_file ) + self.process = process + self.connection = connection if connection is not None: - # If the process was also launched, parse the connection from the - # resolved connection. For example, if the connection - # `connection://localhost:0` was specified then the OS would pick a - # random port for listening and lldb-dap would print the listening - # port to stdout. - if self.process is not None: - # lldb-dap will print the listening address once the listener is - # made to stdout. The listener is formatted like - # `connection://host:port` or `unix-connection:///path`. - expected_prefix = "Listening for: " - out = self.process.stdout.readline().decode() - if not out.startswith(expected_prefix): - self.process.kill() - raise ValueError( - "lldb-dap failed to print listening address, expected '{}', got '{}'".format( - expected_prefix, out - ) - ) - - # If the listener expanded into multiple addresses, use the first. - connection = ( - out.removeprefix(expected_prefix).rstrip("\r\n").split(",", 1)[0] - ) - scheme, address = connection.split("://") if scheme == "unix-connect": # unix-connect:///path s = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) @@ -1226,6 +1189,49 @@ def __init__( self, self.process.stdout, self.process.stdin, init_commands, log_file ) + @classmethod + def launch(cls, /, executable, env=None, log_file=None, connection=None): + adaptor_env = os.environ.copy() + if env is not None: + adaptor_env.update(env) + + if log_file: + adaptor_env["LLDBDAP_LOG"] = log_file + args = [executable] + + if connection is not None: + args.append("--connection") + args.append(connection) + + process = subprocess.Popen( + args, + stdin=subprocess.PIPE, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + env=adaptor_env, + ) + + if connection is None: + return (process, None) + + # lldb-dap will print the listening address once the listener is + # made to stdout. The listener is formatted like + # `connection://host:port` or `unix-connection:///path`. + expected_prefix = "Listening for: " + out = process.stdout.readline().decode() + if not out.startswith(expected_prefix): + self.process.kill() + raise ValueError( + "lldb-dap failed to print listening address, expected '{}', got '{}'".format( + expected_prefix, out + ) + ) + + # If the listener expanded into multiple addresses, use the first. + connection = out.removeprefix(expected_prefix).rstrip("\r\n").split(",", 1)[0] + + return (process, connection) + def get_pid(self): if self.process: return self.process.pid diff --git a/lldb/test/API/tools/lldb-dap/server/TestDAP_server.py b/lldb/test/API/tools/lldb-dap/server/TestDAP_server.py index 595bb7a0d139212..e78fdceed1bbd68 100644 --- a/lldb/test/API/tools/lldb-dap/server/TestDAP_server.py +++ b/lldb/test/API/tools/lldb-dap/server/TestDAP_server.py @@ -15,19 +15,18 @@ class TestDAP_server(lldbdap_testcase.DAPTestCaseBase): def start_server(self, connection): log_file_path = self.getBuildArtifact("dap.txt") - server = dap_server.DebugAdaptorServer( + (process, connection) = dap_server.DebugAdaptorServer.launch( executable=self.lldbDAPExec, connection=connection, - init_commands=self.setUpCommands(), log_file=log_file_path, ) def cleanup(): - server.terminate() + process.terminate() self.addTearDownHook(cleanup) - return server + return (process, connection) def run_debug_session(self, connection, name): self.dap_server = dap_server.DebugAdaptorServer( @@ -54,9 +53,9 @@ def test_server_port(self): Test launching a binary with a lldb-dap in server mode on a specific port. """ self.build() - server = self.start_server(connection="tcp://localhost:0") - self.run_debug_session(server.connection, "Alice") - self.run_debug_session(server.connection, "Bob") + (_, connection) = self.start_server(connection="tcp://localhost:0") + self.run_debug_session(connection, "Alice") + self.run_debug_session(connection, "Bob") @skipIfWindows def test_server_unix_socket(self): @@ -72,9 +71,9 @@ def cleanup(): self.addTearDownHook(cleanup) self.build() - server = self.start_server(connection="unix://" + name) - self.run_debug_session(server.connection, "Alice") - self.run_debug_session(server.connection, "Bob") + (_, connection) = self.start_server(connection="unix://" + name) + self.run_debug_session(connection, "Alice") + self.run_debug_session(connection, "Bob") @skipIfWindows def test_server_interrupt(self): @@ -82,9 +81,9 @@ def test_server_interrupt(self): Test launching a binary with lldb-dap in server mode and shutting down the server while the debug session is still active. """ self.build() - server = self.start_server(connection="tcp://localhost:0") + (process, connection) = self.start_server(connection="tcp://localhost:0") self.dap_server = dap_server.DebugAdaptorServer( - connection=server.connection, + connection=connection, ) program = self.getBuildArtifact("a.out") source = "main.c" @@ -99,7 +98,7 @@ def test_server_interrupt(self): self.continue_to_next_stop() # Interrupt the server which should disconnect all clients. - server.process.send_signal(signal.SIGINT) + process.send_signal(signal.SIGINT) self.dap_server.wait_for_terminated() self.assertIsNone( _______________________________________________ lldb-commits mailing list lldb-commits@lists.llvm.org https://lists.llvm.org/cgi-bin/mailman/listinfo/lldb-commits