https://github.com/ashgti updated https://github.com/llvm/llvm-project/pull/116392
>From acfdb2da30b7a49711c3d1ec3be3c9282d6c51b4 Mon Sep 17 00:00:00 2001 From: John Harrison <harj...@google.com> Date: Fri, 15 Nov 2024 09:56:43 -0500 Subject: [PATCH 1/2] [lldb-da] Refactoring lldb-dap port listening mode to allow multiple connections. This refactors the port listening mode to allocate a new DAP object for each connection, allowing multiple connections to run concurrently. --- .../Python/lldbsuite/test/lldbtest.py | 8 + .../test/tools/lldb-dap/dap_server.py | 75 +++- .../test/tools/lldb-dap/lldbdap_testcase.py | 32 +- lldb/test/API/tools/lldb-dap/server/Makefile | 3 + .../tools/lldb-dap/server/TestDAP_server.py | 66 ++++ lldb/test/API/tools/lldb-dap/server/main.c | 6 + lldb/tools/lldb-dap/DAP.cpp | 23 +- lldb/tools/lldb-dap/DAP.h | 50 +-- lldb/tools/lldb-dap/Options.td | 9 + lldb/tools/lldb-dap/OutputRedirector.cpp | 7 +- lldb/tools/lldb-dap/OutputRedirector.h | 12 +- lldb/tools/lldb-dap/lldb-dap.cpp | 366 ++++++++++++++---- 12 files changed, 506 insertions(+), 151 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/lldbtest.py b/lldb/packages/Python/lldbsuite/test/lldbtest.py index 8884ef5933ada8..a899854bb5ae14 100644 --- a/lldb/packages/Python/lldbsuite/test/lldbtest.py +++ b/lldb/packages/Python/lldbsuite/test/lldbtest.py @@ -39,6 +39,7 @@ import signal from subprocess import * import sys +import socket import time import traceback @@ -250,6 +251,13 @@ def which(program): return None +def pickrandomport(): + """Returns a random open port.""" + with socket.socket() as sock: + sock.bind(("", 0)) + return sock.getsockname()[1] + + class ValueCheck: def __init__( self, 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 c29992ce9c7848..e4a53fe0d45907 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 @@ -1154,34 +1154,38 @@ class DebugAdaptorServer(DebugCommunication): def __init__( self, executable=None, + launch=True, port=None, + unix_socket=None, init_commands=[], log_file=None, env=None, ): self.process = 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 - self.process = subprocess.Popen( - [executable], - stdin=subprocess.PIPE, - stdout=subprocess.PIPE, - stderr=subprocess.PIPE, - env=adaptor_env, + if launch: + self.process = DebugAdaptorServer.launch( + executable, + port=port, + unix_socket=unix_socket, + log_file=log_file, + env=env, ) - DebugCommunication.__init__( - self, self.process.stdout, self.process.stdin, init_commands, log_file - ) - elif port is not None: + + if port: s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) s.connect(("127.0.0.1", port)) DebugCommunication.__init__( - self, s.makefile("r"), s.makefile("w"), init_commands + self, s.makefile("rb"), s.makefile("wb"), init_commands, log_file + ) + elif unix_socket: + s = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) + s.connect(unix_socket) + DebugCommunication.__init__( + self, s.makefile("rb"), s.makefile("wb"), init_commands, log_file + ) + else: + DebugCommunication.__init__( + self, self.process.stdout, self.process.stdin, init_commands, log_file ) def get_pid(self): @@ -1196,6 +1200,39 @@ def terminate(self): self.process.wait() self.process = None + @classmethod + def launch( + cls, executable: str, /, port=None, unix_socket=None, log_file=None, env=None + ) -> subprocess.Popen: + adaptor_env = os.environ.copy() + if env: + adaptor_env.update(env) + + if log_file: + adaptor_env["LLDBDAP_LOG"] = log_file + + args = [executable] + if port: + args.append("--port") + args.append(str(port)) + elif unix_socket: + args.append("--unix-socket") + args.append(unix_socket) + + proc = subprocess.Popen( + args, + stdin=subprocess.PIPE, + stdout=subprocess.PIPE, + stderr=sys.stdout, + env=adaptor_env, + ) + + if port or unix_socket: + # Wait for the server to startup. + time.sleep(0.1) + + return proc + def attach_options_specified(options): if options.pid is not None: 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 a25466f07fa557..3fcc08e9ff55cb 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 @@ -13,7 +13,7 @@ class DAPTestCaseBase(TestBase): 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, env=None, launch=True, port=None, unix_socket=None): """Create the Visual Studio Code debug adaptor""" self.assertTrue( is_exe(self.lldbDAPExec), "lldb-dap must exist and be executable" @@ -21,14 +21,28 @@ def create_debug_adaptor(self, lldbDAPEnv=None): log_file_path = self.getBuildArtifact("dap.txt") self.dap_server = dap_server.DebugAdaptorServer( executable=self.lldbDAPExec, + launch=launch, + port=port, + unix_socket=unix_socket, init_commands=self.setUpCommands(), log_file=log_file_path, - env=lldbDAPEnv, + env=env, ) - def build_and_create_debug_adaptor(self, lldbDAPEnv=None): + def build_and_create_debug_adaptor( + self, + lldbDAPEnv=None, + lldbDAPLaunch=True, + lldbDAPPort=None, + lldbDAPUnixSocket=None, + ): self.build() - self.create_debug_adaptor(lldbDAPEnv) + self.create_debug_adaptor( + env=lldbDAPEnv, + launch=lldbDAPLaunch, + port=lldbDAPPort, + unix_socket=lldbDAPUnixSocket, + ) def set_source_breakpoints(self, source_path, lines, data=None): """Sets source breakpoints and returns an array of strings containing @@ -475,11 +489,19 @@ def build_and_launch( customThreadFormat=None, launchCommands=None, expectFailure=False, + lldbDAPPort=None, + lldbDAPUnixSocket=None, + lldbDAPLaunch=True, ): """Build the default Makefile target, create the DAP debug adaptor, and launch the process. """ - self.build_and_create_debug_adaptor(lldbDAPEnv) + self.build_and_create_debug_adaptor( + lldbDAPEnv=lldbDAPEnv, + lldbDAPLaunch=lldbDAPLaunch, + lldbDAPPort=lldbDAPPort, + lldbDAPUnixSocket=lldbDAPUnixSocket, + ) self.assertTrue(os.path.exists(program), "executable must exist") return self.launch( 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 00000000000000..10495940055b63 --- /dev/null +++ b/lldb/test/API/tools/lldb-dap/server/Makefile @@ -0,0 +1,3 @@ +C_SOURCES := main.c + +include Makefile.rules 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 00000000000000..46b992a77a4815 --- /dev/null +++ b/lldb/test/API/tools/lldb-dap/server/TestDAP_server.py @@ -0,0 +1,66 @@ +""" +Test lldb-dap server integration. +""" + +import os +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 do_test_server(self, port=None, unix_socket=None): + log_file_path = self.getBuildArtifact("dap.txt") + server = dap_server.DebugAdaptorServer.launch( + self.lldbDAPExec, port=port, unix_socket=unix_socket, log_file=log_file_path + ) + + def cleanup(): + server.terminate() + server.wait() + + self.addTearDownHook(cleanup) + + self.build() + program = self.getBuildArtifact("a.out") + source = "main.c" + breakpoint_line = line_number(source, "// breakpoint") + + # Initial connection over the port. + self.create_debug_adaptor(launch=False, port=port, unix_socket=unix_socket) + self.launch( + program, + disconnectAutomatically=False, + ) + self.set_source_breakpoints(source, [breakpoint_line]) + self.continue_to_next_stop() + self.continue_to_exit() + output = self.get_stdout() + self.assertEquals(output, "hello world!\r\n") + self.dap_server.request_disconnect() + + # Second connection over the port. + self.create_debug_adaptor(launch=False, port=port, unix_socket=unix_socket) + self.launch(program) + self.set_source_breakpoints(source, [breakpoint_line]) + self.continue_to_next_stop() + self.continue_to_exit() + output = self.get_stdout() + self.assertEquals(output, "hello world!\r\n") + + def test_server_port(self): + """ + Test launching a binary with a lldb-dap in server mode on a specific port. + """ + port = pickrandomport() + self.do_test_server(port=port) + + def test_server_unix_socket(self): + """ + Test launching a binary with a lldb-dap in server mode on a unix socket. + """ + dir = tempfile.gettempdir() + self.do_test_server(unix_socket=dir + "/dap-connection-" + str(os.getpid())) 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 00000000000000..9a6326f3b57d45 --- /dev/null +++ b/lldb/test/API/tools/lldb-dap/server/main.c @@ -0,0 +1,6 @@ +#include <stdio.h> + +int main(int argc, char const *argv[]) { + printf("hello world!\n"); // breakpoint 1 + return 0; +} \ No newline at end of file diff --git a/lldb/tools/lldb-dap/DAP.cpp b/lldb/tools/lldb-dap/DAP.cpp index 35250d9eef608a..8998036fbedf6b 100644 --- a/lldb/tools/lldb-dap/DAP.cpp +++ b/lldb/tools/lldb-dap/DAP.cpp @@ -32,10 +32,11 @@ using namespace lldb_dap; namespace lldb_dap { -DAP::DAP(llvm::StringRef path, ReplMode repl_mode) +DAP::DAP(llvm::StringRef path, std::shared_ptr<std::ofstream> log, + ReplMode repl_mode, std::vector<std::string> pre_init_commands) : debug_adaptor_path(path), broadcaster("lldb-dap"), - exception_breakpoints(), focus_tid(LLDB_INVALID_THREAD_ID), - stop_at_entry(false), is_attach(false), + log(log), 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), @@ -43,21 +44,7 @@ DAP::DAP(llvm::StringRef path, ReplMode repl_mode) configuration_done_sent(false), waiting_for_run_in_terminal(false), progress_event_reporter( [&](const ProgressEvent &event) { SendJSON(event.ToJSON()); }), - reverse_request_seq(0), repl_mode(repl_mode) { - const char *log_file_path = getenv("LLDBDAP_LOG"); -#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 - if (log_file_path) - log.reset(new std::ofstream(log_file_path)); -} + reverse_request_seq(0), repl_mode(repl_mode) {} DAP::~DAP() = default; diff --git a/lldb/tools/lldb-dap/DAP.h b/lldb/tools/lldb-dap/DAP.h index ae496236f13369..ec70da67edac14 100644 --- a/lldb/tools/lldb-dap/DAP.h +++ b/lldb/tools/lldb-dap/DAP.h @@ -9,36 +9,33 @@ #ifndef LLDB_TOOLS_LLDB_DAP_DAP_H #define LLDB_TOOLS_LLDB_DAP_DAP_H -#include <cstdio> -#include <iosfwd> -#include <map> -#include <optional> -#include <thread> - -#include "llvm/ADT/DenseMap.h" -#include "llvm/ADT/DenseSet.h" -#include "llvm/ADT/StringMap.h" -#include "llvm/ADT/StringRef.h" -#include "llvm/Support/JSON.h" -#include "llvm/Support/Threading.h" -#include "llvm/Support/raw_ostream.h" - +#include "ExceptionBreakpoint.h" +#include "FunctionBreakpoint.h" +#include "IOStream.h" +#include "InstructionBreakpoint.h" +#include "ProgressEvent.h" +#include "SourceBreakpoint.h" #include "lldb/API/SBAttachInfo.h" #include "lldb/API/SBCommandInterpreter.h" #include "lldb/API/SBCommandReturnObject.h" #include "lldb/API/SBDebugger.h" #include "lldb/API/SBEvent.h" +#include "lldb/API/SBFile.h" #include "lldb/API/SBFormat.h" #include "lldb/API/SBLaunchInfo.h" #include "lldb/API/SBTarget.h" #include "lldb/API/SBThread.h" - -#include "ExceptionBreakpoint.h" -#include "FunctionBreakpoint.h" -#include "IOStream.h" -#include "InstructionBreakpoint.h" -#include "ProgressEvent.h" -#include "SourceBreakpoint.h" +#include "llvm/ADT/DenseMap.h" +#include "llvm/ADT/DenseSet.h" +#include "llvm/ADT/StringMap.h" +#include "llvm/ADT/StringRef.h" +#include "llvm/Support/JSON.h" +#include "llvm/Support/Threading.h" +#include "llvm/Support/raw_ostream.h" +#include <iosfwd> +#include <map> +#include <optional> +#include <thread> #define VARREF_LOCALS (int64_t)1 #define VARREF_GLOBALS (int64_t)2 @@ -140,13 +137,16 @@ struct DAP { llvm::StringRef debug_adaptor_path; InputStream input; OutputStream output; + lldb::SBFile in; + lldb::SBFile out; + lldb::SBFile err; lldb::SBDebugger debugger; lldb::SBTarget target; Variables variables; lldb::SBBroadcaster broadcaster; std::thread event_thread; std::thread progress_event_thread; - std::unique_ptr<std::ofstream> log; + std::shared_ptr<std::ofstream> log; llvm::StringMap<SourceBreakpointMap> source_breakpoints; FunctionBreakpointMap function_breakpoints; InstructionBreakpointMap instruction_breakpoints; @@ -198,10 +198,14 @@ struct DAP { // will contain that expression. std::string last_nonempty_var_expression; - DAP(llvm::StringRef path, ReplMode repl_mode); + DAP(llvm::StringRef path, std::shared_ptr<std::ofstream> log, + ReplMode repl_mode, std::vector<std::string> pre_init_commands); ~DAP(); + + DAP() = delete; DAP(const DAP &rhs) = delete; void operator=(const DAP &rhs) = delete; + ExceptionBreakpoint *GetExceptionBreakpoint(const std::string &filter); ExceptionBreakpoint *GetExceptionBreakpoint(const lldb::break_id_t bp_id); diff --git a/lldb/tools/lldb-dap/Options.td b/lldb/tools/lldb-dap/Options.td index d7b4a065abec01..dfca79b2884ac8 100644 --- a/lldb/tools/lldb-dap/Options.td +++ b/lldb/tools/lldb-dap/Options.td @@ -22,8 +22,17 @@ def port: S<"port">, HelpText<"Communicate with the lldb-dap tool over the defined port.">; def: Separate<["-"], "p">, Alias<port>, + MetaVarName<"<port>">, HelpText<"Alias for --port">; +def unix_socket: S<"unix-socket">, + MetaVarName<"<path>">, + HelpText<"Communicate with the lldb-dap tool over the unix socket or named pipe.">; +def: Separate<["-"], "u">, + Alias<unix_socket>, + MetaVarName<"<path>">, + HelpText<"Alias for --unix_socket">; + def launch_target: S<"launch-target">, MetaVarName<"<target>">, HelpText<"Launch a target for the launchInTerminal request. Any argument " diff --git a/lldb/tools/lldb-dap/OutputRedirector.cpp b/lldb/tools/lldb-dap/OutputRedirector.cpp index 2c2f49569869b4..0b725e1901b9fd 100644 --- a/lldb/tools/lldb-dap/OutputRedirector.cpp +++ b/lldb/tools/lldb-dap/OutputRedirector.cpp @@ -21,7 +21,8 @@ using namespace llvm; namespace lldb_dap { -Error RedirectFd(int fd, std::function<void(llvm::StringRef)> callback) { +Expected<int> RedirectFd(int fd, + std::function<void(llvm::StringRef)> callback) { int new_fd[2]; #if defined(_WIN32) if (_pipe(new_fd, 4096, O_TEXT) == -1) { @@ -34,7 +35,7 @@ Error RedirectFd(int fd, std::function<void(llvm::StringRef)> callback) { strerror(error)); } - if (dup2(new_fd[1], fd) == -1) { + if (fd != -1 && dup2(new_fd[1], fd) == -1) { int error = errno; return createStringError(inconvertibleErrorCode(), "Couldn't override the fd %d. %s", fd, @@ -57,7 +58,7 @@ Error RedirectFd(int fd, std::function<void(llvm::StringRef)> callback) { } }); t.detach(); - return Error::success(); + return new_fd[1]; } } // namespace lldb_dap diff --git a/lldb/tools/lldb-dap/OutputRedirector.h b/lldb/tools/lldb-dap/OutputRedirector.h index e26d1648b104f9..418b8bac102c7f 100644 --- a/lldb/tools/lldb-dap/OutputRedirector.h +++ b/lldb/tools/lldb-dap/OutputRedirector.h @@ -16,10 +16,16 @@ namespace lldb_dap { /// Redirects the output of a given file descriptor to a callback. /// +/// \param[in] fd +/// Either -1 or the fd duplicate into the new handle. +/// +/// \param[in] callback +/// A callback invoked each time the file is written. +/// /// \return -/// \a Error::success if the redirection was set up correctly, or an error -/// otherwise. -llvm::Error RedirectFd(int fd, std::function<void(llvm::StringRef)> callback); +/// A new file handle for the output. +llvm::Expected<int> RedirectFd(int fd, + std::function<void(llvm::StringRef)> callback); } // namespace lldb_dap diff --git a/lldb/tools/lldb-dap/lldb-dap.cpp b/lldb/tools/lldb-dap/lldb-dap.cpp index 3bfc578806021e..51197587ffccec 100644 --- a/lldb/tools/lldb-dap/lldb-dap.cpp +++ b/lldb/tools/lldb-dap/lldb-dap.cpp @@ -14,6 +14,7 @@ #include "RunInTerminal.h" #include "Watchpoint.h" #include "lldb/API/SBDeclaration.h" +#include "lldb/API/SBFile.h" #include "lldb/API/SBInstruction.h" #include "lldb/API/SBListener.h" #include "lldb/API/SBMemoryRegionInfo.h" @@ -66,6 +67,7 @@ #else #include <netinet/in.h> #include <sys/socket.h> +#include <sys/un.h> #include <unistd.h> #endif @@ -116,6 +118,8 @@ enum LaunchMethod { Launch, Attach, AttachForSuspendedLaunch }; /// Page size used for reporting addtional frames in the 'stackTrace' request. constexpr int StackPageSize = 20; +void RegisterRequestCallbacks(DAP &dap); + /// Prints a welcome message on the editor if the preprocessor variable /// LLDB_DAP_WELCOME_MESSAGE is defined. static void PrintWelcomeMessage(DAP &dap) { @@ -137,42 +141,232 @@ lldb::SBValueList *GetTopLevelScope(DAP &dap, int64_t variablesReference) { } } -SOCKET AcceptConnection(DAP &dap, int portno) { - // Accept a socket connection from any host on "portno". - SOCKET newsockfd = -1; - struct sockaddr_in serv_addr, cli_addr; +/// Redirect stdout and stderr fo the IDE's console output. +/// +/// Errors in this operation will be printed to the log file and the IDE's +/// console output as well. +/// +/// \return +/// A fd pointing to the original stdout. +void SetupRedirection(DAP &dap, int stdoutfd = -1, int stderrfd = -1) { + auto output_callback_stderr = [&dap](llvm::StringRef data) { + dap.SendOutput(OutputType::Stderr, data); + }; + auto output_callback_stdout = [&dap](llvm::StringRef data) { + dap.SendOutput(OutputType::Stdout, data); + }; + + llvm::Expected<int> new_stdout_fd = + RedirectFd(stdoutfd, output_callback_stdout); + if (auto err = new_stdout_fd.takeError()) { + std::string error_message = llvm::toString(std::move(err)); + if (dap.log) + *dap.log << error_message << std::endl; + output_callback_stderr(error_message); + } + dap.out = lldb::SBFile(new_stdout_fd.get(), "w", false); + + llvm::Expected<int> new_stderr_fd = + RedirectFd(stderrfd, output_callback_stderr); + if (auto err = new_stderr_fd.takeError()) { + std::string error_message = llvm::toString(std::move(err)); + if (dap.log) + *dap.log << error_message << std::endl; + output_callback_stderr(error_message); + } + dap.err = lldb::SBFile(new_stderr_fd.get(), "w", false); +} + +void HandleClient(int clientfd, llvm::StringRef program_path, + const std::vector<std::string> &pre_init_commands, + std::shared_ptr<std::ofstream> log, + ReplMode default_repl_mode) { + if (log) + *log << "client[" << clientfd << "] connected\n"; + DAP dap = DAP(program_path, log, default_repl_mode, pre_init_commands); + dap.debug_adaptor_path = program_path; + + SetupRedirection(dap); + RegisterRequestCallbacks(dap); + + dap.input.descriptor = StreamDescriptor::from_socket(clientfd, false); + dap.output.descriptor = StreamDescriptor::from_socket(clientfd, false); + + for (const std::string &arg : pre_init_commands) { + dap.pre_init_commands.push_back(arg); + } + + if (auto Err = dap.Loop()) { + if (log) + *log << "Transport Error: " << llvm::toString(std::move(Err)) << "\n"; + } + + if (log) + *log << "client[" << clientfd << "] connection closed\n"; +#if defined(_WIN32) + closesocket(clientfd); +#else + close(clientfd); +#endif +} + +std::error_code getLastSocketErrorCode() { +#ifdef _WIN32 + return std::error_code(::WSAGetLastError(), std::system_category()); +#else + return llvm::errnoAsErrorCode(); +#endif +} + +llvm::Expected<int> getSocketFD(llvm::StringRef path) { + if (llvm::sys::fs::exists(path) && (::remove(path.str().c_str()) == -1)) { + return llvm::make_error<llvm::StringError>(getLastSocketErrorCode(), + "Remove existing socket failed"); + } + + SOCKET sockfd = socket(AF_UNIX, SOCK_STREAM, 0); + if (sockfd == -1) { + return llvm::make_error<llvm::StringError>(getLastSocketErrorCode(), + "Create socket failed"); + } + + struct sockaddr_un addr; + bzero(&addr, sizeof(addr)); + addr.sun_family = AF_UNIX; + strncpy(addr.sun_path, path.str().c_str(), sizeof(addr.sun_path) - 1); + + if (::bind(sockfd, (struct sockaddr *)&addr, sizeof(addr)) == -1) { +#if defined(_WIN32) + closesocket(sockfd); +#else + close(sockfd); +#endif + return llvm::make_error<llvm::StringError>(getLastSocketErrorCode(), + "Socket bind() failed"); + } + + if (listen(sockfd, llvm::hardware_concurrency().compute_thread_count()) < 0) { +#if defined(_WIN32) + closesocket(sockfd); +#else + close(sockfd); +#endif + return llvm::make_error<llvm::StringError>(getLastSocketErrorCode(), + "Socket listen() failed"); + } + + return sockfd; +} + +llvm::Expected<int> getSocketFD(int portno) { SOCKET sockfd = socket(AF_INET, SOCK_STREAM, 0); if (sockfd < 0) { - if (dap.log) - *dap.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 (dap.log) - *dap.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 (dap.log) - *dap.log << "error: accept (" << strerror(errno) << ")" << std::endl; - } + return llvm::make_error<llvm::StringError>(getLastSocketErrorCode(), + "Create socket failed"); + } + + struct sockaddr_in serv_addr; + bzero(&serv_addr, 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 defined(_WIN32) closesocket(sockfd); #else close(sockfd); #endif + return llvm::make_error<llvm::StringError>(getLastSocketErrorCode(), + "Socket bind() failed"); } - return newsockfd; + + if (listen(sockfd, llvm::hardware_concurrency().compute_thread_count()) < 0) { +#if defined(_WIN32) + closesocket(sockfd); +#else + close(sockfd); +#endif + return llvm::make_error<llvm::StringError>(getLastSocketErrorCode(), + "Socket listen() failed"); + } + + return sockfd; +} + +int AcceptConnection(llvm::StringRef program_path, + const std::vector<std::string> &pre_init_commands, + std::shared_ptr<std::ofstream> log, + ReplMode default_repl_mode, + llvm::StringRef unix_socket_path) { + auto listening = getSocketFD(unix_socket_path); + if (auto E = listening.takeError()) { + llvm::errs() << "Listening on " << unix_socket_path + << " failed: " << llvm::toString(std::move(E)) << "\n"; + return EXIT_FAILURE; + } + + while (true) { + struct sockaddr_un cli_addr; + bzero(&cli_addr, sizeof(struct sockaddr_un)); + socklen_t clilen = sizeof(cli_addr); + SOCKET clientfd = + llvm::sys::RetryAfterSignal(static_cast<SOCKET>(-1), accept, *listening, + (struct sockaddr *)&cli_addr, &clilen); + if (clientfd < 0) { + llvm::errs() << "Client accept failed: " + << getLastSocketErrorCode().message() << "\n"; + return EXIT_FAILURE; + } + + std::thread t(HandleClient, clientfd, program_path, pre_init_commands, log, + default_repl_mode); + t.detach(); + } + +#if defined(_WIN32) + closesocket(*listening); +#else + close(*listening); +#endif + return 0; +} + +int AcceptConnection(llvm::StringRef program_path, + const std::vector<std::string> &pre_init_commands, + std::shared_ptr<std::ofstream> log, + ReplMode default_repl_mode, int portno) { + auto listening = getSocketFD(portno); + if (auto E = listening.takeError()) { + llvm::errs() << "Listening on " << portno + << " failed: " << llvm::toString(std::move(E)) << "\n"; + return EXIT_FAILURE; + } + + while (true) { + struct sockaddr_in cli_addr; + bzero(&cli_addr, sizeof(struct sockaddr_in)); + socklen_t clilen = sizeof(cli_addr); + SOCKET clientfd = + llvm::sys::RetryAfterSignal(static_cast<SOCKET>(-1), accept, *listening, + (struct sockaddr *)&cli_addr, &clilen); + if (clientfd < 0) { + llvm::errs() << "Client accept failed: " + << getLastSocketErrorCode().message() << "\n"; + return EXIT_FAILURE; + } + + std::thread t(HandleClient, clientfd, program_path, pre_init_commands, log, + default_repl_mode); + t.detach(); + } + +#if defined(_WIN32) + closesocket(*listening); +#else + close(*listening); +#endif + return 0; } std::vector<const char *> MakeArgv(const llvm::ArrayRef<std::string> &strs) { @@ -1868,7 +2062,21 @@ void request_initialize(DAP &dap, const llvm::json::Object &request) { // which may affect the outcome of tests. bool source_init_file = GetBoolean(arguments, "sourceInitFile", true); - dap.debugger = lldb::SBDebugger::Create(source_init_file); + lldb::SBDebugger debugger = lldb::SBDebugger::Create(false); + debugger.SetInputFile(dap.in); + debugger.SetOutputFile(dap.out); + debugger.SetErrorFile(dap.err); + + lldb::SBCommandInterpreter interp = debugger.GetCommandInterpreter(); + + if (source_init_file) { + lldb::SBCommandReturnObject result; + interp.SourceInitFileInCurrentWorkingDirectory(result); + interp.SourceInitFileInHomeDirectory(result, false); + } + + dap.debugger = debugger; + if (llvm::Error err = dap.RunPreInitCommands()) { response["success"] = false; EmplaceSafeString(response, "message", llvm::toString(std::move(err))); @@ -4907,38 +5115,6 @@ static void redirection_test() { fflush(stderr); } -/// Redirect stdout and stderr fo the IDE's console output. -/// -/// Errors in this operation will be printed to the log file and the IDE's -/// console output as well. -/// -/// \return -/// A fd pointing to the original stdout. -static int SetupStdoutStderrRedirection(DAP &dap) { - int stdoutfd = fileno(stdout); - int new_stdout_fd = dup(stdoutfd); - auto output_callback_stderr = [&dap](llvm::StringRef data) { - dap.SendOutput(OutputType::Stderr, data); - }; - auto output_callback_stdout = [&dap](llvm::StringRef data) { - dap.SendOutput(OutputType::Stdout, data); - }; - if (llvm::Error err = RedirectFd(stdoutfd, output_callback_stdout)) { - std::string error_message = llvm::toString(std::move(err)); - if (dap.log) - *dap.log << error_message << std::endl; - output_callback_stderr(error_message); - } - if (llvm::Error err = RedirectFd(fileno(stderr), output_callback_stderr)) { - std::string error_message = llvm::toString(std::move(err)); - if (dap.log) - *dap.log << error_message << std::endl; - output_callback_stderr(error_message); - } - - return new_stdout_fd; -} - int main(int argc, char *argv[]) { llvm::InitLLVM IL(argc, argv, /*InstallPipeSignalExitHandler=*/false); #if !defined(__APPLE__) @@ -5020,6 +5196,20 @@ int main(int argc, char *argv[]) { } } + std::string unix_socket_path; + if (auto *arg = input_args.getLastArg(OPT_unix_socket)) { + const auto *path = arg->getValue(); + unix_socket_path.assign(path); + } + + const char *log_file_path = getenv("LLDBDAP_LOG"); + std::shared_ptr<std::ofstream> log; + if (log_file_path) + log = std::make_shared<std::ofstream>(log_file_path); + + const auto pre_init_commands = + input_args.getAllArgValues(OPT_pre_init_command); + #if !defined(_WIN32) if (input_args.hasArg(OPT_wait_for_debugger)) { printf("Paused waiting for debugger to attach (pid = %i)...\n", getpid()); @@ -5028,36 +5218,52 @@ int main(int argc, char *argv[]) { #endif // Initialize LLDB first before we do anything. - lldb::SBDebugger::Initialize(); + lldb::SBError error = lldb::SBDebugger::InitializeWithErrorHandling(); + if (error.Fail()) { + llvm::errs() << "Failed to initialize LLDB: " << error.GetCString() << "\n"; + return EXIT_FAILURE; + } // Terminate the debugger before the C++ destructor chain kicks in. auto terminate_debugger = llvm::make_scope_exit([] { lldb::SBDebugger::Terminate(); }); - DAP dap = DAP(program_path.str(), default_repl_mode); + if (portno != -1) { + llvm::errs() << llvm::format("Listening on port %i...\n", portno); + return AcceptConnection(program_path.str(), pre_init_commands, log, + default_repl_mode, portno); + } + + if (!unix_socket_path.empty()) { + return AcceptConnection(program_path.str(), pre_init_commands, log, + default_repl_mode, unix_socket_path); + } + + DAP dap = DAP(program_path.str(), log, default_repl_mode, pre_init_commands); RegisterRequestCallbacks(dap); +#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 + // stdout/stderr redirection to the IDE's console - int new_stdout_fd = SetupStdoutStderrRedirection(dap); + int new_stdout_fd = dup(fileno(stdout)); + SetupRedirection(dap, fileno(stdout), fileno(stderr)); - if (portno != -1) { - printf("Listening on port %i...\n", portno); - SOCKET socket_fd = AcceptConnection(dap, portno); - if (socket_fd >= 0) { - dap.input.descriptor = StreamDescriptor::from_socket(socket_fd, true); - dap.output.descriptor = StreamDescriptor::from_socket(socket_fd, false); - } else { - return EXIT_FAILURE; - } - } else { - dap.input.descriptor = StreamDescriptor::from_file(fileno(stdin), false); - dap.output.descriptor = StreamDescriptor::from_file(new_stdout_fd, false); + dap.input.descriptor = StreamDescriptor::from_file(fileno(stdin), false); + dap.output.descriptor = StreamDescriptor::from_file(new_stdout_fd, false); - /// used only by TestVSCode_redirection_to_console.py - if (getenv("LLDB_DAP_TEST_STDOUT_STDERR_REDIRECTION") != nullptr) - redirection_test(); - } + /// used only by TestVSCode_redirection_to_console.py + if (getenv("LLDB_DAP_TEST_STDOUT_STDERR_REDIRECTION") != nullptr) + redirection_test(); for (const std::string &arg : input_args.getAllArgValues(OPT_pre_init_command)) { >From a32f332007ee3ec735f7f72d9601e571567c164a Mon Sep 17 00:00:00 2001 From: John Harrison <harj...@google.com> Date: Fri, 22 Nov 2024 16:02:43 -0800 Subject: [PATCH 2/2] Working to address some of the basic architectural issues with the socket listeners. * Moved the socket handling logic into lldb/tools/lldb-dap/Socket.{h,cpp} * Reworked the unit tests to read the socket information from the DAP process, this allows for lldb-dap to start with a connection like 'tcp://localhost:0' and pick a random port for the test. The output will be printed to stdout, listing all the addresses that were resolved from the connection parameter. --- .../test/tools/lldb-dap/dap_server.py | 79 ++- .../test/tools/lldb-dap/lldbdap_testcase.py | 19 +- .../tools/lldb-dap/server/TestDAP_server.py | 17 +- lldb/tools/lldb-dap/CMakeLists.txt | 5 +- lldb/tools/lldb-dap/DAP.cpp | 50 +- lldb/tools/lldb-dap/DAP.h | 21 +- lldb/tools/lldb-dap/IOStream.cpp | 10 +- lldb/tools/lldb-dap/IOStream.h | 8 +- lldb/tools/lldb-dap/Options.td | 22 +- lldb/tools/lldb-dap/Socket.cpp | 500 ++++++++++++++++++ lldb/tools/lldb-dap/Socket.h | 182 +++++++ lldb/tools/lldb-dap/lldb-dap.cpp | 370 ++++--------- 12 files changed, 923 insertions(+), 360 deletions(-) create mode 100644 lldb/tools/lldb-dap/Socket.cpp create mode 100644 lldb/tools/lldb-dap/Socket.h 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 e4a53fe0d45907..8a71b9e709f138 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 @@ -9,6 +9,7 @@ import string import subprocess import sys +import selectors import threading import time @@ -1150,36 +1151,37 @@ def request_setInstructionBreakpoints(self, memory_reference=[]): } return self.send_recv(command_dict) + class DebugAdaptorServer(DebugCommunication): def __init__( self, executable=None, launch=True, - port=None, - unix_socket=None, + connection=None, init_commands=[], log_file=None, env=None, ): self.process = None if launch: - self.process = DebugAdaptorServer.launch( + self.process, connection = DebugAdaptorServer.launch( executable, - port=port, - unix_socket=unix_socket, + connection=connection, log_file=log_file, env=env, ) - if port: - s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) - s.connect(("127.0.0.1", port)) - DebugCommunication.__init__( - self, s.makefile("rb"), s.makefile("wb"), init_commands, log_file - ) - elif unix_socket: - s = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) - s.connect(unix_socket) + if connection: + print("attempting connection", connection) + if connection.startswith("unix://"): # unix:///path + s = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) + s.connect(connection.removeprefix("unix://")) + elif connection.startswith("tcp://"): # tcp://host:port + host, port = connection.removeprefix("tcp://").split(":", 1) + # create_connection with try both ipv4 and ipv6. + s = socket.create_connection((host, int(port))) + else: + raise ValueError("invalid connection: {}".format(connection)) DebugCommunication.__init__( self, s.makefile("rb"), s.makefile("wb"), init_commands, log_file ) @@ -1202,8 +1204,8 @@ def terminate(self): @classmethod def launch( - cls, executable: str, /, port=None, unix_socket=None, log_file=None, env=None - ) -> subprocess.Popen: + cls, executable: str, /, connection=None, log_file=None, env=None + ) -> tuple[subprocess.Popen, str]: adaptor_env = os.environ.copy() if env: adaptor_env.update(env) @@ -1211,13 +1213,13 @@ def launch( if log_file: adaptor_env["LLDBDAP_LOG"] = log_file + if os.uname().sysname == "Darwin": + adaptor_env["NSUnbufferedIO"] = "YES" + args = [executable] - if port: - args.append("--port") - args.append(str(port)) - elif unix_socket: - args.append("--unix-socket") - args.append(unix_socket) + if connection: + args.append("--connection") + args.append(connection) proc = subprocess.Popen( args, @@ -1227,11 +1229,34 @@ def launch( env=adaptor_env, ) - if port or unix_socket: - # Wait for the server to startup. - time.sleep(0.1) - - return proc + if connection: + # If a conneciton is specified, lldb-dap will print the listening + # address once the listener is made to stdout. The listener is + # formatted like `tcp://host:port` or `unix:///path`. + with selectors.DefaultSelector() as sel: + print("Reading stdout for the listening connection") + os.set_blocking(proc.stdout.fileno(), False) + stdout_key = sel.register(proc.stdout, selectors.EVENT_READ) + rdy_fds = sel.select(timeout=10.0) + for key, _ in rdy_fds: + if key != stdout_key: + continue + + outs = proc.stdout.read(1024).decode() + os.set_blocking(proc.stdout.fileno(), True) + for line in outs.split("\n"): + if not line.startswith("Listening for: "): + continue + # If the listener expanded into multiple addresses, use the first. + connection = line.removeprefix("Listening for: ").split(",")[0] + print("") + return proc, connection + proc.kill() + raise ValueError( + "lldb-dap started with a connection but failed to write the listening address to stdout." + ) + + return proc, None def attach_options_specified(options): 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 3fcc08e9ff55cb..6b1216c8837f7c 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 @@ -10,10 +10,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, env=None, launch=True, port=None, unix_socket=None): + def create_debug_adaptor(self, env=None, launch=True, connection=None): """Create the Visual Studio Code debug adaptor""" self.assertTrue( is_exe(self.lldbDAPExec), "lldb-dap must exist and be executable" @@ -22,8 +22,7 @@ def create_debug_adaptor(self, env=None, launch=True, port=None, unix_socket=Non self.dap_server = dap_server.DebugAdaptorServer( executable=self.lldbDAPExec, launch=launch, - port=port, - unix_socket=unix_socket, + connection=connection, init_commands=self.setUpCommands(), log_file=log_file_path, env=env, @@ -33,15 +32,13 @@ def build_and_create_debug_adaptor( self, lldbDAPEnv=None, lldbDAPLaunch=True, - lldbDAPPort=None, - lldbDAPUnixSocket=None, + lldbDAPConnection=None, ): self.build() self.create_debug_adaptor( env=lldbDAPEnv, launch=lldbDAPLaunch, - port=lldbDAPPort, - unix_socket=lldbDAPUnixSocket, + connection=lldbDAPConnection, ) def set_source_breakpoints(self, source_path, lines, data=None): @@ -489,9 +486,8 @@ def build_and_launch( customThreadFormat=None, launchCommands=None, expectFailure=False, - lldbDAPPort=None, - lldbDAPUnixSocket=None, lldbDAPLaunch=True, + lldbDAPConnection=None, ): """Build the default Makefile target, create the DAP debug adaptor, and launch the process. @@ -499,8 +495,7 @@ def build_and_launch( self.build_and_create_debug_adaptor( lldbDAPEnv=lldbDAPEnv, lldbDAPLaunch=lldbDAPLaunch, - lldbDAPPort=lldbDAPPort, - lldbDAPUnixSocket=lldbDAPUnixSocket, + lldbDAPConnection=lldbDAPConnection, ) self.assertTrue(os.path.exists(program), "executable must exist") 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 46b992a77a4815..27bd583d4f6290 100644 --- a/lldb/test/API/tools/lldb-dap/server/TestDAP_server.py +++ b/lldb/test/API/tools/lldb-dap/server/TestDAP_server.py @@ -12,10 +12,10 @@ class TestDAP_server(lldbdap_testcase.DAPTestCaseBase): - def do_test_server(self, port=None, unix_socket=None): + def do_test_server(self, connection): log_file_path = self.getBuildArtifact("dap.txt") - server = dap_server.DebugAdaptorServer.launch( - self.lldbDAPExec, port=port, unix_socket=unix_socket, log_file=log_file_path + server, connection = dap_server.DebugAdaptorServer.launch( + self.lldbDAPExec, connection, log_file=log_file_path ) def cleanup(): @@ -30,7 +30,7 @@ def cleanup(): breakpoint_line = line_number(source, "// breakpoint") # Initial connection over the port. - self.create_debug_adaptor(launch=False, port=port, unix_socket=unix_socket) + self.create_debug_adaptor(launch=False, connection=connection) self.launch( program, disconnectAutomatically=False, @@ -43,7 +43,7 @@ def cleanup(): self.dap_server.request_disconnect() # Second connection over the port. - self.create_debug_adaptor(launch=False, port=port, unix_socket=unix_socket) + self.create_debug_adaptor(launch=False, connection=connection) self.launch(program) self.set_source_breakpoints(source, [breakpoint_line]) self.continue_to_next_stop() @@ -55,12 +55,13 @@ def test_server_port(self): """ Test launching a binary with a lldb-dap in server mode on a specific port. """ - port = pickrandomport() - self.do_test_server(port=port) + self.do_test_server(connection="tcp://localhost:0") def test_server_unix_socket(self): """ Test launching a binary with a lldb-dap in server mode on a unix socket. """ dir = tempfile.gettempdir() - self.do_test_server(unix_socket=dir + "/dap-connection-" + str(os.getpid())) + self.do_test_server( + connection="unix://" + dir + "/dap-connection-" + str(os.getpid()) + ) diff --git a/lldb/tools/lldb-dap/CMakeLists.txt b/lldb/tools/lldb-dap/CMakeLists.txt index d68098bf7b3266..f81be385c62aab 100644 --- a/lldb/tools/lldb-dap/CMakeLists.txt +++ b/lldb/tools/lldb-dap/CMakeLists.txt @@ -26,19 +26,20 @@ add_lldb_tool(lldb-dap lldb-dap.cpp Breakpoint.cpp BreakpointBase.cpp + DAP.cpp ExceptionBreakpoint.cpp FifoFiles.cpp FunctionBreakpoint.cpp + InstructionBreakpoint.cpp IOStream.cpp JSONUtils.cpp LLDBUtils.cpp OutputRedirector.cpp ProgressEvent.cpp RunInTerminal.cpp + Socket.cpp SourceBreakpoint.cpp - DAP.cpp Watchpoint.cpp - InstructionBreakpoint.cpp LINK_LIBS liblldb diff --git a/lldb/tools/lldb-dap/DAP.cpp b/lldb/tools/lldb-dap/DAP.cpp index 8998036fbedf6b..29e200f2475eb8 100644 --- a/lldb/tools/lldb-dap/DAP.cpp +++ b/lldb/tools/lldb-dap/DAP.cpp @@ -14,11 +14,14 @@ #include "DAP.h" #include "JSONUtils.h" #include "LLDBUtils.h" +#include "OutputRedirector.h" #include "lldb/API/SBCommandInterpreter.h" #include "lldb/API/SBLanguageRuntime.h" #include "lldb/API/SBListener.h" #include "lldb/API/SBStream.h" #include "llvm/ADT/StringExtras.h" +#include "llvm/ADT/StringRef.h" +#include "llvm/Support/Error.h" #include "llvm/Support/FormatVariadic.h" #if defined(_WIN32) @@ -32,10 +35,10 @@ using namespace lldb_dap; namespace lldb_dap { -DAP::DAP(llvm::StringRef path, std::shared_ptr<std::ofstream> log, +DAP::DAP(llvm::StringRef path, std::shared_ptr<llvm::raw_ostream> log, ReplMode repl_mode, std::vector<std::string> pre_init_commands) - : debug_adaptor_path(path), broadcaster("lldb-dap"), - log(log), exception_breakpoints(), pre_init_commands(pre_init_commands), + : debug_adaptor_path(path), broadcaster("lldb-dap"), log(log), + 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), @@ -48,6 +51,28 @@ DAP::DAP(llvm::StringRef path, std::shared_ptr<std::ofstream> log, DAP::~DAP() = default; +llvm::Error DAP::ConfigureIO(int out_fd, int err_fd) { + llvm::Expected<int> new_stdout_fd = + RedirectFd(out_fd, [this](llvm::StringRef data) { + SendOutput(OutputType::Stdout, data); + }); + if (auto Err = new_stdout_fd.takeError()) { + return Err; + } + llvm::Expected<int> new_stderr_fd = + RedirectFd(err_fd, [this](llvm::StringRef data) { + SendOutput(OutputType::Stderr, data); + }); + if (auto Err = new_stderr_fd.takeError()) { + return Err; + } + + out = lldb::SBFile(new_stdout_fd.get(), "w", false); + err = lldb::SBFile(new_stderr_fd.get(), "w", false); + + return llvm::Error::success(); +} + /// Return string with first character capitalized. static std::string capitalize(llvm::StringRef str) { if (str.empty()) @@ -183,9 +208,9 @@ 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} <-- ", now.count()).str() << "\n" << "Content-Length: " << json_str.size() << "\r\n\r\n" - << llvm::formatv("{0:2}", json).str() << std::endl; + << llvm::formatv("{0:2}", json).str() << "\n"; } } @@ -213,8 +238,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 - << "Content-Length: " << length << "\r\n\r\n"; + *log << llvm::formatv("{0:f9} --> ", now.count()).str() + << "\nContent-Length: " << length << "\r\n\r\n"; } return json_str; } @@ -654,20 +679,20 @@ PacketStatus DAP::GetNextObject(llvm::json::Object &object) { std::string error_str; llvm::raw_string_ostream strm(error_str); strm << error; - *log << "error: failed to parse JSON: " << error_str << std::endl - << json << std::endl; + *log << "error: failed to parse JSON: " << error_str << "\n" + << json << "\n"; } return PacketStatus::JSONMalformed; } if (log) { - *log << llvm::formatv("{0:2}", *json_value).str() << std::endl; + *log << llvm::formatv("{0:2}", *json_value).str() << "\n"; } llvm::json::Object *object_ptr = json_value->getAsObject(); if (!object_ptr) { if (log) - *log << "error: json packet isn't a object" << std::endl; + *log << "error: json packet isn't a object\n"; return PacketStatus::JSONNotObject; } object = *object_ptr; @@ -681,8 +706,7 @@ bool DAP::HandleObject(const llvm::json::Object &object) { auto handler_pos = request_handlers.find(command); if (handler_pos == request_handlers.end()) { if (log) - *log << "error: unhandled command \"" << command.data() << "\"" - << std::endl; + *log << "error: unhandled command \"" << command.data() << "\"\n"; return false; // Fail } diff --git a/lldb/tools/lldb-dap/DAP.h b/lldb/tools/lldb-dap/DAP.h index ec70da67edac14..8f847c1fdffbe4 100644 --- a/lldb/tools/lldb-dap/DAP.h +++ b/lldb/tools/lldb-dap/DAP.h @@ -146,7 +146,7 @@ struct DAP { lldb::SBBroadcaster broadcaster; std::thread event_thread; std::thread progress_event_thread; - std::shared_ptr<std::ofstream> log; + std::shared_ptr<llvm::raw_ostream> log; llvm::StringMap<SourceBreakpointMap> source_breakpoints; FunctionBreakpointMap function_breakpoints; InstructionBreakpointMap instruction_breakpoints; @@ -198,7 +198,7 @@ struct DAP { // will contain that expression. std::string last_nonempty_var_expression; - DAP(llvm::StringRef path, std::shared_ptr<std::ofstream> log, + DAP(llvm::StringRef path, std::shared_ptr<llvm::raw_ostream> log, ReplMode repl_mode, std::vector<std::string> pre_init_commands); ~DAP(); @@ -206,6 +206,23 @@ struct DAP { DAP(const DAP &rhs) = delete; void operator=(const DAP &rhs) = delete; + void disconnect(); + + /// Configures the DAP session stdout and stderr to redirect to the DAP + /// connection. + /// + /// Errors in this operation will be printed to the log file and the IDE's + /// console output as well. + /// + /// \param[in] out_fd + /// If not -1, uses the given file descriptor as stdout. + /// + /// \param[in] err_fd + /// If not -1, uses the given file descriptor as stderr. + /// + /// \return An error indiciating if the configuration was applied. + llvm::Error ConfigureIO(int out_fd = -1, int err_fd = -1); + ExceptionBreakpoint *GetExceptionBreakpoint(const std::string &filter); ExceptionBreakpoint *GetExceptionBreakpoint(const lldb::break_id_t bp_id); diff --git a/lldb/tools/lldb-dap/IOStream.cpp b/lldb/tools/lldb-dap/IOStream.cpp index d2e8ec40b0a7b8..188f6ec1fae9d9 100644 --- a/lldb/tools/lldb-dap/IOStream.cpp +++ b/lldb/tools/lldb-dap/IOStream.cpp @@ -7,6 +7,8 @@ //===----------------------------------------------------------------------===// #include "IOStream.h" +#include <string> +#include "llvm/Support/raw_ostream.h" #if defined(_WIN32) #include <io.h> @@ -16,8 +18,6 @@ #include <unistd.h> #endif -#include <fstream> -#include <string> using namespace lldb_dap; @@ -87,7 +87,7 @@ bool OutputStream::write_full(llvm::StringRef str) { return true; } -bool InputStream::read_full(std::ofstream *log, size_t length, +bool InputStream::read_full(llvm::raw_ostream *log, size_t length, std::string &text) { std::string data; data.resize(length); @@ -131,7 +131,7 @@ bool InputStream::read_full(std::ofstream *log, size_t length, return true; } -bool InputStream::read_line(std::ofstream *log, std::string &line) { +bool InputStream::read_line(llvm::raw_ostream *log, std::string &line) { line.clear(); while (true) { if (!read_full(log, 1, line)) @@ -144,7 +144,7 @@ bool InputStream::read_line(std::ofstream *log, std::string &line) { return true; } -bool InputStream::read_expected(std::ofstream *log, llvm::StringRef expected) { +bool InputStream::read_expected(llvm::raw_ostream *log, llvm::StringRef expected) { std::string result; if (!read_full(log, expected.size(), result)) return false; diff --git a/lldb/tools/lldb-dap/IOStream.h b/lldb/tools/lldb-dap/IOStream.h index 57d5fd458b7165..1933399e075363 100644 --- a/lldb/tools/lldb-dap/IOStream.h +++ b/lldb/tools/lldb-dap/IOStream.h @@ -22,7 +22,7 @@ typedef int SOCKET; #endif #include "llvm/ADT/StringRef.h" - +#include "llvm/Support/raw_ostream.h" #include <fstream> #include <string> @@ -52,11 +52,11 @@ struct StreamDescriptor { struct InputStream { StreamDescriptor descriptor; - bool read_full(std::ofstream *log, size_t length, std::string &text); + bool read_full(llvm::raw_ostream *log, size_t length, std::string &text); - bool read_line(std::ofstream *log, std::string &line); + bool read_line(llvm::raw_ostream *log, std::string &line); - bool read_expected(std::ofstream *log, llvm::StringRef expected); + bool read_expected(llvm::raw_ostream *log, llvm::StringRef expected); }; struct OutputStream { diff --git a/lldb/tools/lldb-dap/Options.td b/lldb/tools/lldb-dap/Options.td index dfca79b2884ac8..41d8912ed6c6bf 100644 --- a/lldb/tools/lldb-dap/Options.td +++ b/lldb/tools/lldb-dap/Options.td @@ -17,21 +17,10 @@ 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>, - MetaVarName<"<port>">, - HelpText<"Alias for --port">; - -def unix_socket: S<"unix-socket">, - MetaVarName<"<path>">, - HelpText<"Communicate with the lldb-dap tool over the unix socket or named pipe.">; -def: Separate<["-"], "u">, - Alias<unix_socket>, - MetaVarName<"<path>">, - HelpText<"Alias for --unix_socket">; +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>">, @@ -51,7 +40,8 @@ def debugger_pid: S<"debugger-pid">, def repl_mode: S<"repl-mode">, MetaVarName<"<mode>">, - HelpText<"The mode for handling repl evaluation requests, supported modes: variable, command, auto.">; + 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/Socket.cpp b/lldb/tools/lldb-dap/Socket.cpp new file mode 100644 index 00000000000000..c5c90a81547878 --- /dev/null +++ b/lldb/tools/lldb-dap/Socket.cpp @@ -0,0 +1,500 @@ +//===-- Socket.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 "Socket.h" +#include "llvm/ADT/StringExtras.h" +#include "llvm/ADT/StringRef.h" +#include "llvm/Config/config.h" +#include "llvm/Support/Errno.h" +#include "llvm/Support/Error.h" +#include "llvm/Support/FileSystem.h" +#include "llvm/Support/Signals.h" +#include "llvm/Support/Threading.h" +#include <cstdint> +#include <netdb.h> +#include <netinet/in.h> +#include <sys/_types/_socklen_t.h> +#include <sys/poll.h> +#include <system_error> + +#ifndef _WIN32 +#include <poll.h> +#include <sys/socket.h> +#include <sys/un.h> +#else +#include "llvm/Support/Windows/WindowsSupport.h" +// winsock2.h must be included before afunix.h. Briefly turn off clang-format to +// avoid error. +// clang-format off +#include <winsock2.h> +#include <afunix.h> +// clang-format on +#include <io.h> +#endif // _WIN32 + +#if defined(HAVE_UNISTD_H) +#include <unistd.h> +#endif + +namespace { + +enum SocketProtocol { ProtocolTcp, ProtocolUnixDomain }; + +std::error_code getLastSocketErrorCode() { +#ifdef _WIN32 + return std::error_code(::WSAGetLastError(), std::system_category()); +#else + return llvm::errnoAsErrorCode(); +#endif +} + +llvm::Expected<std::vector<lldb_dap::SocketAddress>> +getAddressInfo(llvm::StringRef host) { + std::vector<lldb_dap::SocketAddress> add_list; + + struct addrinfo hints; + memset(&hints, 0, sizeof(hints)); + hints.ai_family = AF_UNSPEC; + hints.ai_socktype = SOCK_STREAM; + hints.ai_protocol = IPPROTO_TCP; + hints.ai_flags = 0; + + struct addrinfo *service_info_list = nullptr; + + int err = + getaddrinfo(host.str().c_str(), nullptr, &hints, &service_info_list); + if (err != 0) + return llvm::createStringError("getaddrinfo failed: %s", gai_strerror(err)); + + for (struct addrinfo *service_ptr = service_info_list; service_ptr != nullptr; + service_ptr = service_ptr->ai_next) { + add_list.emplace_back(lldb_dap::SocketAddress(service_ptr)); + } + + if (service_info_list) + ::freeaddrinfo(service_info_list); + + return add_list; +} + +int CloseSocket(NativeSocket sockfd) { +#ifdef _WIN32 + return ::closesocket(sockfd); +#else + return ::close(sockfd); +#endif +} + +llvm::Expected<std::pair<llvm::StringRef, int>> +detectHostAndPort(llvm::StringRef name) { + llvm::StringRef host; + llvm::StringRef port; + std::tie(host, port) = name.split(":"); + + if (host == "" || host == "*") { + host = "0.0.0.0"; + } + + if (port == "") { + port = "0"; + } + + int portnu = 0; + if (!llvm::to_integer(port, portnu, 10)) { + return llvm::createStringError(llvm::inconvertibleErrorCode(), + "invalid host:port specification: '%s'", + name.str().c_str()); + } + + return std::make_pair(host, portnu); +} + +} // namespace + +namespace lldb_dap { + +#ifdef _WIN32 +WSABalancer::WSABalancer() { + WSADATA WsaData; + ::memset(&WsaData, 0, sizeof(WsaData)); + if (WSAStartup(MAKEWORD(2, 2), &WsaData) != 0) { + llvm::report_fatal_error("WSAStartup failed"); + } +} + +WSABalancer::~WSABalancer() { WSACleanup(); } +#endif // _WIN32 + +SocketAddress::SocketAddress() { clear(); } + +SocketAddress::SocketAddress(const struct addrinfo *info) { + clear(); + if (info && info->ai_addrlen > 0 && + size_t(info->ai_addrlen) <= sizeof m_address) { + memcpy(&m_address.sa, info->ai_addr, static_cast<size_t>(info->ai_addrlen)); + m_address.sa.sa_len = info->ai_addrlen; + } +} + +SocketAddress::SocketAddress(const struct sockaddr &sa) { m_address.sa = sa; } + +SocketAddress::SocketAddress(const struct sockaddr_in &sa) { + m_address.sa_ipv4 = sa; +} + +SocketAddress::SocketAddress(const struct sockaddr_in6 &sa) { + m_address.sa_ipv6 = sa; +} + +SocketAddress::SocketAddress(const struct sockaddr_un &su) { + m_address.su = su; +} + +std::string SocketAddress::getPath() const { + if (getFamily() == AF_UNIX) + return std::string(m_address.su.sun_path); + return ""; +} + +std::string SocketAddress::getName() const { + if (getFamily() == AF_UNIX) + return "unix://" + getPath(); + + uint64_t port = getPort(); + std::string hostname; + if (isLocalhost()) + hostname = "localhost"; + else if (isAnyAddr()) + hostname = getFamily() == AF_INET ? "0.0.0.0" : "[::]"; + else { + char hbuf[NI_MAXHOST]; + if (getnameinfo(&m_address.sa, getLength(), hbuf, sizeof(hbuf), nullptr, 0, + NI_NUMERICHOST) == 0) { + hostname = std::string(hbuf); + } + } + + return "tcp://" + hostname + ":" + std::to_string(port); +} + +bool SocketAddress::isLocalhost() const { + switch (getFamily()) { + case AF_INET: + return m_address.sa_ipv4.sin_addr.s_addr == htonl(INADDR_LOOPBACK); + case AF_INET6: + return 0 == memcmp(&m_address.sa_ipv6.sin6_addr, &in6addr_loopback, 16); + } + return false; +} + +bool SocketAddress::isAnyAddr() const { + switch (getFamily()) { + case AF_INET: + return m_address.sa_ipv4.sin_addr.s_addr == htonl(INADDR_ANY); + case AF_INET6: + return 0 == memcmp(&m_address.sa_ipv6.sin6_addr, &in6addr_any, 16); + } + return false; +} + +void SocketAddress::setFamily(sa_family_t family) { + m_address.sa.sa_family = family; +#if defined(__APPLE__) || defined(__FreeBSD__) || defined(__NetBSD__) || \ + defined(__OpenBSD__) + switch (family) { + case AF_INET: + m_address.sa.sa_len = sizeof(struct sockaddr_in); + break; + + case AF_INET6: + m_address.sa.sa_len = sizeof(struct sockaddr_in6); + break; + + case AF_UNIX: + m_address.sa.sa_len = SUN_LEN(&m_address.su); + break; + + default: + assert(0 && "unsupported socket family"); + } +#endif +} + +SocketAddress SocketAddress::addressOf(const Socket &s, std::error_code &EC) { + SocketAddress sa; + socklen_t len = sizeof(sockaddr_t); + + if (::getsockname(s.fd, &sa.sockaddr(), &len) == -1) { + EC = getLastSocketErrorCode(); + return SocketAddress{}; + } + + // If the socket is a struct sockaddr_un then the length is returned by the + // len parameter not the sa_len field. Update the field to match the + // returned address. + sa.m_address.sa.sa_len = len; + return sa; +} + +bool SocketAddress::setToAnyAddress(sa_family_t family, uint16_t port) { + switch (family) { + case AF_INET: + setFamily(family); + if (setPort(port)) { + m_address.sa_ipv4.sin_addr.s_addr = htonl(INADDR_ANY); + return true; + } + break; + + case AF_INET6: + setFamily(family); + if (setPort(port)) { + m_address.sa_ipv6.sin6_addr = in6addr_any; + return true; + } + break; + } + + clear(); + return false; +} + +Socket::Socket(NativeSocket fd) : fd(fd) {} +Socket::Socket(Socket &&S) : fd(S.fd) { S.fd = -1; } + +Socket::~Socket() { + close(); +} + +void Socket::close() { + if (fd == -1) { + return; + } + + CloseSocket(fd); + fd = -1; +} + +SocketListener::SocketListener(std::vector<Socket> sockets, int fds[2]) + : m_listening(true), m_sockets(std::move(sockets)), m_pipe{fds[0], fds[1]} { +} + +SocketListener::SocketListener(SocketListener &&SL) + : m_listening(SL.m_listening.load()), m_sockets(std::move(SL.m_sockets)), + m_pipe{SL.m_pipe[0], SL.m_pipe[1]} { + SL.m_listening = false; + SL.m_sockets.clear(); + SL.m_pipe[0] = -1; + SL.m_pipe[1] = -1; +} + +llvm::Expected<SocketListener> +SocketListener::createListener(llvm::StringRef name) { + SocketProtocol protocol; + + if (name.consume_front("tcp://") || name.consume_front("tcp:") || + name.starts_with(":")) { + protocol = ProtocolTcp; + } else if (name.consume_front("unix://") || name.consume_front("unix:") || + name.starts_with("/")) { + protocol = ProtocolUnixDomain; + } else if (name.contains(":")) { + protocol = ProtocolTcp; + } else { + return llvm::createStringError( + "invalid address, expected '[tcp://][host]:port' or " + "'[unix://]/path' but got %s.", + name.str().c_str()); + } + + std::vector<SocketAddress> addresses; + if (protocol == ProtocolTcp) { + auto maybeHostAndPort = detectHostAndPort(name); + if (auto Err = maybeHostAndPort.takeError()) + return Err; + + llvm::StringRef host; + int port; + std::tie(host, port) = *maybeHostAndPort; + + llvm::Expected<std::vector<SocketAddress>> maybeAddresses = + getAddressInfo(host); + if (auto Err = maybeAddresses.takeError()) { + return Err; + } + + for (auto &address : *maybeAddresses) { + SocketAddress listen_address = address; + if (!listen_address.isLocalhost()) + listen_address.setToAnyAddress(address.getFamily(), port); + else + listen_address.setPort(port); + + addresses.push_back(std::move(listen_address)); + } + } else { + llvm::StringRef path = name; + if (llvm::sys::fs::exists(path)) { + return llvm::make_error<llvm::StringError>( + std::make_error_code(std::errc::file_exists), "file exists at path"); + } + + struct sockaddr_un addr; + bzero(&addr, sizeof(struct sockaddr_un)); + addr.sun_family = AF_UNIX; + strncpy(addr.sun_path, path.str().c_str(), sizeof(addr.sun_path) - 1); + addr.sun_len = SUN_LEN(&addr); + + addresses.push_back(SocketAddress(addr)); + } + + std::vector<Socket> sockets; + for (auto &address : addresses) { + bool isTCP = + address.getFamily() == AF_INET || address.getFamily() == AF_INET6; + + NativeSocket fd = + ::socket(address.getFamily(), SOCK_STREAM, isTCP ? IPPROTO_TCP : 0); + if (fd == -1) { + return llvm::createStringError(getLastSocketErrorCode(), + "socket() failed"); + } + + int val = 1; + if (::setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, &val, sizeof(int)) == -1) { + return llvm::make_error<llvm::StringError>(getLastSocketErrorCode(), + "setsockopt() failed"); + } + + if (::bind(fd, &address.sockaddr(), address.getLength()) == -1) { + return llvm::make_error<llvm::StringError>(getLastSocketErrorCode(), + "bind() failed"); + } + + if (::listen(fd, llvm::hardware_concurrency().compute_thread_count()) == + -1) { + return llvm::make_error<llvm::StringError>(getLastSocketErrorCode(), + "listen() failed"); + } + + sockets.emplace_back(fd); + } + + int pipeFD[2]; +#ifdef _WIN32 + // Reserve 1 byte for the pipe and use default textmode + if (::_pipe(pipeFD, 1, 0) == -1) +#else + if (::pipe(pipeFD) == -1) +#endif // _WIN32 + return llvm::make_error<llvm::StringError>(getLastSocketErrorCode(), + "pipe failed"); + + return SocketListener{std::move(sockets), pipeFD}; +} + +SocketListener::~SocketListener() { + shutdown(); + if (m_pipe[0] != -1) + close(m_pipe[0]); + if (m_pipe[1] != -1) + close(m_pipe[1]); +} + +llvm::Expected<std::vector<std::string>> SocketListener::addresses() const { + std::vector<std::string> addrs; + std::error_code EC; + for (auto &s : m_sockets) { + auto addr = SocketAddress::addressOf(s, EC); + if (EC) + return llvm::make_error<llvm::StringError>(EC); + + addrs.push_back(addr.getName()); + } + return addrs; +} + +void SocketListener::shutdown() { + bool listening = m_listening; + + if (!listening) + return; + + if (!m_listening.compare_exchange_strong(listening, false)) + return; + + for (auto &s : m_sockets) { + // If the socket has a path (e.g. a unix:// socket), remove it after + // closing. + std::error_code _; // ignoring failures during shutdown. + std::string path = SocketAddress::addressOf(s, _).getPath(); + s.close(); + if (!path.empty()) + unlink(path.c_str()); + } + + // Write to the pipe to indiciate that accept() should exit immediately. + char byte = 'A'; + ssize_t written = ::write(m_pipe[1], &byte, 1); + + // Ignore any write() error + (void)written; +} + +llvm::Expected<Socket> SocketListener::accept() { + if (m_sockets.empty()) { + return llvm::createStringError( + std::make_error_code(std::errc::bad_file_descriptor), "no open socket"); + } + + std::vector<struct pollfd> pollfds; + + for (auto &s : m_sockets) + pollfds.emplace_back(pollfd{s.fd, POLLIN, 0}); + + pollfds.emplace_back(pollfd{m_pipe[0], POLLIN, 0}); + + while (m_listening.load()) { + int status; +#ifdef _WIN32 + status = WSAPoll(pollfds.data(), pollfds.size(), -1); +#else + status = ::poll(pollfds.data(), pollfds.size(), -1); +#endif + + if (status == -1) { + return llvm::make_error<llvm::StringError>(getLastSocketErrorCode(), + "poll() failed"); + } + + for (auto &pfd : pollfds) { + if (pfd.revents & POLLIN) { + // Check if shutdown was requested. + if (pfd.fd == m_pipe[0]) { + break; + } + + struct sockaddr client; + socklen_t len; + int fd = + llvm::sys::RetryAfterSignal(-1, ::accept, pfd.fd, &client, &len); + if (fd == -1) { + return llvm::make_error<llvm::StringError>(getLastSocketErrorCode(), + "accept() failed"); + } + + return Socket{fd}; + } + } + } + + return llvm::make_error<llvm::StringError>( + std::make_error_code(std::errc::connection_aborted), "socket shutdown"); +} + +} // namespace lldb_dap diff --git a/lldb/tools/lldb-dap/Socket.h b/lldb/tools/lldb-dap/Socket.h new file mode 100644 index 00000000000000..98923032f91417 --- /dev/null +++ b/lldb/tools/lldb-dap/Socket.h @@ -0,0 +1,182 @@ +//===-- Socket.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_SOCKET_H +#define LLDB_TOOLS_LLDB_DAP_SOCKET_H + +#include "llvm/ADT/StringRef.h" +#include "llvm/Support/Error.h" +#include <cstdint> +#include <string> +#include <vector> + +#ifdef _WIN32 +#include "lldb/Host/windows/windows.h" +#include <winsock2.h> +#include <ws2tcpip.h> +typedef ADDRESS_FAMILY sa_family_t; +typedef SOCKET NativeSocket; +#else +#include <netdb.h> +#include <netinet/in.h> +#include <sys/socket.h> +#include <sys/un.h> +typedef int NativeSocket; +#endif + +namespace lldb_dap { + +struct Socket; + +/// Manages socket addresses for socket based communication. +struct SocketAddress { + SocketAddress(); + explicit SocketAddress(const struct addrinfo *); + explicit SocketAddress(const struct sockaddr &); + explicit SocketAddress(const struct sockaddr_in &); + explicit SocketAddress(const struct sockaddr_in6 &); + explicit SocketAddress(const struct sockaddr_un &); + + static SocketAddress addressOf(const Socket &s, std::error_code &EC); + + sa_family_t getFamily() const { return m_address.sa.sa_family; } + uint16_t getPort() const { + switch (getFamily()) { + case AF_INET: + return ntohs(m_address.sa_ipv4.sin_port); + case AF_INET6: + return ntohs(m_address.sa_ipv6.sin6_port); + } + return 0; + } + std::string getName() const; + /// Returns the path for AF_UNIX sockets, otherwise "". + std::string getPath() const; + /// Get the length for the current socket address family + socklen_t getLength() const { return m_address.sa.sa_len; }; + + bool isLocalhost() const; + bool isAnyAddr() const; + + void setFamily(sa_family_t family); + bool setToAnyAddress(sa_family_t family, uint16_t port); + bool setPort(uint16_t port) { + switch (getFamily()) { + case AF_INET: + m_address.sa_ipv4.sin_port = htons(port); + return true; + case AF_INET6: + m_address.sa_ipv6.sin6_port = htons(port); + return true; + } + return false; + } + + void clear() { bzero(&m_address, sizeof(sockaddr_t)); } + + struct sockaddr &sockaddr() { return m_address.sa; } + const struct sockaddr &sockaddr() const { return m_address.sa; } + +private: + typedef union sockaddr_tag { + struct sockaddr sa; + struct sockaddr_in sa_ipv4; + struct sockaddr_in6 sa_ipv6; + struct sockaddr_un su; + struct sockaddr_storage ss; + } sockaddr_t; + sockaddr_t m_address; +}; + +/// Manages the lifetime of a socket. +struct Socket { + NativeSocket fd; + + /// Close the socket. + void close(); + + explicit Socket(NativeSocket fd); + ~Socket(); + Socket(Socket &&S); + Socket(const Socket &S) = delete; + Socket &operator=(const Socket &S) = delete; +}; + +#ifdef _WIN32 +/// Ensures proper initialization and cleanup of winsock resources +/// +/// Make sure that calls to WSAStartup and WSACleanup are balanced. +class WSABalancer { +public: + WSABalancer(); + ~WSABalancer(); +}; +#endif // _WIN32 + +/// Manages listening for socket connections. +/// +/// SocketListener handles listening for both unix and tcp based connections. +struct SocketListener { + SocketListener(const SocketListener &S) = delete; + SocketListener &operator=(const SocketListener &S) = delete; + SocketListener(SocketListener &&SL); + ~SocketListener(); + + /// Creates a listening socket bound to the specified name. + /// + /// Handles the socket creation, binding, and immediately starts listening for + /// incoming connections. + /// + /// \param[in] name + /// Socket names formatted like `protocol:name`. + /// + /// Supported protocols include tcp and unix sockets. + /// + /// Names must follow the following formats: + /// + /// * tcp://host:port + /// * tcp:host:port + /// * tcp:port (host will be assumed 0.0.0.0) + /// * :port (implicit tcp) + /// * unix:///path + /// * /path (implicit unix) + static llvm::Expected<SocketListener> createListener(llvm::StringRef name); + + /// Returns an array of active listening sockets. + llvm::Expected<std::vector<std::string>> addresses() const; + + /// Shutdown the socket listener. + void shutdown(); + + /// Returns true if listening, otherwise false. + bool isListening() const { return m_listening; } + + /// Accept returns the next client connection, blocking until a connection is + /// made or Shutdown is called. + llvm::Expected<Socket> accept(); + +private: + SocketListener(std::vector<Socket> sockets, int fds[2]); + + std::atomic<bool> m_listening; + std::vector<Socket> m_sockets; + + /// If a separate thread calls shutdown, the listening file descriptor + /// could be closed while ::poll is waiting for it to be ready to perform a + /// I/O operations. ::poll will continue to block even after FD is closed so + /// use a self-pipe mechanism to get ::poll to return + int m_pipe[2] = {}; + +#ifdef _WIN32 + WSABalancer _; +#endif // _WIN32 +}; + +} // namespace lldb_dap + +#endif diff --git a/lldb/tools/lldb-dap/lldb-dap.cpp b/lldb/tools/lldb-dap/lldb-dap.cpp index 51197587ffccec..0c602ecbbbe836 100644 --- a/lldb/tools/lldb-dap/lldb-dap.cpp +++ b/lldb/tools/lldb-dap/lldb-dap.cpp @@ -10,8 +10,8 @@ #include "FifoFiles.h" #include "JSONUtils.h" #include "LLDBUtils.h" -#include "OutputRedirector.h" #include "RunInTerminal.h" +#include "Socket.h" #include "Watchpoint.h" #include "lldb/API/SBDeclaration.h" #include "lldb/API/SBFile.h" @@ -31,11 +31,12 @@ #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/raw_ostream.h" #include <algorithm> #include <array> @@ -45,6 +46,7 @@ #include <cstdio> #include <cstdlib> #include <cstring> +#include <iostream> #include <map> #include <memory> #include <optional> @@ -52,6 +54,7 @@ #include <sys/stat.h> #include <sys/types.h> #include <thread> +#include <utility> #include <vector> #if defined(_WIN32) @@ -141,234 +144,6 @@ lldb::SBValueList *GetTopLevelScope(DAP &dap, int64_t variablesReference) { } } -/// Redirect stdout and stderr fo the IDE's console output. -/// -/// Errors in this operation will be printed to the log file and the IDE's -/// console output as well. -/// -/// \return -/// A fd pointing to the original stdout. -void SetupRedirection(DAP &dap, int stdoutfd = -1, int stderrfd = -1) { - auto output_callback_stderr = [&dap](llvm::StringRef data) { - dap.SendOutput(OutputType::Stderr, data); - }; - auto output_callback_stdout = [&dap](llvm::StringRef data) { - dap.SendOutput(OutputType::Stdout, data); - }; - - llvm::Expected<int> new_stdout_fd = - RedirectFd(stdoutfd, output_callback_stdout); - if (auto err = new_stdout_fd.takeError()) { - std::string error_message = llvm::toString(std::move(err)); - if (dap.log) - *dap.log << error_message << std::endl; - output_callback_stderr(error_message); - } - dap.out = lldb::SBFile(new_stdout_fd.get(), "w", false); - - llvm::Expected<int> new_stderr_fd = - RedirectFd(stderrfd, output_callback_stderr); - if (auto err = new_stderr_fd.takeError()) { - std::string error_message = llvm::toString(std::move(err)); - if (dap.log) - *dap.log << error_message << std::endl; - output_callback_stderr(error_message); - } - dap.err = lldb::SBFile(new_stderr_fd.get(), "w", false); -} - -void HandleClient(int clientfd, llvm::StringRef program_path, - const std::vector<std::string> &pre_init_commands, - std::shared_ptr<std::ofstream> log, - ReplMode default_repl_mode) { - if (log) - *log << "client[" << clientfd << "] connected\n"; - DAP dap = DAP(program_path, log, default_repl_mode, pre_init_commands); - dap.debug_adaptor_path = program_path; - - SetupRedirection(dap); - RegisterRequestCallbacks(dap); - - dap.input.descriptor = StreamDescriptor::from_socket(clientfd, false); - dap.output.descriptor = StreamDescriptor::from_socket(clientfd, false); - - for (const std::string &arg : pre_init_commands) { - dap.pre_init_commands.push_back(arg); - } - - if (auto Err = dap.Loop()) { - if (log) - *log << "Transport Error: " << llvm::toString(std::move(Err)) << "\n"; - } - - if (log) - *log << "client[" << clientfd << "] connection closed\n"; -#if defined(_WIN32) - closesocket(clientfd); -#else - close(clientfd); -#endif -} - -std::error_code getLastSocketErrorCode() { -#ifdef _WIN32 - return std::error_code(::WSAGetLastError(), std::system_category()); -#else - return llvm::errnoAsErrorCode(); -#endif -} - -llvm::Expected<int> getSocketFD(llvm::StringRef path) { - if (llvm::sys::fs::exists(path) && (::remove(path.str().c_str()) == -1)) { - return llvm::make_error<llvm::StringError>(getLastSocketErrorCode(), - "Remove existing socket failed"); - } - - SOCKET sockfd = socket(AF_UNIX, SOCK_STREAM, 0); - if (sockfd == -1) { - return llvm::make_error<llvm::StringError>(getLastSocketErrorCode(), - "Create socket failed"); - } - - struct sockaddr_un addr; - bzero(&addr, sizeof(addr)); - addr.sun_family = AF_UNIX; - strncpy(addr.sun_path, path.str().c_str(), sizeof(addr.sun_path) - 1); - - if (::bind(sockfd, (struct sockaddr *)&addr, sizeof(addr)) == -1) { -#if defined(_WIN32) - closesocket(sockfd); -#else - close(sockfd); -#endif - return llvm::make_error<llvm::StringError>(getLastSocketErrorCode(), - "Socket bind() failed"); - } - - if (listen(sockfd, llvm::hardware_concurrency().compute_thread_count()) < 0) { -#if defined(_WIN32) - closesocket(sockfd); -#else - close(sockfd); -#endif - return llvm::make_error<llvm::StringError>(getLastSocketErrorCode(), - "Socket listen() failed"); - } - - return sockfd; -} - -llvm::Expected<int> getSocketFD(int portno) { - SOCKET sockfd = socket(AF_INET, SOCK_STREAM, 0); - if (sockfd < 0) { - return llvm::make_error<llvm::StringError>(getLastSocketErrorCode(), - "Create socket failed"); - } - - struct sockaddr_in serv_addr; - bzero(&serv_addr, 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 defined(_WIN32) - closesocket(sockfd); -#else - close(sockfd); -#endif - return llvm::make_error<llvm::StringError>(getLastSocketErrorCode(), - "Socket bind() failed"); - } - - if (listen(sockfd, llvm::hardware_concurrency().compute_thread_count()) < 0) { -#if defined(_WIN32) - closesocket(sockfd); -#else - close(sockfd); -#endif - return llvm::make_error<llvm::StringError>(getLastSocketErrorCode(), - "Socket listen() failed"); - } - - return sockfd; -} - -int AcceptConnection(llvm::StringRef program_path, - const std::vector<std::string> &pre_init_commands, - std::shared_ptr<std::ofstream> log, - ReplMode default_repl_mode, - llvm::StringRef unix_socket_path) { - auto listening = getSocketFD(unix_socket_path); - if (auto E = listening.takeError()) { - llvm::errs() << "Listening on " << unix_socket_path - << " failed: " << llvm::toString(std::move(E)) << "\n"; - return EXIT_FAILURE; - } - - while (true) { - struct sockaddr_un cli_addr; - bzero(&cli_addr, sizeof(struct sockaddr_un)); - socklen_t clilen = sizeof(cli_addr); - SOCKET clientfd = - llvm::sys::RetryAfterSignal(static_cast<SOCKET>(-1), accept, *listening, - (struct sockaddr *)&cli_addr, &clilen); - if (clientfd < 0) { - llvm::errs() << "Client accept failed: " - << getLastSocketErrorCode().message() << "\n"; - return EXIT_FAILURE; - } - - std::thread t(HandleClient, clientfd, program_path, pre_init_commands, log, - default_repl_mode); - t.detach(); - } - -#if defined(_WIN32) - closesocket(*listening); -#else - close(*listening); -#endif - return 0; -} - -int AcceptConnection(llvm::StringRef program_path, - const std::vector<std::string> &pre_init_commands, - std::shared_ptr<std::ofstream> log, - ReplMode default_repl_mode, int portno) { - auto listening = getSocketFD(portno); - if (auto E = listening.takeError()) { - llvm::errs() << "Listening on " << portno - << " failed: " << llvm::toString(std::move(E)) << "\n"; - return EXIT_FAILURE; - } - - while (true) { - struct sockaddr_in cli_addr; - bzero(&cli_addr, sizeof(struct sockaddr_in)); - socklen_t clilen = sizeof(cli_addr); - SOCKET clientfd = - llvm::sys::RetryAfterSignal(static_cast<SOCKET>(-1), accept, *listening, - (struct sockaddr *)&cli_addr, &clilen); - if (clientfd < 0) { - llvm::errs() << "Client accept failed: " - << getLastSocketErrorCode().message() << "\n"; - return EXIT_FAILURE; - } - - std::thread t(HandleClient, clientfd, program_path, pre_init_commands, log, - default_repl_mode); - t.detach(); - } - -#if defined(_WIN32) - closesocket(*listening); -#else - close(*listening); -#endif - return 0; -} - 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 @@ -500,12 +275,11 @@ void SendThreadStoppedEvent(DAP &dap) { if (dap.log) *dap.log << "error: SendThreadStoppedEvent() when process" " isn't stopped (" - << lldb::SBDebugger::StateAsCString(state) << ')' << std::endl; + << lldb::SBDebugger::StateAsCString(state) << ")\n"; } } else { if (dap.log) - *dap.log << "error: SendThreadStoppedEvent() invalid process" - << std::endl; + *dap.log << "error: SendThreadStoppedEvent() invalid process\n"; } dap.RunStopCommands(); } @@ -5115,6 +4889,8 @@ static void redirection_test() { fflush(stderr); } +static SocketListener *g_listener = nullptr; + int main(int argc, char *argv[]) { llvm::InitLLVM IL(argc, argv, /*InstallPipeSignalExitHandler=*/false); #if !defined(__APPLE__) @@ -5185,27 +4961,23 @@ 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 unix_socket_path; - if (auto *arg = input_args.getLastArg(OPT_unix_socket)) { + std::string connection; + if (auto *arg = input_args.getLastArg(OPT_connection)) { const auto *path = arg->getValue(); - unix_socket_path.assign(path); + connection.assign(path); } const char *log_file_path = getenv("LLDBDAP_LOG"); - std::shared_ptr<std::ofstream> log; - if (log_file_path) - log = std::make_shared<std::ofstream>(log_file_path); + std::shared_ptr<llvm::raw_ostream> log; + if (log_file_path) { + std::error_code EC; + log.reset(new llvm::raw_fd_ostream(log_file_path, EC)); + if (EC) { + llvm::errs() << "Could not open log file: " << EC.message() << ", " + << log_file_path << '\n'; + return EXIT_FAILURE; + } + } const auto pre_init_commands = input_args.getAllArgValues(OPT_pre_init_command); @@ -5228,25 +5000,90 @@ int main(int argc, char *argv[]) { auto terminate_debugger = llvm::make_scope_exit([] { lldb::SBDebugger::Terminate(); }); - if (portno != -1) { - llvm::errs() << llvm::format("Listening on port %i...\n", portno); - return AcceptConnection(program_path.str(), pre_init_commands, log, - default_repl_mode, portno); - } + if (!connection.empty()) { + auto maybeListener = SocketListener::createListener(connection); + if (auto Err = maybeListener.takeError()) { + llvm::errs() << "Failed to listen for connections: " + << llvm::toString(std::move(Err)) << "\n"; + return EXIT_FAILURE; + } + + g_listener = &*maybeListener; + + llvm::sys::SetInterruptFunction([]() { + if (g_listener) + g_listener->shutdown(); + }); + + auto maybeListenerAddresses = g_listener->addresses(); + if (auto Err = maybeListenerAddresses.takeError()) { + llvm::errs() << "listener failed to retrieve listening socket address: " + << Err << "\n"; + return EXIT_FAILURE; + } + + if (log) + *log << "started with connection listeners " + << llvm::join(*maybeListenerAddresses, ", ") << "\n"; + + llvm::outs() << "Listening for: " + << llvm::join(*maybeListenerAddresses, ", ") << "\n"; + // Ensure listening address are flushed for calles to retrieve the resolve address. + llvm::outs().flush(); + + while (g_listener && g_listener->isListening()) { + llvm::Expected<Socket> maybeClient = g_listener->accept(); + if (auto Err = maybeClient.takeError()) { + if (log) + *log << "client accept failed: " << Err << "\n"; + llvm::errs() << "client connection failed: " << Err << "\n"; + continue; + } + + Socket client = std::move(*maybeClient); + std::thread t([=, client = std::move(client)]() mutable { + DAP dap = DAP(program_path, log, default_repl_mode, pre_init_commands); + + if (auto Err = dap.ConfigureIO()) { + llvm::errs() << "failed to configure client connect: " + << llvm::toString(std::move(Err)) << "\n"; + return; + } + + RegisterRequestCallbacks(dap); + dap.input.descriptor = StreamDescriptor::from_socket(client.fd, false); + dap.output.descriptor = StreamDescriptor::from_socket(client.fd, false); + if (auto Err = dap.Loop()) { + if (log) + *log << "client[" << client.fd + << "] Transport Error: " << llvm::toString(std::move(Err)) + << "\n"; + } + + if (log) + *log << "client[" << client.fd << "] connection closed\n"; + + client.close(); + }); + t.detach(); + } - if (!unix_socket_path.empty()) { - return AcceptConnection(program_path.str(), pre_init_commands, log, - default_repl_mode, unix_socket_path); + return EXIT_SUCCESS; } + // stdout/stderr redirection to the IDE's console + int new_stdout_fd = dup(fileno(stdout)); DAP dap = DAP(program_path.str(), log, default_repl_mode, pre_init_commands); - + if (auto Err = dap.ConfigureIO(fileno(stdout), fileno(stderr))) { + llvm::errs() << "Failed to create lldb-dap instance: " << Err << "\n"; + return EXIT_FAILURE; + } RegisterRequestCallbacks(dap); #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. + // 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); @@ -5254,10 +5091,6 @@ int main(int argc, char *argv[]) { assert(result); #endif - // stdout/stderr redirection to the IDE's console - int new_stdout_fd = dup(fileno(stdout)); - SetupRedirection(dap, fileno(stdout), fileno(stderr)); - dap.input.descriptor = StreamDescriptor::from_file(fileno(stdin), false); dap.output.descriptor = StreamDescriptor::from_file(new_stdout_fd, false); @@ -5265,15 +5098,10 @@ int main(int argc, char *argv[]) { if (getenv("LLDB_DAP_TEST_STDOUT_STDERR_REDIRECTION") != nullptr) redirection_test(); - for (const std::string &arg : - input_args.getAllArgValues(OPT_pre_init_command)) { - dap.pre_init_commands.push_back(arg); - } - bool CleanExit = true; if (auto Err = dap.Loop()) { if (dap.log) - *dap.log << "Transport Error: " << llvm::toString(std::move(Err)) << "\n"; + *dap.log << "Transport Error: " << Err << "\n"; CleanExit = false; } _______________________________________________ lldb-commits mailing list lldb-commits@lists.llvm.org https://lists.llvm.org/cgi-bin/mailman/listinfo/lldb-commits