Jonas Devlieghere 4f991cc995
[lldb-dap] Make connection URLs match lldb (#144770)
Use the same scheme as ConnectionFileDescriptor::Connect and use
"listen" and "accept". Addresses feedback from a Pavel in a different PR
[1].

[1] https://github.com/llvm/llvm-project/pull/143628#discussion_r2152225200
2025-06-19 20:48:07 -05:00

578 lines
19 KiB
C++

//===-- 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 <condition_variable>
#include <cstdio>
#include <cstdlib>
#include <fcntl.h>
#include <map>
#include <memory>
#include <mutex>
#include <string>
#include <system_error>
#include <thread>
#include <utility>
#include <vector>
#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 <windows.h>
#undef GetObject
#include <io.h>
typedef int socklen_t;
#else
#include <netinet/in.h>
#include <sys/socket.h>
#include <sys/un.h>
#include <unistd.h>
#endif
#if defined(__linux__)
#include <sys/prctl.h>
#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:<port>
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<std::pair<Socket::SocketProtocol, std::string>>
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<Socket::ProtocolModePair> 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<std::string> &pre_init_commands) {
Status status;
static std::unique_ptr<Socket> 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<IOObject *, DAP *> dap_sessions;
unsigned int clientCount = 0;
auto handle = listener->Accept(g_loop, [=, &dap_sessions_condition,
&dap_sessions_mutex, &dap_sessions,
&clientCount](
std::unique_ptr<Socket> 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<std::mutex> 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<std::mutex> 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<std::mutex> 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";
}
// Close the socket to ensure the DAP::Loop read finishes.
sock->Close();
}
}
// Wait for all clients to finish disconnecting.
std::unique_lock<std::mutex> lock(dap_sessions_mutex);
dap_sessions_condition.wait(lock, [&] { return dap_sessions.empty(); });
if (client_failed)
return llvm::make_error<llvm::StringError>(
"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<const char *> 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> log = nullptr;
const char *log_file_path = getenv("LLDBDAP_LOG");
if (log_file_path) {
std::error_code EC;
log = std::make_unique<Log>(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<lldb_private::MemoryMonitor> 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<std::string> 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<NativeFile>(
fileno(stdin), File::eOpenOptionReadOnly, NativeFile::Unowned);
lldb::IOObjectSP output = std::make_shared<NativeFile>(
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;
}