[lldb-dap] Refactoring lldb-dap port listening mode to allow multiple connections. (#116392)

This adjusts the lldb-dap listening mode to accept multiple clients.
Each client initializes a new instance of DAP and an associated
`lldb::SBDebugger` instance.

The listening mode is configured with the `--connection` option and
supports listening on a port or a unix socket on supported platforms.

When running in server mode launch and attach performance should
be improved by lldb sharing symbols for core libraries between debug
sessions.
This commit is contained in:
John Harrison 2025-02-21 18:30:43 -08:00 committed by GitHub
parent 1bb43068f1
commit 998b28f196
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
12 changed files with 544 additions and 205 deletions

View File

@ -927,7 +927,7 @@ class DebugCommunication(object):
"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
@ -1170,40 +1170,88 @@ class DebugCommunication(object):
}
return self.send_recv(command_dict)
class DebugAdaptorServer(DebugCommunication):
def __init__(
self,
executable=None,
port=None,
connection=None,
init_commands=[],
log_file=None,
env=None,
):
self.process = None
self.connection = None
if executable is not None:
adaptor_env = os.environ.copy()
if env is not None:
adaptor_env.update(env)
if log_file:
adaptor_env["LLDBDAP_LOG"] = log_file
self.process = subprocess.Popen(
[executable],
stdin=subprocess.PIPE,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
env=adaptor_env,
process, connection = DebugAdaptorServer.launch(
executable=executable, connection=connection, env=env, log_file=log_file
)
self.process = process
self.connection = connection
if connection is not None:
scheme, address = connection.split("://")
if scheme == "unix-connect": # unix-connect:///path
s = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
s.connect(address)
elif scheme == "connection": # connection://[host]:port
host, port = address.rsplit(":", 1)
# create_connection with try both ipv4 and ipv6.
s = socket.create_connection((host.strip("[]"), int(port)))
else:
raise ValueError("invalid connection: {}".format(connection))
DebugCommunication.__init__(
self, s.makefile("rb"), s.makefile("wb"), init_commands, log_file
)
self.connection = connection
else:
DebugCommunication.__init__(
self, self.process.stdout, self.process.stdin, init_commands, log_file
)
elif port is not None:
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.connect(("127.0.0.1", port))
DebugCommunication.__init__(
self, s.makefile("r"), s.makefile("w"), init_commands
@classmethod
def launch(cls, /, executable, env=None, log_file=None, connection=None):
adaptor_env = os.environ.copy()
if env is not None:
adaptor_env.update(env)
if log_file:
adaptor_env["LLDBDAP_LOG"] = log_file
args = [executable]
if connection is not None:
args.append("--connection")
args.append(connection)
process = subprocess.Popen(
args,
stdin=subprocess.PIPE,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
env=adaptor_env,
)
if connection is None:
return (process, None)
# lldb-dap will print the listening address once the listener is
# made to stdout. The listener is formatted like
# `connection://host:port` or `unix-connection:///path`.
expected_prefix = "Listening for: "
out = process.stdout.readline().decode()
if not out.startswith(expected_prefix):
self.process.kill()
raise ValueError(
"lldb-dap failed to print listening address, expected '{}', got '{}'".format(
expected_prefix, out
)
)
# If the listener expanded into multiple addresses, use the first.
connection = out.removeprefix(expected_prefix).rstrip("\r\n").split(",", 1)[0]
return (process, connection)
def get_pid(self):
if self.process:
return self.process.pid
@ -1369,10 +1417,9 @@ def main():
)
parser.add_option(
"--port",
type="int",
dest="port",
help="Attach a socket to a port instead of using STDIN for VSCode",
"--connection",
dest="connection",
help="Attach a socket connection of using STDIN for VSCode",
default=None,
)
@ -1518,15 +1565,16 @@ def main():
(options, args) = parser.parse_args(sys.argv[1:])
if options.vscode_path is None and options.port is None:
if options.vscode_path is None and options.connection is None:
print(
"error: must either specify a path to a Visual Studio Code "
"Debug Adaptor vscode executable path using the --vscode "
"option, or a port to attach to for an existing lldb-dap "
"using the --port option"
"option, or using the --connection option"
)
return
dbg = DebugAdaptorServer(executable=options.vscode_path, port=options.port)
dbg = DebugAdaptorServer(
executable=options.vscode_path, connection=options.connection
)
if options.debug:
raw_input('Waiting for debugger to attach pid "%i"' % (dbg.get_pid()))
if options.replay:

View File

@ -1,5 +1,6 @@
import os
import time
import subprocess
import dap_server
from lldbsuite.test.lldbtest import *
@ -10,10 +11,10 @@ import lldbgdbserverutils
class DAPTestCaseBase(TestBase):
# set timeout based on whether ASAN was enabled or not. Increase
# timeout by a factor of 10 if ASAN is enabled.
timeoutval = 10 * (10 if ('ASAN_OPTIONS' in os.environ) else 1)
timeoutval = 10 * (10 if ("ASAN_OPTIONS" in os.environ) else 1)
NO_DEBUG_INFO_TESTCASE = True
def create_debug_adaptor(self, lldbDAPEnv=None):
def create_debug_adaptor(self, lldbDAPEnv=None, connection=None):
"""Create the Visual Studio Code debug adaptor"""
self.assertTrue(
is_exe(self.lldbDAPExec), "lldb-dap must exist and be executable"
@ -21,6 +22,7 @@ class DAPTestCaseBase(TestBase):
log_file_path = self.getBuildArtifact("dap.txt")
self.dap_server = dap_server.DebugAdaptorServer(
executable=self.lldbDAPExec,
connection=connection,
init_commands=self.setUpCommands(),
log_file=log_file_path,
env=lldbDAPEnv,

View File

@ -0,0 +1,3 @@
C_SOURCES := main.c
include Makefile.rules

View File

@ -0,0 +1,107 @@
"""
Test lldb-dap server integration.
"""
import os
import signal
import tempfile
import dap_server
from lldbsuite.test.decorators import *
from lldbsuite.test.lldbtest import *
import lldbdap_testcase
class TestDAP_server(lldbdap_testcase.DAPTestCaseBase):
def start_server(self, connection):
log_file_path = self.getBuildArtifact("dap.txt")
(process, connection) = dap_server.DebugAdaptorServer.launch(
executable=self.lldbDAPExec,
connection=connection,
log_file=log_file_path,
)
def cleanup():
process.terminate()
self.addTearDownHook(cleanup)
return (process, connection)
def run_debug_session(self, connection, name):
self.dap_server = dap_server.DebugAdaptorServer(
connection=connection,
)
program = self.getBuildArtifact("a.out")
source = "main.c"
breakpoint_line = line_number(source, "// breakpoint")
self.launch(
program,
args=[name],
disconnectAutomatically=False,
)
self.set_source_breakpoints(source, [breakpoint_line])
self.continue_to_next_stop()
self.continue_to_exit()
output = self.get_stdout()
self.assertEqual(output, f"Hello {name}!\r\n")
self.dap_server.request_disconnect()
def test_server_port(self):
"""
Test launching a binary with a lldb-dap in server mode on a specific port.
"""
self.build()
(_, connection) = self.start_server(connection="tcp://localhost:0")
self.run_debug_session(connection, "Alice")
self.run_debug_session(connection, "Bob")
@skipIfWindows
def test_server_unix_socket(self):
"""
Test launching a binary with a lldb-dap in server mode on a unix socket.
"""
dir = tempfile.gettempdir()
name = dir + "/dap-connection-" + str(os.getpid())
def cleanup():
os.unlink(name)
self.addTearDownHook(cleanup)
self.build()
(_, connection) = self.start_server(connection="unix://" + name)
self.run_debug_session(connection, "Alice")
self.run_debug_session(connection, "Bob")
@skipIfWindows
def test_server_interrupt(self):
"""
Test launching a binary with lldb-dap in server mode and shutting down the server while the debug session is still active.
"""
self.build()
(process, connection) = self.start_server(connection="tcp://localhost:0")
self.dap_server = dap_server.DebugAdaptorServer(
connection=connection,
)
program = self.getBuildArtifact("a.out")
source = "main.c"
breakpoint_line = line_number(source, "// breakpoint")
self.launch(
program,
args=["Alice"],
disconnectAutomatically=False,
)
self.set_source_breakpoints(source, [breakpoint_line])
self.continue_to_next_stop()
# Interrupt the server which should disconnect all clients.
process.send_signal(signal.SIGINT)
self.dap_server.wait_for_terminated()
self.assertIsNone(
self.dap_server.exit_status,
"Process exited before interrupting lldb-dap server",
)

View File

@ -0,0 +1,10 @@
#include <stdio.h>
int main(int argc, char const *argv[]) {
if (argc == 2) { // breakpoint 1
printf("Hello %s!\n", argv[1]);
} else {
printf("Hello World!\n");
}
return 0;
}

View File

@ -1,8 +1,8 @@
# RUN: lldb-dap --help | FileCheck %s
# CHECK: --connection
# CHECK: -g
# CHECK: --help
# CHECK: -h
# CHECK: --port
# CHECK: -p
# CHECK: --repl-mode
# CHECK: --wait-for-debugger

View File

@ -17,11 +17,11 @@
#include "lldb/API/SBListener.h"
#include "lldb/API/SBProcess.h"
#include "lldb/API/SBStream.h"
#include "lldb/Host/FileSystem.h"
#include "lldb/Utility/Status.h"
#include "lldb/lldb-defines.h"
#include "lldb/lldb-enumerations.h"
#include "llvm/ADT/ArrayRef.h"
#include "llvm/ADT/ScopeExit.h"
#include "llvm/ADT/StringExtras.h"
#include "llvm/ADT/Twine.h"
#include "llvm/Support/Error.h"
@ -58,12 +58,14 @@ const char DEV_NULL[] = "/dev/null";
namespace lldb_dap {
DAP::DAP(llvm::StringRef path, std::ofstream *log, ReplMode repl_mode,
StreamDescriptor input, StreamDescriptor output)
: debug_adaptor_path(path), log(log), input(std::move(input)),
output(std::move(output)), broadcaster("lldb-dap"),
exception_breakpoints(), focus_tid(LLDB_INVALID_THREAD_ID),
stop_at_entry(false), is_attach(false),
DAP::DAP(std::string name, llvm::StringRef path, std::ofstream *log,
StreamDescriptor input, StreamDescriptor output, ReplMode repl_mode,
std::vector<std::string> pre_init_commands)
: name(std::move(name)), debug_adaptor_path(path), log(log),
input(std::move(input)), output(std::move(output)),
broadcaster("lldb-dap"), exception_breakpoints(),
pre_init_commands(std::move(pre_init_commands)),
focus_tid(LLDB_INVALID_THREAD_ID), stop_at_entry(false), is_attach(false),
enable_auto_variable_summaries(false),
enable_synthetic_child_debugging(false),
display_extended_backtrace(false),
@ -224,6 +226,15 @@ llvm::Error DAP::ConfigureIO(std::FILE *overrideOut, std::FILE *overrideErr) {
void DAP::StopIO() {
out.Stop();
err.Stop();
if (event_thread.joinable()) {
broadcaster.BroadcastEventByType(eBroadcastBitStopEventThread);
event_thread.join();
}
if (progress_event_thread.joinable()) {
broadcaster.BroadcastEventByType(eBroadcastBitStopProgressThread);
progress_event_thread.join();
}
}
// Send the JSON in "json_str" to the "out" stream. Correctly send the
@ -249,7 +260,8 @@ void DAP::SendJSON(const llvm::json::Value &json) {
if (log) {
auto now = std::chrono::duration<double>(
std::chrono::system_clock::now().time_since_epoch());
*log << llvm::formatv("{0:f9} <-- ", now.count()).str() << std::endl
*log << llvm::formatv("{0:f9} {1} <-- ", now.count(), name).str()
<< std::endl
<< "Content-Length: " << json_str.size() << "\r\n\r\n"
<< llvm::formatv("{0:2}", json).str() << std::endl;
}
@ -279,7 +291,8 @@ std::string DAP::ReadJSON() {
if (log) {
auto now = std::chrono::duration<double>(
std::chrono::system_clock::now().time_since_epoch());
*log << llvm::formatv("{0:f9} --> ", now.count()).str() << std::endl
*log << llvm::formatv("{0:f9} {1} --> ", now.count(), name).str()
<< std::endl
<< "Content-Length: " << length << "\r\n\r\n";
}
return json_str;
@ -793,7 +806,51 @@ bool DAP::HandleObject(const llvm::json::Object &object) {
return false;
}
void DAP::SendTerminatedEvent() {
// Prevent races if the process exits while we're being asked to disconnect.
llvm::call_once(terminated_event_flag, [&] {
RunTerminateCommands();
// Send a "terminated" event
llvm::json::Object event(CreateTerminatedEventObject(target));
SendJSON(llvm::json::Value(std::move(event)));
});
}
lldb::SBError DAP::Disconnect() { return Disconnect(is_attach); }
lldb::SBError DAP::Disconnect(bool terminateDebuggee) {
lldb::SBError error;
lldb::SBProcess process = target.GetProcess();
auto state = process.GetState();
switch (state) {
case lldb::eStateInvalid:
case lldb::eStateUnloaded:
case lldb::eStateDetached:
case lldb::eStateExited:
break;
case lldb::eStateConnected:
case lldb::eStateAttaching:
case lldb::eStateLaunching:
case lldb::eStateStepping:
case lldb::eStateCrashed:
case lldb::eStateSuspended:
case lldb::eStateStopped:
case lldb::eStateRunning:
debugger.SetAsync(false);
error = terminateDebuggee ? process.Kill() : process.Detach();
debugger.SetAsync(true);
break;
}
SendTerminatedEvent();
disconnecting = true;
return error;
}
llvm::Error DAP::Loop() {
auto stop_io = llvm::make_scope_exit([this]() { StopIO(); });
while (!disconnecting) {
llvm::json::Object object;
lldb_dap::PacketStatus status = GetNextObject(object);

View File

@ -140,6 +140,7 @@ struct SendEventRequestHandler : public lldb::SBCommandPluginInterface {
};
struct DAP {
std::string name;
llvm::StringRef debug_adaptor_path;
std::ofstream *log;
InputStream input;
@ -204,8 +205,9 @@ struct DAP {
// will contain that expression.
std::string last_nonempty_var_expression;
DAP(llvm::StringRef path, std::ofstream *log, ReplMode repl_mode,
StreamDescriptor input, StreamDescriptor output);
DAP(std::string name, llvm::StringRef path, std::ofstream *log,
StreamDescriptor input, StreamDescriptor output, ReplMode repl_mode,
std::vector<std::string> pre_init_commands);
~DAP();
DAP(const DAP &rhs) = delete;
void operator=(const DAP &rhs) = delete;
@ -216,7 +218,8 @@ struct DAP {
///
/// Errors in this operation will be printed to the log file and the IDE's
/// console output as well.
llvm::Error ConfigureIO(std::FILE *overrideOut, std::FILE *overrideErr);
llvm::Error ConfigureIO(std::FILE *overrideOut = nullptr,
std::FILE *overrideErr = nullptr);
/// Stop the redirected IO threads and associated pipes.
void StopIO();
@ -304,6 +307,15 @@ struct DAP {
PacketStatus GetNextObject(llvm::json::Object &object);
bool HandleObject(const llvm::json::Object &object);
/// Disconnect the DAP session.
lldb::SBError Disconnect();
/// Disconnect the DAP session and optionally terminate the debuggee.
lldb::SBError Disconnect(bool terminateDebuggee);
/// Send a "terminated" event to indicate the process is done being debugged.
void SendTerminatedEvent();
llvm::Error Loop();
/// Send a Debug Adapter Protocol reverse request to the IDE.

View File

@ -7,6 +7,8 @@
//===----------------------------------------------------------------------===//
#include "IOStream.h"
#include <fstream>
#include <string>
#if defined(_WIN32)
#include <io.h>
@ -16,9 +18,6 @@
#include <unistd.h>
#endif
#include <fstream>
#include <string>
using namespace lldb_dap;
StreamDescriptor::StreamDescriptor() = default;

View File

@ -17,12 +17,13 @@ def: Flag<["-"], "g">,
Alias<wait_for_debugger>,
HelpText<"Alias for --wait-for-debugger">;
def port: S<"port">,
MetaVarName<"<port>">,
HelpText<"Communicate with the lldb-dap tool over the defined port.">;
def: Separate<["-"], "p">,
Alias<port>,
HelpText<"Alias for --port">;
def connection
: S<"connection">,
MetaVarName<"<connection>">,
HelpText<
"Communicate with the lldb-dap tool over the specified connection. "
"Connections are specified like 'tcp://[host]:port' or "
"'unix:///path'.">;
def launch_target: S<"launch-target">,
MetaVarName<"<target>">,
@ -40,9 +41,12 @@ def debugger_pid: S<"debugger-pid">,
HelpText<"The PID of the lldb-dap instance that sent the launchInTerminal "
"request when using --launch-target.">;
def repl_mode: S<"repl-mode">,
MetaVarName<"<mode>">,
HelpText<"The mode for handling repl evaluation requests, supported modes: variable, command, auto.">;
def repl_mode
: S<"repl-mode">,
MetaVarName<"<mode>">,
HelpText<
"The mode for handling repl evaluation requests, supported modes: "
"variable, command, auto.">;
def pre_init_command: S<"pre-init-command">,
MetaVarName<"<command>">,

View File

@ -62,6 +62,9 @@ Error OutputRedirector::RedirectTo(std::function<void(StringRef)> callback) {
continue;
break;
}
// Skip the null byte used to trigger a Stop.
if (bytes_count == 1 && buffer[0] == '\0')
continue;
StringRef data(buffer, bytes_count);
if (m_stopped)

View File

@ -14,45 +14,59 @@
#include "Watchpoint.h"
#include "lldb/API/SBDeclaration.h"
#include "lldb/API/SBEvent.h"
#include "lldb/API/SBFile.h"
#include "lldb/API/SBInstruction.h"
#include "lldb/API/SBListener.h"
#include "lldb/API/SBMemoryRegionInfo.h"
#include "lldb/API/SBStream.h"
#include "lldb/API/SBStringList.h"
#include "lldb/Host/Config.h"
#include "lldb/Host/MainLoop.h"
#include "lldb/Host/MainLoopBase.h"
#include "lldb/Host/Socket.h"
#include "lldb/Utility/Status.h"
#include "lldb/Utility/UriParser.h"
#include "lldb/lldb-forward.h"
#include "llvm/ADT/ArrayRef.h"
#include "llvm/ADT/DenseMap.h"
#include "llvm/ADT/DenseSet.h"
#include "llvm/ADT/ScopeExit.h"
#include "llvm/ADT/StringExtras.h"
#include "llvm/ADT/StringRef.h"
#include "llvm/Option/Arg.h"
#include "llvm/Option/ArgList.h"
#include "llvm/Option/OptTable.h"
#include "llvm/Option/Option.h"
#include "llvm/Support/Base64.h"
#include "llvm/Support/Errno.h"
#include "llvm/Support/Error.h"
#include "llvm/Support/FileSystem.h"
#include "llvm/Support/InitLLVM.h"
#include "llvm/Support/Path.h"
#include "llvm/Support/PrettyStackTrace.h"
#include "llvm/Support/Signals.h"
#include "llvm/Support/Threading.h"
#include "llvm/Support/raw_ostream.h"
#include <algorithm>
#include <array>
#include <cassert>
#include <climits>
#include <cstdarg>
#include <condition_variable>
#include <cstdint>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <fcntl.h>
#include <fstream>
#include <iostream>
#include <map>
#include <memory>
#include <mutex>
#include <optional>
#include <ostream>
#include <set>
#include <string>
#include <sys/stat.h>
#include <sys/types.h>
#include <thread>
#include <utility>
#include <vector>
#if defined(_WIN32)
@ -68,6 +82,7 @@
#else
#include <netinet/in.h>
#include <sys/socket.h>
#include <sys/un.h>
#include <unistd.h>
#endif
@ -83,6 +98,9 @@ typedef int socklen_t;
#endif
using namespace lldb_dap;
using lldb_private::NativeSocket;
using lldb_private::Socket;
using lldb_private::Status;
namespace {
using namespace llvm::opt;
@ -142,43 +160,6 @@ lldb::SBValueList *GetTopLevelScope(DAP &dap, int64_t variablesReference) {
}
}
SOCKET AcceptConnection(std::ofstream *log, int portno) {
// Accept a socket connection from any host on "portno".
SOCKET newsockfd = -1;
struct sockaddr_in serv_addr, cli_addr;
SOCKET sockfd = socket(AF_INET, SOCK_STREAM, 0);
if (sockfd < 0) {
if (log)
*log << "error: opening socket (" << strerror(errno) << ")" << std::endl;
} else {
memset((char *)&serv_addr, 0, sizeof(serv_addr));
serv_addr.sin_family = AF_INET;
// serv_addr.sin_addr.s_addr = htonl(INADDR_ANY);
serv_addr.sin_addr.s_addr = htonl(INADDR_LOOPBACK);
serv_addr.sin_port = htons(portno);
if (bind(sockfd, (struct sockaddr *)&serv_addr, sizeof(serv_addr)) < 0) {
if (log)
*log << "error: binding socket (" << strerror(errno) << ")"
<< std::endl;
} else {
listen(sockfd, 5);
socklen_t clilen = sizeof(cli_addr);
newsockfd =
llvm::sys::RetryAfterSignal(static_cast<SOCKET>(-1), accept, sockfd,
(struct sockaddr *)&cli_addr, &clilen);
if (newsockfd < 0)
if (log)
*log << "error: accept (" << strerror(errno) << ")" << std::endl;
}
#if defined(_WIN32)
closesocket(sockfd);
#else
close(sockfd);
#endif
}
return newsockfd;
}
std::vector<const char *> MakeArgv(const llvm::ArrayRef<std::string> &strs) {
// Create and return an array of "const char *", one for each C string in
// "strs" and terminate the list with a NULL. This can be used for argument
@ -230,18 +211,6 @@ void SendContinuedEvent(DAP &dap) {
dap.SendJSON(llvm::json::Value(std::move(event)));
}
// Send a "terminated" event to indicate the process is done being
// debugged.
void SendTerminatedEvent(DAP &dap) {
// Prevent races if the process exits while we're being asked to disconnect.
llvm::call_once(dap.terminated_event_flag, [&] {
dap.RunTerminateCommands();
// Send a "terminated" event
llvm::json::Object event(CreateTerminatedEventObject(dap.target));
dap.SendJSON(llvm::json::Value(std::move(event)));
});
}
// Send a thread stopped event for all threads as long as the process
// is stopped.
void SendThreadStoppedEvent(DAP &dap) {
@ -413,6 +382,7 @@ void SendStdOutStdErr(DAP &dap, lldb::SBProcess &process) {
}
void ProgressEventThreadFunction(DAP &dap) {
llvm::set_thread_name(dap.name + ".progress_handler");
lldb::SBListener listener("lldb-dap.progress.listener");
dap.debugger.GetBroadcaster().AddListener(
listener, lldb::SBDebugger::eBroadcastBitProgress |
@ -447,8 +417,10 @@ void ProgressEventThreadFunction(DAP &dap) {
// them prevent multiple threads from writing simultaneously so no locking
// is required.
void EventThreadFunction(DAP &dap) {
llvm::set_thread_name(dap.name + ".event_handler");
lldb::SBEvent event;
lldb::SBListener listener = dap.debugger.GetListener();
dap.broadcaster.AddListener(listener, eBroadcastBitStopEventThread);
bool done = false;
while (!done) {
if (listener.WaitForEvent(1, event)) {
@ -513,7 +485,7 @@ void EventThreadFunction(DAP &dap) {
// launch.json
dap.RunExitCommands();
SendProcessExitedEvent(dap, process);
SendTerminatedEvent(dap);
dap.SendTerminatedEvent();
done = true;
}
break;
@ -1261,41 +1233,12 @@ void request_disconnect(DAP &dap, const llvm::json::Object &request) {
bool defaultTerminateDebuggee = dap.is_attach ? false : true;
bool terminateDebuggee =
GetBoolean(arguments, "terminateDebuggee", defaultTerminateDebuggee);
lldb::SBProcess process = dap.target.GetProcess();
auto state = process.GetState();
switch (state) {
case lldb::eStateInvalid:
case lldb::eStateUnloaded:
case lldb::eStateDetached:
case lldb::eStateExited:
break;
case lldb::eStateConnected:
case lldb::eStateAttaching:
case lldb::eStateLaunching:
case lldb::eStateStepping:
case lldb::eStateCrashed:
case lldb::eStateSuspended:
case lldb::eStateStopped:
case lldb::eStateRunning:
dap.debugger.SetAsync(false);
lldb::SBError error = terminateDebuggee ? process.Kill() : process.Detach();
if (!error.Success())
EmplaceSafeString(response, "error", error.GetCString());
dap.debugger.SetAsync(true);
break;
}
SendTerminatedEvent(dap);
lldb::SBError error = dap.Disconnect(terminateDebuggee);
if (error.Fail())
EmplaceSafeString(response, "error", error.GetCString());
dap.SendJSON(llvm::json::Value(std::move(response)));
if (dap.event_thread.joinable()) {
dap.broadcaster.BroadcastEventByType(eBroadcastBitStopEventThread);
dap.event_thread.join();
}
if (dap.progress_event_thread.joinable()) {
dap.broadcaster.BroadcastEventByType(eBroadcastBitStopProgressThread);
dap.progress_event_thread.join();
}
dap.StopIO();
dap.disconnecting = true;
}
// "ExceptionInfoRequest": {
@ -5052,10 +4995,10 @@ 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 port number causes lldb-dap to start listening
for connections on that port.
parent over stdio. Passing a --connection URI will cause lldb-dap to listen
for a connection in the specified mode.
lldb-dap -p <port>
lldb-dap --connection connection://localhost:<port>
Passing --wait-for-debugger will pause the process at startup and wait for a
debugger to attach to the process.
@ -5147,6 +5090,159 @@ static int DuplicateFileDescriptor(int fd) {
#endif
}
static llvm::Expected<std::pair<Socket::SocketProtocol, std::string>>
validateConnection(llvm::StringRef conn) {
auto uri = lldb_private::URI::Parse(conn);
if (uri && (uri->scheme == "tcp" || uri->scheme == "connect" ||
!uri->hostname.empty() || uri->port)) {
return std::make_pair(
Socket::ProtocolTcp,
formatv("[{0}]:{1}", uri->hostname.empty() ? "0.0.0.0" : uri->hostname,
uri->port.value_or(0)));
}
if (uri && (uri->scheme == "unix" || uri->scheme == "unix-connect" ||
uri->path != "/")) {
return std::make_pair(Socket::ProtocolUnixDomain, uri->path.str());
}
return llvm::createStringError(
"Unsupported connection specifier, expected 'unix-connect:///path' or "
"'connect://[host]:port', got '%s'.",
conn.str().c_str());
}
static llvm::Error
serveConnection(const Socket::SocketProtocol &protocol, const std::string &name,
std::ofstream *log, llvm::StringRef program_path,
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(), ", ");
if (log)
*log << "started with connection listeners " << address << "\n";
llvm::outs() << "Listening for: " << address << "\n";
// Ensure listening address are flushed for calles to retrieve the resolve
// address.
llvm::outs().flush();
static lldb_private::MainLoop g_loop;
llvm::sys::SetInterruptFunction([]() {
g_loop.AddPendingCallback(
[](lldb_private::MainLoopBase &loop) { loop.RequestTermination(); });
});
std::condition_variable dap_sessions_condition;
std::mutex dap_sessions_mutex;
std::map<Socket *, DAP *> dap_sessions;
unsigned int clientCount = 0;
auto handle = listener->Accept(g_loop, [=, &dap_sessions_condition,
&dap_sessions_mutex, &dap_sessions,
&clientCount](
std::unique_ptr<Socket> sock) {
std::string name = llvm::formatv("client_{0}", clientCount++).str();
if (log) {
auto now = std::chrono::duration<double>(
std::chrono::system_clock::now().time_since_epoch());
*log << llvm::formatv("{0:f9}", now.count()).str()
<< " client connected: " << name << "\n";
}
// Move the client into a background thread to unblock accepting the next
// client.
std::thread client([=, &dap_sessions_condition, &dap_sessions_mutex,
&dap_sessions, sock = std::move(sock)]() {
llvm::set_thread_name(name + ".runloop");
StreamDescriptor input =
StreamDescriptor::from_socket(sock->GetNativeSocket(), false);
// Close the output last for the best chance at error reporting.
StreamDescriptor output =
StreamDescriptor::from_socket(sock->GetNativeSocket(), false);
DAP dap = DAP(name, program_path, log, std::move(input),
std::move(output), default_repl_mode, pre_init_commands);
if (auto Err = dap.ConfigureIO()) {
llvm::logAllUnhandledErrors(std::move(Err), llvm::errs(),
"Failed to configure stdout redirect: ");
return;
}
RegisterRequestCallbacks(dap);
{
std::scoped_lock lock(dap_sessions_mutex);
dap_sessions[sock.get()] = &dap;
}
if (auto Err = dap.Loop()) {
llvm::logAllUnhandledErrors(std::move(Err), llvm::errs(),
"DAP session error: ");
}
if (log) {
auto now = std::chrono::duration<double>(
std::chrono::system_clock::now().time_since_epoch());
*log << llvm::formatv("{0:f9}", now.count()).str()
<< " client closed: " << name << "\n";
}
std::unique_lock lock(dap_sessions_mutex);
dap_sessions.erase(sock.get());
std::notify_all_at_thread_exit(dap_sessions_condition, std::move(lock));
});
client.detach();
});
if (auto Err = handle.takeError()) {
return Err;
}
status = g_loop.Run();
if (status.Fail()) {
return status.takeError();
}
if (log)
*log << "lldb-dap server shutdown requested, disconnecting remaining "
"clients...\n";
bool client_failed = false;
{
std::scoped_lock lock(dap_sessions_mutex);
for (auto [sock, dap] : dap_sessions) {
auto error = dap->Disconnect();
if (error.Fail()) {
client_failed = true;
llvm::errs() << "DAP client " << dap->name
<< " disconnected failed: " << error.GetCString() << "\n";
}
// Close the socket to ensure the DAP::Loop read finishes.
sock->Close();
}
}
// Wait for all clients to finish disconnecting.
std::unique_lock lock(dap_sessions_mutex);
dap_sessions_condition.wait(lock, [&] { return dap_sessions.empty(); });
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__)
@ -5182,9 +5278,9 @@ int main(int argc, char *argv[]) {
} else if (repl_mode_value == "command") {
default_repl_mode = ReplMode::Command;
} else {
llvm::errs()
<< "'" << repl_mode_value
<< "' is not a valid option, use 'variable', 'command' or 'auto'.\n";
llvm::errs() << "'" << repl_mode_value
<< "' is not a valid option, use 'variable', 'command' or "
"'auto'.\n";
return EXIT_FAILURE;
}
}
@ -5217,15 +5313,10 @@ int main(int argc, char *argv[]) {
}
}
int portno = -1;
if (auto *arg = input_args.getLastArg(OPT_port)) {
const auto *optarg = arg->getValue();
char *remainder;
portno = strtol(optarg, &remainder, 0);
if (remainder == optarg || *remainder != '\0') {
fprintf(stderr, "'%s' is not a valid port number.\n", optarg);
return EXIT_FAILURE;
}
std::string connection;
if (auto *arg = input_args.getLastArg(OPT_connection)) {
const auto *path = arg->getValue();
connection.assign(path);
}
#if !defined(_WIN32)
@ -5253,72 +5344,75 @@ int main(int argc, char *argv[]) {
auto terminate_debugger =
llvm::make_scope_exit([] { lldb::SBDebugger::Terminate(); });
StreamDescriptor input;
StreamDescriptor output;
std::FILE *redirectOut = nullptr;
std::FILE *redirectErr = nullptr;
if (portno != -1) {
printf("Listening on port %i...\n", portno);
SOCKET socket_fd = AcceptConnection(log.get(), portno);
if (socket_fd < 0)
return EXIT_FAILURE;
std::vector<std::string> pre_init_commands;
for (const std::string &arg :
input_args.getAllArgValues(OPT_pre_init_command)) {
pre_init_commands.push_back(arg);
}
input = StreamDescriptor::from_socket(socket_fd, true);
output = StreamDescriptor::from_socket(socket_fd, false);
} else {
#if defined(_WIN32)
// Windows opens stdout and stdin in text mode which converts \n to 13,10
// while the value is just 10 on Darwin/Linux. Setting the file mode to
// binary fixes this.
int result = _setmode(fileno(stdout), _O_BINARY);
assert(result);
result = _setmode(fileno(stdin), _O_BINARY);
UNUSED_IF_ASSERT_DISABLED(result);
assert(result);
#endif
int stdout_fd = DuplicateFileDescriptor(fileno(stdout));
if (stdout_fd == -1) {
llvm::logAllUnhandledErrors(
llvm::errorCodeToError(llvm::errnoAsErrorCode()), llvm::errs(),
"Failed to configure stdout redirect: ");
if (!connection.empty()) {
auto maybeProtoclAndName = validateConnection(connection);
if (auto Err = maybeProtoclAndName.takeError()) {
llvm::logAllUnhandledErrors(std::move(Err), llvm::errs(),
"Invalid connection: ");
return EXIT_FAILURE;
}
redirectOut = stdout;
redirectErr = stderr;
Socket::SocketProtocol protocol;
std::string name;
std::tie(protocol, name) = *maybeProtoclAndName;
if (auto Err = serveConnection(protocol, name, log.get(), program_path,
default_repl_mode, pre_init_commands)) {
llvm::logAllUnhandledErrors(std::move(Err), llvm::errs(),
"Connection failed: ");
return EXIT_FAILURE;
}
input = StreamDescriptor::from_file(fileno(stdin), false);
output = StreamDescriptor::from_file(stdout_fd, false);
return EXIT_SUCCESS;
}
DAP dap = DAP(program_path.str(), log.get(), default_repl_mode,
std::move(input), std::move(output));
#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;
}
StreamDescriptor input = StreamDescriptor::from_file(fileno(stdin), false);
StreamDescriptor output = StreamDescriptor::from_file(stdout_fd, false);
DAP dap = DAP("stdin/stdout", program_path, log.get(), std::move(input),
std::move(output), default_repl_mode, pre_init_commands);
// stdout/stderr redirection to the IDE's console
if (auto Err = dap.ConfigureIO(redirectOut, redirectErr)) {
if (auto Err = dap.ConfigureIO(stdout, stderr)) {
llvm::logAllUnhandledErrors(std::move(Err), llvm::errs(),
"Failed to configure lldb-dap IO operations: ");
"Failed to configure stdout redirect: ");
return EXIT_FAILURE;
}
RegisterRequestCallbacks(dap);
for (const std::string &arg :
input_args.getAllArgValues(OPT_pre_init_command)) {
dap.pre_init_commands.push_back(arg);
}
// used only by TestVSCode_redirection_to_console.py
if (getenv("LLDB_DAP_TEST_STDOUT_STDERR_REDIRECTION") != nullptr)
redirection_test();
bool CleanExit = true;
if (auto Err = dap.Loop()) {
if (log)
*log << "Transport Error: " << llvm::toString(std::move(Err)) << "\n";
CleanExit = false;
llvm::logAllUnhandledErrors(std::move(Err), llvm::errs(),
"DAP session error: ");
return EXIT_FAILURE;
}
return CleanExit ? EXIT_SUCCESS : EXIT_FAILURE;
return EXIT_SUCCESS;
}