Skip to content

Commit 83d1599

Browse files
authored
[lldb-dap] Addressing orphaned processes in tests. (llvm#166205)
In lldb-dap tests, we sometimes spawn subprocesses directly but do not always correctly clean them up. This can cause some tests, like the `TestDAP_disconnect.test_attach` to hang and not properly respect timeouts. To fix this, I am passing the `lldbtest.Base.spawnSubprocess` helper to the adapter client so it can be used spawn subprocesses in a way that we can ensure they're cleaned up.
1 parent 89ec96b commit 83d1599

File tree

4 files changed

+45
-43
lines changed

4 files changed

+45
-43
lines changed

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

Lines changed: 38 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,10 @@
3232
# timeout by a factor of 10 if ASAN is enabled.
3333
DEFAULT_TIMEOUT = 10 * (10 if ("ASAN_OPTIONS" in os.environ) else 1)
3434

35+
# See lldbtest.Base.spawnSubprocess, which should help ensure any processes
36+
# created by the DAP client are terminated correctly when the test ends.
37+
SpawnHelperCallback = Callable[[str, List[str], List[str]], subprocess.Popen]
38+
3539
## DAP type references
3640

3741

@@ -191,14 +195,16 @@ def __init__(
191195
self,
192196
recv: BinaryIO,
193197
send: BinaryIO,
194-
init_commands: list[str],
195-
log_file: Optional[TextIO] = None,
198+
init_commands: Optional[List[str]] = None,
199+
log_file: Optional[str] = None,
200+
spawn_helper: Optional[SpawnHelperCallback] = None,
196201
):
197202
# For debugging test failures, try setting `trace_file = sys.stderr`.
198203
self.trace_file: Optional[TextIO] = None
199204
self.log_file = log_file
200205
self.send = send
201206
self.recv = recv
207+
self.spawn_helper = spawn_helper
202208

203209
# Packets that have been received and processed but have not yet been
204210
# requested by a test case.
@@ -211,7 +217,7 @@ def __init__(
211217
self._recv_thread = threading.Thread(target=self._read_packet_thread)
212218

213219
# session state
214-
self.init_commands = init_commands
220+
self.init_commands = init_commands if init_commands else []
215221
self.exit_status: Optional[int] = None
216222
self.capabilities: Dict = {}
217223
self.initialized: bool = False
@@ -310,11 +316,6 @@ def collect_output(
310316
output += self.get_output(category, clear=clear)
311317
return output
312318

313-
def _enqueue_recv_packet(self, packet: Optional[ProtocolMessage]):
314-
with self.recv_condition:
315-
self.recv_packets.append(packet)
316-
self.recv_condition.notify()
317-
318319
def _handle_recv_packet(self, packet: Optional[ProtocolMessage]) -> bool:
319320
"""Handles an incoming packet.
320321
@@ -460,22 +461,11 @@ def _handle_reverse_request(self, request: Request) -> None:
460461
self.reverse_requests.append(request)
461462
arguments = request.get("arguments")
462463
if request["command"] == "runInTerminal" and arguments is not None:
463-
in_shell = arguments.get("argsCanBeInterpretedByShell", False)
464-
print("spawning...", arguments["args"])
465-
proc = subprocess.Popen(
466-
arguments["args"],
467-
env=arguments.get("env", {}),
468-
cwd=arguments.get("cwd", None),
469-
stdin=subprocess.DEVNULL,
470-
stdout=sys.stderr,
471-
stderr=sys.stderr,
472-
shell=in_shell,
473-
)
474-
body = {}
475-
if in_shell:
476-
body["shellProcessId"] = proc.pid
477-
else:
478-
body["processId"] = proc.pid
464+
assert self.spawn_helper is not None, "Not configured to spawn subprocesses"
465+
[exe, *args] = arguments["args"]
466+
env = [f"{k}={v}" for k, v in arguments.get("env", {}).items()]
467+
proc = self.spawn_helper(exe, args, env)
468+
body = {"processId": proc.pid}
479469
self.send_packet(
480470
{
481471
"type": "response",
@@ -1501,12 +1491,14 @@ def request_setInstructionBreakpoints(self, memory_reference=[]):
15011491
class DebugAdapterServer(DebugCommunication):
15021492
def __init__(
15031493
self,
1494+
*,
15041495
executable: Optional[str] = None,
15051496
connection: Optional[str] = None,
1506-
init_commands: list[str] = [],
1507-
log_file: Optional[TextIO] = None,
1508-
env: Optional[dict[str, str]] = None,
1509-
additional_args: list[str] = [],
1497+
init_commands: Optional[list[str]] = None,
1498+
log_file: Optional[str] = None,
1499+
env: Optional[Dict[str, str]] = None,
1500+
additional_args: Optional[List[str]] = None,
1501+
spawn_helper: Optional[SpawnHelperCallback] = None,
15101502
):
15111503
self.process = None
15121504
self.connection = None
@@ -1532,36 +1524,45 @@ def __init__(
15321524
s = socket.create_connection((host.strip("[]"), int(port)))
15331525
else:
15341526
raise ValueError("invalid connection: {}".format(connection))
1535-
DebugCommunication.__init__(
1536-
self, s.makefile("rb"), s.makefile("wb"), init_commands, log_file
1527+
super().__init__(
1528+
s.makefile("rb"),
1529+
s.makefile("wb"),
1530+
init_commands,
1531+
log_file,
1532+
spawn_helper,
15371533
)
15381534
self.connection = connection
15391535
else:
1540-
DebugCommunication.__init__(
1541-
self, self.process.stdout, self.process.stdin, init_commands, log_file
1536+
super().__init__(
1537+
self.process.stdout,
1538+
self.process.stdin,
1539+
init_commands,
1540+
log_file,
1541+
spawn_helper,
15421542
)
15431543

15441544
@classmethod
15451545
def launch(
15461546
cls,
15471547
*,
15481548
executable: str,
1549-
env: Optional[dict[str, str]] = None,
1550-
log_file: Optional[TextIO] = None,
1549+
env: Optional[Dict[str, str]] = None,
1550+
log_file: Optional[str] = None,
15511551
connection: Optional[str] = None,
15521552
connection_timeout: Optional[int] = None,
1553-
additional_args: list[str] = [],
1553+
additional_args: Optional[List[str]] = None,
15541554
) -> tuple[subprocess.Popen, Optional[str]]:
15551555
adapter_env = os.environ.copy()
1556-
if env is not None:
1556+
if env:
15571557
adapter_env.update(env)
15581558

15591559
if log_file:
15601560
adapter_env["LLDBDAP_LOG"] = log_file
15611561
args = [executable]
15621562

15631563
# Add additional arguments first (like --no-lldbinit)
1564-
args.extend(additional_args)
1564+
if additional_args:
1565+
args.extend(additional_args)
15651566

15661567
if connection is not None:
15671568
args.append("--connection")

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ def create_debug_adapter(
3939
log_file=log_file_path,
4040
env=lldbDAPEnv,
4141
additional_args=additional_args or [],
42+
spawn_helper=self.spawnSubprocess,
4243
)
4344

4445
def build_and_create_debug_adapter(

lldb/test/API/tools/lldb-dap/disconnect/TestDAP_disconnect.py

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,17 +3,15 @@
33
"""
44

55

6-
import dap_server
76
from lldbsuite.test.decorators import *
87
from lldbsuite.test.lldbtest import *
98
from lldbsuite.test import lldbutil
109
import lldbdap_testcase
11-
import subprocess
1210
import time
1311
import os
1412

1513

16-
class TestDAP_launch(lldbdap_testcase.DAPTestCaseBase):
14+
class TestDAP_disconnect(lldbdap_testcase.DAPTestCaseBase):
1715
source = "main.cpp"
1816

1917
def disconnect_and_assert_no_output_printed(self):
@@ -67,10 +65,11 @@ def test_attach(self):
6765
lambda: self.run_platform_command("rm %s" % (sync_file_path))
6866
)
6967

70-
self.process = subprocess.Popen([program, sync_file_path])
68+
proc = self.spawnSubprocess(program, [sync_file_path])
7169
lldbutil.wait_for_file_on_target(self, sync_file_path)
7270

73-
self.attach(pid=self.process.pid, disconnectAutomatically=False)
71+
self.attach(pid=proc.pid, disconnectAutomatically=False, stopOnEntry=True)
72+
self.continue_to_next_stop()
7473
response = self.dap_server.request_evaluate("wait_for_attach = false;")
7574
self.assertTrue(response["success"])
7675

lldb/test/API/tools/lldb-dap/server/TestDAP_server.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ def cleanup():
3737

3838
def run_debug_session(self, connection, name, sleep_seconds_in_middle=None):
3939
self.dap_server = dap_server.DebugAdapterServer(
40-
connection=connection,
40+
connection=connection, spawn_helper=self.spawnSubprocess
4141
)
4242
program = self.getBuildArtifact("a.out")
4343
source = "main.c"
@@ -94,6 +94,7 @@ def test_server_interrupt(self):
9494
(process, connection) = self.start_server(connection="listen://localhost:0")
9595
self.dap_server = dap_server.DebugAdapterServer(
9696
connection=connection,
97+
spawn_helper=self.spawnSubprocess,
9798
)
9899
program = self.getBuildArtifact("a.out")
99100
source = "main.c"

0 commit comments

Comments
 (0)