Skip to content

Commit f946fd7

Browse files
committed
[lldb-dap] Refactoring lldb-dap port listening mode to allow multiple connections.
This adjusts the lldb-dap listening mode to accept multiple clients. Each client initializes a new instance of DAP and an associated `lldb::SBDebugger` instance. The listening mode is configured with the `--connection` option and supports listening on a port or a unix socket on supported platforms.
1 parent 1bc5fe6 commit f946fd7

File tree

10 files changed

+351
-133
lines changed

10 files changed

+351
-133
lines changed

lldb/packages/Python/lldbsuite/test/tools/lldb-dap/dap_server.py

Lines changed: 49 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -903,7 +903,7 @@ def request_setBreakpoints(self, file_path, line_array, data=None):
903903
"sourceModified": False,
904904
}
905905
if line_array is not None:
906-
args_dict["lines"] = "%s" % line_array
906+
args_dict["lines"] = line_array
907907
breakpoints = []
908908
for i, line in enumerate(line_array):
909909
breakpoint_data = None
@@ -1150,11 +1150,12 @@ def request_setInstructionBreakpoints(self, memory_reference=[]):
11501150
}
11511151
return self.send_recv(command_dict)
11521152

1153+
11531154
class DebugAdaptorServer(DebugCommunication):
11541155
def __init__(
11551156
self,
11561157
executable=None,
1157-
port=None,
1158+
connection=None,
11581159
init_commands=[],
11591160
log_file=None,
11601161
env=None,
@@ -1167,21 +1168,61 @@ def __init__(
11671168

11681169
if log_file:
11691170
adaptor_env["LLDBDAP_LOG"] = log_file
1171+
args = [executable]
1172+
1173+
if connection is not None:
1174+
args.append("--connection")
1175+
args.append(connection)
1176+
11701177
self.process = subprocess.Popen(
1171-
[executable],
1178+
args,
11721179
stdin=subprocess.PIPE,
11731180
stdout=subprocess.PIPE,
11741181
stderr=subprocess.PIPE,
11751182
env=adaptor_env,
11761183
)
1184+
1185+
if connection is not None:
1186+
# If the process was also launched, parse the connection from the
1187+
# resolved connection. For example, if the connection
1188+
# `connection://localhost:0` was specified then the OS would pick a
1189+
# random port for listening and lldb-dap would print the listening
1190+
# port to stdout.
1191+
if self.process is not None:
1192+
# lldb-dap will print the listening address once the listener is
1193+
# made to stdout. The listener is formatted like
1194+
# `connection://host:port` or `unix-connection:///path`.
1195+
expected_prefix = "Listening for: "
1196+
out = self.process.stdout.readline().decode()
1197+
if not out.startswith(expected_prefix):
1198+
self.process.kill()
1199+
raise ValueError(
1200+
"lldb-dap failed to print listening address, expected '{}', got '{}'".format(
1201+
expected_prefix, out
1202+
)
1203+
)
1204+
1205+
# If the listener expanded into multiple addresses, use the first.
1206+
connection = (
1207+
out.removeprefix(expected_prefix).rstrip("\r\n").split(",", 1)[0]
1208+
)
1209+
1210+
if connection.startswith("unix-connect://"): # unix-connect:///path
1211+
s = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
1212+
s.connect(connection.removeprefix("unix-connect://"))
1213+
elif connection.startswith("connection://"): # connection://[host]:port
1214+
host, port = connection.removeprefix("connection://").rsplit(":", 1)
1215+
# create_connection with try both ipv4 and ipv6.
1216+
s = socket.create_connection((host.strip("[]"), int(port)))
1217+
else:
1218+
raise ValueError("invalid connection: {}".format(connection))
11771219
DebugCommunication.__init__(
1178-
self, self.process.stdout, self.process.stdin, init_commands, log_file
1220+
self, s.makefile("rb"), s.makefile("wb"), init_commands, log_file
11791221
)
1180-
elif port is not None:
1181-
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
1182-
s.connect(("127.0.0.1", port))
1222+
self.connection = connection
1223+
else:
11831224
DebugCommunication.__init__(
1184-
self, s.makefile("r"), s.makefile("w"), init_commands
1225+
self, self.process.stdout, self.process.stdin, init_commands, log_file
11851226
)
11861227

11871228
def get_pid(self):

lldb/packages/Python/lldbsuite/test/tools/lldb-dap/lldbdap_testcase.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import os
22
import time
3+
import subprocess
34

45
import dap_server
56
from lldbsuite.test.lldbtest import *
@@ -10,17 +11,18 @@
1011
class DAPTestCaseBase(TestBase):
1112
# set timeout based on whether ASAN was enabled or not. Increase
1213
# timeout by a factor of 10 if ASAN is enabled.
13-
timeoutval = 10 * (10 if ('ASAN_OPTIONS' in os.environ) else 1)
14+
timeoutval = 10 * (10 if ("ASAN_OPTIONS" in os.environ) else 1)
1415
NO_DEBUG_INFO_TESTCASE = True
1516

16-
def create_debug_adaptor(self, lldbDAPEnv=None):
17+
def create_debug_adaptor(self, lldbDAPEnv=None, connection=None):
1718
"""Create the Visual Studio Code debug adaptor"""
1819
self.assertTrue(
1920
is_exe(self.lldbDAPExec), "lldb-dap must exist and be executable"
2021
)
2122
log_file_path = self.getBuildArtifact("dap.txt")
2223
self.dap_server = dap_server.DebugAdaptorServer(
2324
executable=self.lldbDAPExec,
25+
connection=connection,
2426
init_commands=self.setUpCommands(),
2527
log_file=log_file_path,
2628
env=lldbDAPEnv,
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
C_SOURCES := main.c
2+
3+
include Makefile.rules
Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
"""
2+
Test lldb-dap server integration.
3+
"""
4+
5+
import os
6+
import tempfile
7+
8+
import dap_server
9+
from lldbsuite.test.decorators import *
10+
from lldbsuite.test.lldbtest import *
11+
import lldbdap_testcase
12+
13+
14+
class TestDAP_server(lldbdap_testcase.DAPTestCaseBase):
15+
def do_test_server(self, connection):
16+
self.build()
17+
18+
log_file_path = self.getBuildArtifact("dap.txt")
19+
server = dap_server.DebugAdaptorServer(
20+
executable=self.lldbDAPExec,
21+
connection=connection,
22+
init_commands=self.setUpCommands(),
23+
log_file=log_file_path,
24+
)
25+
26+
def cleanup():
27+
server.terminate()
28+
29+
self.addTearDownHook(cleanup)
30+
31+
self.dap_server = dap_server.DebugAdaptorServer(
32+
connection=server.connection,
33+
)
34+
program = self.getBuildArtifact("a.out")
35+
source = "main.c"
36+
breakpoint_line = line_number(source, "// breakpoint")
37+
38+
# Initial connection over the connection.
39+
self.launch(
40+
program,
41+
args=["Alice"],
42+
disconnectAutomatically=False,
43+
)
44+
self.set_source_breakpoints(source, [breakpoint_line])
45+
self.continue_to_next_stop()
46+
self.continue_to_exit()
47+
output = self.get_stdout()
48+
self.assertEqual(output, "Hello Alice!\r\n")
49+
self.dap_server.request_disconnect()
50+
51+
# Second connection over the connection.
52+
self.dap_server = dap_server.DebugAdaptorServer(
53+
connection=server.connection,
54+
)
55+
self.launch(
56+
program,
57+
args=["Bob"],
58+
disconnectAutomatically=False,
59+
)
60+
self.set_source_breakpoints(source, [breakpoint_line])
61+
self.continue_to_next_stop()
62+
self.continue_to_exit()
63+
output = self.get_stdout()
64+
self.assertEqual(output, "Hello Bob!\r\n")
65+
66+
def test_server_port(self):
67+
"""
68+
Test launching a binary with a lldb-dap in server mode on a specific port.
69+
"""
70+
self.do_test_server(connection="tcp://localhost:0")
71+
72+
@skipIfWindows
73+
def test_server_unix_socket(self):
74+
"""
75+
Test launching a binary with a lldb-dap in server mode on a unix socket.
76+
"""
77+
dir = tempfile.gettempdir()
78+
name = dir + "/dap-connection-" + str(os.getpid())
79+
80+
def cleanup():
81+
os.unlink(name)
82+
83+
self.addTearDownHook(cleanup)
84+
self.do_test_server(connection="unix://" + name)
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
#include <stdio.h>
2+
3+
int main(int argc, char const *argv[]) {
4+
if (argc == 2) { // breakpoint 1
5+
printf("Hello %s!\n", argv[1]);
6+
} else {
7+
printf("Hello World!\n");
8+
}
9+
return 0;
10+
}

lldb/test/Shell/DAP/TestOptions.test

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
# RUN: lldb-dap --help | FileCheck %s
2+
# CHECK: --connection
23
# CHECK: -g
34
# CHECK: --help
45
# CHECK: -h
5-
# CHECK: --port
6-
# CHECK: -p
6+
# CHECK: --repl-mode
77
# CHECK: --wait-for-debugger
88

lldb/tools/lldb-dap/DAP.cpp

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -58,9 +58,9 @@ const char DEV_NULL[] = "/dev/null";
5858

5959
namespace lldb_dap {
6060

61-
DAP::DAP(llvm::StringRef path, std::ofstream *log, ReplMode repl_mode,
62-
StreamDescriptor input, StreamDescriptor output)
63-
: debug_adaptor_path(path), log(log), input(std::move(input)),
61+
DAP::DAP(std::string name, llvm::StringRef path, std::ofstream *log,
62+
ReplMode repl_mode, StreamDescriptor input, StreamDescriptor output)
63+
: name(name), debug_adaptor_path(path), log(log), input(std::move(input)),
6464
output(std::move(output)), broadcaster("lldb-dap"),
6565
exception_breakpoints(), focus_tid(LLDB_INVALID_THREAD_ID),
6666
stop_at_entry(false), is_attach(false),
@@ -249,7 +249,8 @@ void DAP::SendJSON(const llvm::json::Value &json) {
249249
if (log) {
250250
auto now = std::chrono::duration<double>(
251251
std::chrono::system_clock::now().time_since_epoch());
252-
*log << llvm::formatv("{0:f9} <-- ", now.count()).str() << std::endl
252+
*log << llvm::formatv("{0:f9} {1} <-- ", now.count(), name).str()
253+
<< std::endl
253254
<< "Content-Length: " << json_str.size() << "\r\n\r\n"
254255
<< llvm::formatv("{0:2}", json).str() << std::endl;
255256
}
@@ -279,7 +280,8 @@ std::string DAP::ReadJSON() {
279280
if (log) {
280281
auto now = std::chrono::duration<double>(
281282
std::chrono::system_clock::now().time_since_epoch());
282-
*log << llvm::formatv("{0:f9} --> ", now.count()).str() << std::endl
283+
*log << llvm::formatv("{0:f9} {1} --> ", now.count(), name).str()
284+
<< std::endl
283285
<< "Content-Length: " << length << "\r\n\r\n";
284286
}
285287
return json_str;

lldb/tools/lldb-dap/DAP.h

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -139,6 +139,7 @@ struct SendEventRequestHandler : public lldb::SBCommandPluginInterface {
139139
};
140140

141141
struct DAP {
142+
std::string name;
142143
llvm::StringRef debug_adaptor_path;
143144
std::ofstream *log;
144145
InputStream input;
@@ -203,8 +204,8 @@ struct DAP {
203204
// will contain that expression.
204205
std::string last_nonempty_var_expression;
205206

206-
DAP(llvm::StringRef path, std::ofstream *log, ReplMode repl_mode,
207-
StreamDescriptor input, StreamDescriptor output);
207+
DAP(std::string name, llvm::StringRef path, std::ofstream *log,
208+
ReplMode repl_mode, StreamDescriptor input, StreamDescriptor output);
208209
~DAP();
209210
DAP(const DAP &rhs) = delete;
210211
void operator=(const DAP &rhs) = delete;

lldb/tools/lldb-dap/Options.td

Lines changed: 13 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -17,12 +17,13 @@ def: Flag<["-"], "g">,
1717
Alias<wait_for_debugger>,
1818
HelpText<"Alias for --wait-for-debugger">;
1919

20-
def port: S<"port">,
21-
MetaVarName<"<port>">,
22-
HelpText<"Communicate with the lldb-dap tool over the defined port.">;
23-
def: Separate<["-"], "p">,
24-
Alias<port>,
25-
HelpText<"Alias for --port">;
20+
def connection
21+
: S<"connection">,
22+
MetaVarName<"<connection>">,
23+
HelpText<
24+
"Communicate with the lldb-dap tool over the specified connection. "
25+
"Connections are specified like 'tcp://[host]:port' or "
26+
"'unix:///path'.">;
2627

2728
def launch_target: S<"launch-target">,
2829
MetaVarName<"<target>">,
@@ -40,9 +41,12 @@ def debugger_pid: S<"debugger-pid">,
4041
HelpText<"The PID of the lldb-dap instance that sent the launchInTerminal "
4142
"request when using --launch-target.">;
4243

43-
def repl_mode: S<"repl-mode">,
44-
MetaVarName<"<mode>">,
45-
HelpText<"The mode for handling repl evaluation requests, supported modes: variable, command, auto.">;
44+
def repl_mode
45+
: S<"repl-mode">,
46+
MetaVarName<"<mode>">,
47+
HelpText<
48+
"The mode for handling repl evaluation requests, supported modes: "
49+
"variable, command, auto.">;
4650

4751
def pre_init_command: S<"pre-init-command">,
4852
MetaVarName<"<command>">,

0 commit comments

Comments
 (0)