//===-- lldb-dap.cpp ------------------------------------------------------===// // // 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 "DAP.h" #include "DAPLog.h" #include "EventHelper.h" #include "Handler/RequestHandler.h" #include "RunInTerminal.h" #include "Transport.h" #include "lldb/API/SBDebugger.h" #include "lldb/API/SBStream.h" #include "lldb/Host/Config.h" #include "lldb/Host/File.h" #include "lldb/Host/MainLoop.h" #include "lldb/Host/MainLoopBase.h" #include "lldb/Host/MemoryMonitor.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/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/CommandLine.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/Threading.h" #include "llvm/Support/raw_ostream.h" #include #include #include #include #include #include #include #include #include #include #include #include #if defined(_WIN32) // We need to #define NOMINMAX in order to skip `min()` and `max()` macro // definitions that conflict with other system headers. // We also need to #undef GetObject (which is defined to GetObjectW) because // the JSON code we use also has methods named `GetObject()` and we conflict // against these. #define NOMINMAX #include #undef GetObject #include typedef int socklen_t; #else #include #include #include #include #endif #if defined(__linux__) #include #endif using namespace lldb_dap; using lldb_private::File; using lldb_private::IOObject; using lldb_private::MainLoop; using lldb_private::MainLoopBase; using lldb_private::NativeFile; using lldb_private::Socket; using lldb_private::Status; namespace { using namespace llvm::opt; enum ID { OPT_INVALID = 0, // This is not an option ID. #define OPTION(...) LLVM_MAKE_OPT_ID(__VA_ARGS__), #include "Options.inc" #undef OPTION }; #define OPTTABLE_STR_TABLE_CODE #include "Options.inc" #undef OPTTABLE_STR_TABLE_CODE #define OPTTABLE_PREFIXES_TABLE_CODE #include "Options.inc" #undef OPTTABLE_PREFIXES_TABLE_CODE static constexpr llvm::opt::OptTable::Info InfoTable[] = { #define OPTION(...) LLVM_CONSTRUCT_OPT_INFO(__VA_ARGS__), #include "Options.inc" #undef OPTION }; class LLDBDAPOptTable : public llvm::opt::GenericOptTable { public: LLDBDAPOptTable() : llvm::opt::GenericOptTable(OptionStrTable, OptionPrefixesTable, InfoTable, true) {} }; } // anonymous namespace static void PrintHelp(LLDBDAPOptTable &table, llvm::StringRef tool_name) { std::string usage_str = tool_name.str() + " options"; table.printHelp(llvm::outs(), usage_str.c_str(), "LLDB DAP", false); llvm::outs() << R"___( EXAMPLES: 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 --connection URI will cause lldb-dap to listen for a connection in the specified mode. lldb-dap --connection listen://localhost: Passing --wait-for-debugger will pause the process at startup and wait for a debugger to attach to the process. lldb-dap -g )___"; } static void PrintVersion() { llvm::outs() << "lldb-dap: "; llvm::cl::PrintVersionMessage(); llvm::outs() << "liblldb: " << lldb::SBDebugger::GetVersionString() << '\n'; } // If --launch-target is provided, this instance of lldb-dap becomes a // runInTerminal launcher. It will ultimately launch the program specified in // the --launch-target argument, which is the original program the user wanted // to debug. This is done in such a way that the actual debug adapter can // place breakpoints at the beginning of the program. // // The launcher will communicate with the debug adapter using a fifo file in the // directory specified in the --comm-file argument. // // Regarding the actual flow, this launcher will first notify the debug adapter // of its pid. Then, the launcher will be in a pending state waiting to be // attached by the adapter. // // Once attached and resumed, the launcher will exec and become the program // specified by --launch-target, which is the original target the // user wanted to run. // // In case of errors launching the target, a suitable error message will be // emitted to the debug adapter. static llvm::Error LaunchRunInTerminalTarget(llvm::opt::Arg &target_arg, llvm::StringRef comm_file, lldb::pid_t debugger_pid, char *argv[]) { #if defined(_WIN32) return llvm::createStringError( "runInTerminal is only supported on POSIX systems"); #else // On Linux with the Yama security module enabled, a process can only attach // to its descendants by default. In the runInTerminal case the target // process is launched by the client so we need to allow tracing explicitly. #if defined(__linux__) if (debugger_pid != LLDB_INVALID_PROCESS_ID) (void)prctl(PR_SET_PTRACER, debugger_pid, 0, 0, 0); #endif RunInTerminalLauncherCommChannel comm_channel(comm_file); if (llvm::Error err = comm_channel.NotifyPid()) return err; // We will wait to be attached with a timeout. We don't wait indefinitely // using a signal to prevent being paused forever. // This env var should be used only for tests. const char *timeout_env_var = getenv("LLDB_DAP_RIT_TIMEOUT_IN_MS"); int timeout_in_ms = timeout_env_var != nullptr ? atoi(timeout_env_var) : 20000; if (llvm::Error err = comm_channel.WaitUntilDebugAdapterAttaches( std::chrono::milliseconds(timeout_in_ms))) { return err; } const char *target = target_arg.getValue(); execvp(target, argv); std::string error = std::strerror(errno); comm_channel.NotifyError(error); return llvm::createStringError(llvm::inconvertibleErrorCode(), std::move(error)); #endif } /// used only by TestVSCode_redirection_to_console.py static void redirection_test() { printf("stdout message\n"); fprintf(stderr, "stderr message\n"); fflush(stdout); fflush(stderr); } /// Duplicates a file descriptor, setting FD_CLOEXEC if applicable. static int DuplicateFileDescriptor(int fd) { #if defined(F_DUPFD_CLOEXEC) // Ensure FD_CLOEXEC is set. return ::fcntl(fd, F_DUPFD_CLOEXEC, 0); #else return ::dup(fd); #endif } static llvm::Expected> validateConnection(llvm::StringRef conn) { auto uri = lldb_private::URI::Parse(conn); auto make_error = [conn]() -> llvm::Error { return llvm::createStringError( "Unsupported connection specifier, expected 'accept:///path' or " "'listen://[host]:port', got '%s'.", conn.str().c_str()); }; if (!uri) return make_error(); std::optional protocol_and_mode = Socket::GetProtocolAndMode(uri->scheme); if (!protocol_and_mode || protocol_and_mode->second != Socket::ModeAccept) return make_error(); if (protocol_and_mode->first == Socket::ProtocolTcp) { return std::make_pair( Socket::ProtocolTcp, formatv("[{0}]:{1}", uri->hostname.empty() ? "0.0.0.0" : uri->hostname, uri->port.value_or(0))); } if (protocol_and_mode->first == Socket::ProtocolUnixDomain) return std::make_pair(Socket::ProtocolUnixDomain, uri->path.str()); return make_error(); } static llvm::Error serveConnection(const Socket::SocketProtocol &protocol, const std::string &name, Log *log, const ReplMode default_repl_mode, const std::vector &pre_init_commands) { Status status; static std::unique_ptr listener = Socket::Create(protocol, status); if (status.Fail()) { return status.takeError(); } status = listener->Listen(name, /*backlog=*/5); if (status.Fail()) { return status.takeError(); } std::string address = llvm::join(listener->GetListeningConnectionURI(), ", "); DAP_LOG(log, "started with connection listeners {0}", address); llvm::outs() << "Listening for: " << address << "\n"; // Ensure listening address are flushed for calles to retrieve the resolve // address. llvm::outs().flush(); static MainLoop g_loop; llvm::sys::SetInterruptFunction([]() { g_loop.AddPendingCallback( [](MainLoopBase &loop) { loop.RequestTermination(); }); }); std::condition_variable dap_sessions_condition; std::mutex dap_sessions_mutex; std::map dap_sessions; unsigned int clientCount = 0; auto handle = listener->Accept(g_loop, [=, &dap_sessions_condition, &dap_sessions_mutex, &dap_sessions, &clientCount]( std::unique_ptr sock) { std::string client_name = llvm::formatv("client_{0}", clientCount++).str(); DAP_LOG(log, "({0}) client connected", client_name); lldb::IOObjectSP io(std::move(sock)); // Move the client into a background thread to unblock accepting the next // client. std::thread client([=, &dap_sessions_condition, &dap_sessions_mutex, &dap_sessions]() { llvm::set_thread_name(client_name + ".runloop"); Transport transport(client_name, log, io, io); DAP dap(log, default_repl_mode, pre_init_commands, transport); if (auto Err = dap.ConfigureIO()) { llvm::logAllUnhandledErrors(std::move(Err), llvm::errs(), "Failed to configure stdout redirect: "); return; } { std::scoped_lock lock(dap_sessions_mutex); dap_sessions[io.get()] = &dap; } if (auto Err = dap.Loop()) { llvm::logAllUnhandledErrors(std::move(Err), llvm::errs(), "DAP session (" + client_name + ") error: "); } DAP_LOG(log, "({0}) client disconnected", client_name); std::unique_lock lock(dap_sessions_mutex); dap_sessions.erase(io.get()); std::notify_all_at_thread_exit(dap_sessions_condition, std::move(lock)); }); client.detach(); }); if (auto Err = handle.takeError()) { return Err; } status = g_loop.Run(); if (status.Fail()) { return status.takeError(); } DAP_LOG( log, "lldb-dap server shutdown requested, disconnecting remaining clients..."); bool client_failed = false; { std::scoped_lock lock(dap_sessions_mutex); for (auto [sock, dap] : dap_sessions) { if (llvm::Error error = dap->Disconnect()) { client_failed = true; llvm::errs() << "DAP client " << dap->transport.GetClientName() << " disconnected failed: " << llvm::toString(std::move(error)) << "\n"; } } } // Wait for all clients to finish disconnecting. std::unique_lock lock(dap_sessions_mutex); dap_sessions_condition.wait(lock, [&] { return dap_sessions.empty(); }); if (client_failed) return llvm::make_error( "disconnecting all clients failed", llvm::inconvertibleErrorCode()); return llvm::Error::success(); } int main(int argc, char *argv[]) { llvm::InitLLVM IL(argc, argv, /*InstallPipeSignalExitHandler=*/false); #if !defined(__APPLE__) llvm::setBugReportMsg("PLEASE submit a bug report to " LLDB_BUG_REPORT_URL " and include the crash backtrace.\n"); #else llvm::setBugReportMsg("PLEASE submit a bug report to " LLDB_BUG_REPORT_URL " and include the crash report from " "~/Library/Logs/DiagnosticReports/.\n"); #endif llvm::SmallString<256> program_path(argv[0]); llvm::sys::fs::make_absolute(program_path); DAP::debug_adapter_path = program_path; LLDBDAPOptTable T; unsigned MAI, MAC; llvm::ArrayRef ArgsArr = llvm::ArrayRef(argv + 1, argc); llvm::opt::InputArgList input_args = T.ParseArgs(ArgsArr, MAI, MAC); if (input_args.hasArg(OPT_help)) { PrintHelp(T, llvm::sys::path::filename(argv[0])); return EXIT_SUCCESS; } if (input_args.hasArg(OPT_version)) { PrintVersion(); return EXIT_SUCCESS; } ReplMode default_repl_mode = ReplMode::Auto; if (input_args.hasArg(OPT_repl_mode)) { llvm::opt::Arg *repl_mode = input_args.getLastArg(OPT_repl_mode); llvm::StringRef repl_mode_value = repl_mode->getValue(); if (repl_mode_value == "auto") { default_repl_mode = ReplMode::Auto; } else if (repl_mode_value == "variable") { default_repl_mode = ReplMode::Variable; } 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"; return EXIT_FAILURE; } } if (llvm::opt::Arg *target_arg = input_args.getLastArg(OPT_launch_target)) { if (llvm::opt::Arg *comm_file = input_args.getLastArg(OPT_comm_file)) { lldb::pid_t pid = LLDB_INVALID_PROCESS_ID; llvm::opt::Arg *debugger_pid = input_args.getLastArg(OPT_debugger_pid); if (debugger_pid) { llvm::StringRef debugger_pid_value = debugger_pid->getValue(); if (debugger_pid_value.getAsInteger(10, pid)) { llvm::errs() << "'" << debugger_pid_value << "' is not a valid " "PID\n"; return EXIT_FAILURE; } } int target_args_pos = argc; for (int i = 0; i < argc; i++) { if (strcmp(argv[i], "--launch-target") == 0) { target_args_pos = i + 1; break; } } if (llvm::Error err = LaunchRunInTerminalTarget(*target_arg, comm_file->getValue(), pid, argv + target_args_pos)) { llvm::errs() << llvm::toString(std::move(err)) << '\n'; return EXIT_FAILURE; } } else { llvm::errs() << "\"--launch-target\" requires \"--comm-file\" to be " "specified\n"; 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) if (input_args.hasArg(OPT_wait_for_debugger)) { printf("Paused waiting for debugger to attach (pid = %i)...\n", getpid()); pause(); } #endif std::unique_ptr log = nullptr; const char *log_file_path = getenv("LLDBDAP_LOG"); if (log_file_path) { std::error_code EC; log = std::make_unique(log_file_path, EC); if (EC) { llvm::logAllUnhandledErrors(llvm::errorCodeToError(EC), llvm::errs(), "Failed to create log file: "); return EXIT_FAILURE; } } // Initialize LLDB first before we do anything. lldb::SBError error = lldb::SBDebugger::InitializeWithErrorHandling(); if (error.Fail()) { lldb::SBStream os; error.GetDescription(os); llvm::errs() << "lldb initialize failed: " << os.GetData() << "\n"; return EXIT_FAILURE; } // Create a memory monitor. This can return nullptr if the host platform is // not supported. std::unique_ptr memory_monitor = lldb_private::MemoryMonitor::Create([log = log.get()]() { DAP_LOG(log, "memory pressure detected"); lldb::SBDebugger::MemoryPressureDetected(); }); if (memory_monitor) memory_monitor->Start(); // Terminate the debugger before the C++ destructor chain kicks in. auto terminate_debugger = llvm::make_scope_exit([&] { if (memory_monitor) memory_monitor->Stop(); lldb::SBDebugger::Terminate(); }); std::vector pre_init_commands; for (const std::string &arg : input_args.getAllArgValues(OPT_pre_init_command)) { pre_init_commands.push_back(arg); } if (!connection.empty()) { auto maybeProtoclAndName = validateConnection(connection); if (auto Err = maybeProtoclAndName.takeError()) { llvm::logAllUnhandledErrors(std::move(Err), llvm::errs(), "Invalid connection: "); return EXIT_FAILURE; } Socket::SocketProtocol protocol; std::string name; std::tie(protocol, name) = *maybeProtoclAndName; if (auto Err = serveConnection(protocol, name, log.get(), default_repl_mode, pre_init_commands)) { llvm::logAllUnhandledErrors(std::move(Err), llvm::errs(), "Connection failed: "); return EXIT_FAILURE; } return EXIT_SUCCESS; } #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 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; } lldb::IOObjectSP input = std::make_shared( fileno(stdin), File::eOpenOptionReadOnly, NativeFile::Unowned); lldb::IOObjectSP output = std::make_shared( stdout_fd, File::eOpenOptionWriteOnly, NativeFile::Unowned); constexpr llvm::StringLiteral client_name = "stdio"; Transport transport(client_name, log.get(), input, output); DAP dap(log.get(), default_repl_mode, pre_init_commands, transport); // 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; } // used only by TestVSCode_redirection_to_console.py if (getenv("LLDB_DAP_TEST_STDOUT_STDERR_REDIRECTION") != nullptr) redirection_test(); if (auto Err = dap.Loop()) { DAP_LOG(log.get(), "({0}) DAP session error: {1}", client_name, llvm::toStringWithoutConsuming(Err)); llvm::logAllUnhandledErrors(std::move(Err), llvm::errs(), "DAP session error: "); return EXIT_FAILURE; } return EXIT_SUCCESS; }