Skip to content

Commit 998b28f

Browse files
authored
[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.
1 parent 1bb4306 commit 998b28f

File tree

12 files changed

+543
-204
lines changed

12 files changed

+543
-204
lines changed

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

Lines changed: 75 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -927,7 +927,7 @@ def request_setBreakpoints(self, file_path, line_array, data=None):
927927
"sourceModified": False,
928928
}
929929
if line_array is not None:
930-
args_dict["lines"] = "%s" % line_array
930+
args_dict["lines"] = line_array
931931
breakpoints = []
932932
for i, line in enumerate(line_array):
933933
breakpoint_data = None
@@ -1170,40 +1170,88 @@ def request_setInstructionBreakpoints(self, memory_reference=[]):
11701170
}
11711171
return self.send_recv(command_dict)
11721172

1173+
11731174
class DebugAdaptorServer(DebugCommunication):
11741175
def __init__(
11751176
self,
11761177
executable=None,
1177-
port=None,
1178+
connection=None,
11781179
init_commands=[],
11791180
log_file=None,
11801181
env=None,
11811182
):
11821183
self.process = None
1184+
self.connection = None
11831185
if executable is not None:
1184-
adaptor_env = os.environ.copy()
1185-
if env is not None:
1186-
adaptor_env.update(env)
1187-
1188-
if log_file:
1189-
adaptor_env["LLDBDAP_LOG"] = log_file
1190-
self.process = subprocess.Popen(
1191-
[executable],
1192-
stdin=subprocess.PIPE,
1193-
stdout=subprocess.PIPE,
1194-
stderr=subprocess.PIPE,
1195-
env=adaptor_env,
1186+
process, connection = DebugAdaptorServer.launch(
1187+
executable=executable, connection=connection, env=env, log_file=log_file
11961188
)
1189+
self.process = process
1190+
self.connection = connection
1191+
1192+
if connection is not None:
1193+
scheme, address = connection.split("://")
1194+
if scheme == "unix-connect": # unix-connect:///path
1195+
s = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
1196+
s.connect(address)
1197+
elif scheme == "connection": # connection://[host]:port
1198+
host, port = address.rsplit(":", 1)
1199+
# create_connection with try both ipv4 and ipv6.
1200+
s = socket.create_connection((host.strip("[]"), int(port)))
1201+
else:
1202+
raise ValueError("invalid connection: {}".format(connection))
11971203
DebugCommunication.__init__(
1198-
self, self.process.stdout, self.process.stdin, init_commands, log_file
1204+
self, s.makefile("rb"), s.makefile("wb"), init_commands, log_file
11991205
)
1200-
elif port is not None:
1201-
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
1202-
s.connect(("127.0.0.1", port))
1206+
self.connection = connection
1207+
else:
12031208
DebugCommunication.__init__(
1204-
self, s.makefile("r"), s.makefile("w"), init_commands
1209+
self, self.process.stdout, self.process.stdin, init_commands, log_file
1210+
)
1211+
1212+
@classmethod
1213+
def launch(cls, /, executable, env=None, log_file=None, connection=None):
1214+
adaptor_env = os.environ.copy()
1215+
if env is not None:
1216+
adaptor_env.update(env)
1217+
1218+
if log_file:
1219+
adaptor_env["LLDBDAP_LOG"] = log_file
1220+
args = [executable]
1221+
1222+
if connection is not None:
1223+
args.append("--connection")
1224+
args.append(connection)
1225+
1226+
process = subprocess.Popen(
1227+
args,
1228+
stdin=subprocess.PIPE,
1229+
stdout=subprocess.PIPE,
1230+
stderr=subprocess.PIPE,
1231+
env=adaptor_env,
1232+
)
1233+
1234+
if connection is None:
1235+
return (process, None)
1236+
1237+
# lldb-dap will print the listening address once the listener is
1238+
# made to stdout. The listener is formatted like
1239+
# `connection://host:port` or `unix-connection:///path`.
1240+
expected_prefix = "Listening for: "
1241+
out = process.stdout.readline().decode()
1242+
if not out.startswith(expected_prefix):
1243+
self.process.kill()
1244+
raise ValueError(
1245+
"lldb-dap failed to print listening address, expected '{}', got '{}'".format(
1246+
expected_prefix, out
1247+
)
12051248
)
12061249

1250+
# If the listener expanded into multiple addresses, use the first.
1251+
connection = out.removeprefix(expected_prefix).rstrip("\r\n").split(",", 1)[0]
1252+
1253+
return (process, connection)
1254+
12071255
def get_pid(self):
12081256
if self.process:
12091257
return self.process.pid
@@ -1369,10 +1417,9 @@ def main():
13691417
)
13701418

13711419
parser.add_option(
1372-
"--port",
1373-
type="int",
1374-
dest="port",
1375-
help="Attach a socket to a port instead of using STDIN for VSCode",
1420+
"--connection",
1421+
dest="connection",
1422+
help="Attach a socket connection of using STDIN for VSCode",
13761423
default=None,
13771424
)
13781425

@@ -1518,15 +1565,16 @@ def main():
15181565

15191566
(options, args) = parser.parse_args(sys.argv[1:])
15201567

1521-
if options.vscode_path is None and options.port is None:
1568+
if options.vscode_path is None and options.connection is None:
15221569
print(
15231570
"error: must either specify a path to a Visual Studio Code "
15241571
"Debug Adaptor vscode executable path using the --vscode "
1525-
"option, or a port to attach to for an existing lldb-dap "
1526-
"using the --port option"
1572+
"option, or using the --connection option"
15271573
)
15281574
return
1529-
dbg = DebugAdaptorServer(executable=options.vscode_path, port=options.port)
1575+
dbg = DebugAdaptorServer(
1576+
executable=options.vscode_path, connection=options.connection
1577+
)
15301578
if options.debug:
15311579
raw_input('Waiting for debugger to attach pid "%i"' % (dbg.get_pid()))
15321580
if options.replay:

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: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
"""
2+
Test lldb-dap server integration.
3+
"""
4+
5+
import os
6+
import signal
7+
import tempfile
8+
9+
import dap_server
10+
from lldbsuite.test.decorators import *
11+
from lldbsuite.test.lldbtest import *
12+
import lldbdap_testcase
13+
14+
15+
class TestDAP_server(lldbdap_testcase.DAPTestCaseBase):
16+
def start_server(self, connection):
17+
log_file_path = self.getBuildArtifact("dap.txt")
18+
(process, connection) = dap_server.DebugAdaptorServer.launch(
19+
executable=self.lldbDAPExec,
20+
connection=connection,
21+
log_file=log_file_path,
22+
)
23+
24+
def cleanup():
25+
process.terminate()
26+
27+
self.addTearDownHook(cleanup)
28+
29+
return (process, connection)
30+
31+
def run_debug_session(self, connection, name):
32+
self.dap_server = dap_server.DebugAdaptorServer(
33+
connection=connection,
34+
)
35+
program = self.getBuildArtifact("a.out")
36+
source = "main.c"
37+
breakpoint_line = line_number(source, "// breakpoint")
38+
39+
self.launch(
40+
program,
41+
args=[name],
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, f"Hello {name}!\r\n")
49+
self.dap_server.request_disconnect()
50+
51+
def test_server_port(self):
52+
"""
53+
Test launching a binary with a lldb-dap in server mode on a specific port.
54+
"""
55+
self.build()
56+
(_, connection) = self.start_server(connection="tcp://localhost:0")
57+
self.run_debug_session(connection, "Alice")
58+
self.run_debug_session(connection, "Bob")
59+
60+
@skipIfWindows
61+
def test_server_unix_socket(self):
62+
"""
63+
Test launching a binary with a lldb-dap in server mode on a unix socket.
64+
"""
65+
dir = tempfile.gettempdir()
66+
name = dir + "/dap-connection-" + str(os.getpid())
67+
68+
def cleanup():
69+
os.unlink(name)
70+
71+
self.addTearDownHook(cleanup)
72+
73+
self.build()
74+
(_, connection) = self.start_server(connection="unix://" + name)
75+
self.run_debug_session(connection, "Alice")
76+
self.run_debug_session(connection, "Bob")
77+
78+
@skipIfWindows
79+
def test_server_interrupt(self):
80+
"""
81+
Test launching a binary with lldb-dap in server mode and shutting down the server while the debug session is still active.
82+
"""
83+
self.build()
84+
(process, connection) = self.start_server(connection="tcp://localhost:0")
85+
self.dap_server = dap_server.DebugAdaptorServer(
86+
connection=connection,
87+
)
88+
program = self.getBuildArtifact("a.out")
89+
source = "main.c"
90+
breakpoint_line = line_number(source, "// breakpoint")
91+
92+
self.launch(
93+
program,
94+
args=["Alice"],
95+
disconnectAutomatically=False,
96+
)
97+
self.set_source_breakpoints(source, [breakpoint_line])
98+
self.continue_to_next_stop()
99+
100+
# Interrupt the server which should disconnect all clients.
101+
process.send_signal(signal.SIGINT)
102+
103+
self.dap_server.wait_for_terminated()
104+
self.assertIsNone(
105+
self.dap_server.exit_status,
106+
"Process exited before interrupting lldb-dap server",
107+
)
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

0 commit comments

Comments
 (0)