[Lldb-commits] [lldb] [lldb-dap] Fix running dap_server.py directly for debugging tests. (PR #167754)
John Harrison via lldb-commits
lldb-commits at lists.llvm.org
Thu Nov 13 09:20:59 PST 2025
https://github.com/ashgti updated https://github.com/llvm/llvm-project/pull/167754
>From c148dad94f05ff4ea81fe088df2d06c72fd8daf6 Mon Sep 17 00:00:00 2001
From: John Harrison <harjohn at google.com>
Date: Wed, 12 Nov 2025 12:10:36 -0800
Subject: [PATCH 1/2] [lldb-dap] Fix running dap_server.py directly for
debugging tests.
This adjusts the behavior of running dap_server.py directly to better support the current state of development.
* Instead of the custom tracefile parsing logic, I adjusted the replay helper to handle parsing lldb-dap log files created with the `LLDBDAP_LOG` env variable.
* Migrated argument parsing to `argparse`, that is in all verisons of py3+ and has a few improvements over `optparse`.
* Corrected the existing arguments and updated `run_vscode` > `run_adapter`. You can use this for simple debugging like: `xcrun python3 lldb/packages/Python/lldbsuite/test/tools/lldb-dap/dap_server.py --adapter=lldb-dap --adapter-arg='--pre-init-command' --adapter-arg 'help' --program a.out --init-command 'help'`
---
.../test/tools/lldb-dap/dap_server.py | 414 ++++++++----------
1 file changed, 183 insertions(+), 231 deletions(-)
diff --git a/lldb/packages/Python/lldbsuite/test/tools/lldb-dap/dap_server.py b/lldb/packages/Python/lldbsuite/test/tools/lldb-dap/dap_server.py
index ac550962cfb85..f0aef30b3cd1d 100644
--- a/lldb/packages/Python/lldbsuite/test/tools/lldb-dap/dap_server.py
+++ b/lldb/packages/Python/lldbsuite/test/tools/lldb-dap/dap_server.py
@@ -1,8 +1,8 @@
-#!/usr/bin/env python
+#!/usr/bin/env python3
import binascii
import json
-import optparse
+import argparse
import os
import pprint
import socket
@@ -10,6 +10,7 @@
import subprocess
import signal
import sys
+import pathlib
import threading
import warnings
import time
@@ -20,10 +21,8 @@
cast,
List,
Callable,
- IO,
Union,
BinaryIO,
- TextIO,
TypedDict,
Literal,
)
@@ -143,35 +142,6 @@ def dump_memory(base_addr, data, num_per_line, outfile):
outfile.write("\n")
-def read_packet(
- f: IO[bytes], trace_file: Optional[IO[str]] = None
-) -> Optional[ProtocolMessage]:
- """Decode a JSON packet that starts with the content length and is
- followed by the JSON bytes from a file 'f'. Returns None on EOF.
- """
- line = f.readline().decode("utf-8")
- if len(line) == 0:
- return None # EOF.
-
- # Watch for line that starts with the prefix
- prefix = "Content-Length: "
- if line.startswith(prefix):
- # Decode length of JSON bytes
- length = int(line[len(prefix) :])
- # Skip empty line
- separator = f.readline().decode()
- if separator != "":
- Exception("malformed DAP content header, unexpected line: " + separator)
- # Read JSON bytes
- json_str = f.read(length).decode()
- if trace_file:
- trace_file.write("from adapter:\n%s\n" % (json_str))
- # Decode the JSON bytes into a python dictionary
- return json.loads(json_str)
-
- raise Exception("unexpected malformed message from lldb-dap: " + line)
-
-
def packet_type_is(packet, packet_type):
return "type" in packet and packet["type"] == packet_type
@@ -199,8 +169,6 @@ def __init__(
log_file: Optional[str] = None,
spawn_helper: Optional[SpawnHelperCallback] = None,
):
- # For debugging test failures, try setting `trace_file = sys.stderr`.
- self.trace_file: Optional[TextIO] = None
self.log_file = log_file
self.send = send
self.recv = recv
@@ -258,10 +226,34 @@ def validate_response(cls, command, response):
f"seq mismatch in response {command['seq']} != {response['request_seq']}"
)
+ def _read_packet(self) -> Optional[ProtocolMessage]:
+ """Decode a JSON packet that starts with the content length and is
+ followed by the JSON bytes. Returns None on EOF.
+ """
+ line = self.recv.readline().decode("utf-8")
+ if len(line) == 0:
+ return None # EOF.
+
+ # Watch for line that starts with the prefix
+ prefix = "Content-Length: "
+ if line.startswith(prefix):
+ # Decode length of JSON bytes
+ length = int(line[len(prefix) :])
+ # Skip empty line
+ separator = self.recv.readline().decode()
+ if separator != "":
+ Exception("malformed DAP content header, unexpected line: " + separator)
+ # Read JSON bytes
+ json_str = self.recv.read(length).decode()
+ # Decode the JSON bytes into a python dictionary
+ return json.loads(json_str)
+
+ raise Exception("unexpected malformed message from lldb-dap: " + line)
+
def _read_packet_thread(self):
try:
while True:
- packet = read_packet(self.recv, trace_file=self.trace_file)
+ packet = self._read_packet()
# `packet` will be `None` on EOF. We want to pass it down to
# handle_recv_packet anyway so the main thread can handle unexpected
# termination of lldb-dap and stop waiting for new packets.
@@ -521,9 +513,6 @@ def send_packet(self, packet: ProtocolMessage) -> int:
# Encode our command dictionary as a JSON string
json_str = json.dumps(packet, separators=(",", ":"))
- if self.trace_file:
- self.trace_file.write("to adapter:\n%s\n" % (json_str))
-
length = len(json_str)
if length > 0:
# Send the encoded JSON packet and flush the 'send' file
@@ -735,45 +724,30 @@ def get_local_variable_child(
return child
return None
- def replay_packets(self, replay_file_path):
- f = open(replay_file_path, "r")
- mode = "invalid"
- set_sequence = False
- command_dict = None
- while mode != "eof":
- if mode == "invalid":
- line = f.readline()
- if line.startswith("to adapter:"):
- mode = "send"
- elif line.startswith("from adapter:"):
- mode = "recv"
- elif mode == "send":
- command_dict = read_packet(f)
- # Skip the end of line that follows the JSON
- f.readline()
- if command_dict is None:
- raise ValueError("decode packet failed from replay file")
- print("Sending:")
- pprint.PrettyPrinter(indent=2).pprint(command_dict)
- # raw_input('Press ENTER to send:')
- self.send_packet(command_dict, set_sequence)
- mode = "invalid"
- elif mode == "recv":
- print("Replay response:")
- replay_response = read_packet(f)
- # Skip the end of line that follows the JSON
- f.readline()
- pprint.PrettyPrinter(indent=2).pprint(replay_response)
- actual_response = self.recv_packet()
- if actual_response:
- type = actual_response["type"]
+ def replay_packets(self, file: pathlib.Path, verbosity: int) -> None:
+ inflight: Dict[int, dict] = {} # requests, keyed by seq
+ with open(file, "r") as f:
+ for line in f:
+ if "-->" in line:
+ command_dict = json.loads(line.split("--> ")[1])
+ if verbosity > 0:
+ print("Sending:")
+ pprint.PrettyPrinter(indent=2).pprint(command_dict)
+ seq = self.send_packet(command_dict)
+ if command_dict["type"] == "request":
+ inflight[seq] = command_dict
+ elif "<--" in line:
+ replay_response = json.loads(line.split("<-- ")[1])
+ print("Replay response:")
+ pprint.PrettyPrinter(indent=2).pprint(replay_response)
+ actual_response = self._recv_packet(
+ predicate=lambda packet: replay_response == packet
+ )
print("Actual response:")
- if type == "response":
- self.validate_response(command_dict, actual_response)
pprint.PrettyPrinter(indent=2).pprint(actual_response)
- else:
- print("error: didn't get a valid response")
- mode = "invalid"
+ if actual_response and actual_response["type"] == "response":
+ command_dict = inflight[actual_response["request_seq"]]
+ self.validate_response(command_dict, actual_response)
def request_attach(
self,
@@ -1646,65 +1620,63 @@ def __str__(self):
return f"lldb-dap returned non-zero exit status {self.returncode}."
-def attach_options_specified(options):
- if options.pid is not None:
+def attach_options_specified(opts):
+ if opts.pid is not None:
return True
- if options.waitFor:
+ if opts.wait_for:
return True
- if options.attach:
+ if opts.attach:
return True
- if options.attachCmds:
+ if opts.attach_command:
return True
return False
-def run_vscode(dbg, args, options):
- dbg.request_initialize(options.sourceInitFile)
+def run_adapter(dbg: DebugCommunication, opts: argparse.Namespace) -> None:
+ dbg.request_initialize(opts.source_init_file)
- if options.sourceBreakpoints:
- source_to_lines = {}
- for file_line in options.sourceBreakpoints:
- (path, line) = file_line.split(":")
- if len(path) == 0 or len(line) == 0:
- print('error: invalid source with line "%s"' % (file_line))
-
- else:
- if path in source_to_lines:
- source_to_lines[path].append(int(line))
- else:
- source_to_lines[path] = [int(line)]
- for source in source_to_lines:
- dbg.request_setBreakpoints(Source(source), source_to_lines[source])
- if options.funcBreakpoints:
- dbg.request_setFunctionBreakpoints(options.funcBreakpoints)
+ source_to_lines: Dict[str, List[int]] = {}
+ for sbp in cast(List[str], opts.source_bp):
+ if ":" not in sbp:
+ print('error: invalid source with line "%s"' % (sbp))
+ continue
+ path, line = sbp.split(":")
+ if path in source_to_lines:
+ source_to_lines[path].append(int(line))
+ else:
+ source_to_lines[path] = [int(line)]
+ for source in source_to_lines:
+ dbg.request_setBreakpoints(Source.build(path=source), source_to_lines[source])
+ if opts.function_bp:
+ dbg.request_setFunctionBreakpoints(opts.function_bp)
dbg.request_configurationDone()
- if attach_options_specified(options):
+ if attach_options_specified(opts):
response = dbg.request_attach(
- program=options.program,
- pid=options.pid,
- waitFor=options.waitFor,
- attachCommands=options.attachCmds,
- initCommands=options.initCmds,
- preRunCommands=options.preRunCmds,
- stopCommands=options.stopCmds,
- exitCommands=options.exitCmds,
- terminateCommands=options.terminateCmds,
+ program=opts.program,
+ pid=opts.pid,
+ waitFor=opts.wait_for,
+ attachCommands=opts.attach_command,
+ initCommands=opts.init_command,
+ preRunCommands=opts.pre_run_command,
+ stopCommands=opts.stop_command,
+ terminateCommands=opts.terminate_command,
+ exitCommands=opts.exit_command,
)
else:
response = dbg.request_launch(
- options.program,
- args=args,
- env=options.envs,
- cwd=options.workingDir,
- debuggerRoot=options.debuggerRoot,
- sourcePath=options.sourcePath,
- initCommands=options.initCmds,
- preRunCommands=options.preRunCmds,
- stopCommands=options.stopCmds,
- exitCommands=options.exitCmds,
- terminateCommands=options.terminateCmds,
+ opts.program,
+ args=opts.args,
+ env=opts.env,
+ cwd=opts.working_dir,
+ debuggerRoot=opts.debugger_root,
+ sourceMap=opts.source_map,
+ initCommands=opts.init_command,
+ preRunCommands=opts.pre_run_command,
+ stopCommands=opts.stop_command,
+ exitCommands=opts.exit_command,
+ terminateCommands=opts.terminate_command,
)
if response["success"]:
@@ -1716,110 +1688,98 @@ def run_vscode(dbg, args, options):
def main():
- parser = optparse.OptionParser(
+ parser = argparse.ArgumentParser(
+ prog="dap_server.py",
description=(
"A testing framework for the Visual Studio Code Debug Adapter protocol"
- )
+ ),
)
- parser.add_option(
- "--vscode",
- type="string",
- dest="vscode_path",
+ parser.add_argument(
+ "--adapter",
help=(
- "The path to the command line program that implements the "
- "Visual Studio Code Debug Adapter protocol."
+ "The path to the command line program that implements the Debug Adapter protocol."
),
- default=None,
)
- parser.add_option(
+ parser.add_argument(
+ "--adapter-arg",
+ action="append",
+ default=[],
+ help="Additional args to pass to the debug adapter.",
+ )
+
+ parser.add_argument(
"--program",
- type="string",
- dest="program",
help="The path to the program to debug.",
- default=None,
)
- parser.add_option(
- "--workingDir",
- type="string",
- dest="workingDir",
- default=None,
+ parser.add_argument(
+ "--working-dir",
help="Set the working directory for the process we launch.",
)
- parser.add_option(
- "--sourcePath",
- type="string",
- dest="sourcePath",
- default=None,
+ parser.add_argument(
+ "--source-map",
+ nargs=2,
+ action="extend",
+ metavar=("PREFIX", "REPLACEMENT"),
help=(
- "Set the relative source root for any debug info that has "
- "relative paths in it."
+ "Source path remappings apply substitutions to the paths of source "
+ "files, typically needed to debug from a different host than the "
+ "one that built the target."
),
)
- parser.add_option(
- "--debuggerRoot",
- type="string",
- dest="debuggerRoot",
- default=None,
+ parser.add_argument(
+ "--debugger-root",
help=(
"Set the working directory for lldb-dap for any object files "
"with relative paths in the Mach-o debug map."
),
)
- parser.add_option(
+ parser.add_argument(
"-r",
"--replay",
- type="string",
- dest="replay",
help=(
"Specify a file containing a packet log to replay with the "
- "current Visual Studio Code Debug Adapter executable."
+ "current debug adapter."
),
- default=None,
)
- parser.add_option(
+ parser.add_argument(
"-g",
"--debug",
action="store_true",
- dest="debug",
- default=False,
- help="Pause waiting for a debugger to attach to the debug adapter",
+ help="Pause waiting for a debugger to attach to the debug adapter.",
)
- parser.add_option(
- "--sourceInitFile",
+ parser.add_argument(
+ "--source-init-file",
action="store_true",
- dest="sourceInitFile",
- default=False,
- help="Whether lldb-dap should source .lldbinit file or not",
+ help="Whether lldb-dap should source .lldbinit file or not.",
)
- parser.add_option(
+ parser.add_argument(
"--connection",
dest="connection",
- help="Attach a socket connection of using STDIN for VSCode",
- default=None,
+ help=(
+ "Communicate with the debug adapter over specified connection "
+ "instead of launching the debug adapter directly."
+ ),
)
- parser.add_option(
+ parser.add_argument(
"--pid",
- type="int",
+ type=int,
dest="pid",
- help="The process ID to attach to",
- default=None,
+ help="The process ID to attach to.",
)
- parser.add_option(
+ parser.add_argument(
"--attach",
action="store_true",
- dest="attach",
- default=False,
help=(
"Specify this option to attach to a process by name. The "
"process name is the basename of the executable specified with "
@@ -1827,38 +1787,30 @@ def main():
),
)
- parser.add_option(
+ parser.add_argument(
"-f",
"--function-bp",
- type="string",
action="append",
- dest="funcBreakpoints",
+ default=[],
+ metavar="FUNCTION",
help=(
- "Specify the name of a function to break at. "
- "Can be specified more than once."
+ "Specify the name of a function to break at. Can be specified more "
+ "than once."
),
- default=[],
)
- parser.add_option(
+ parser.add_argument(
"-s",
"--source-bp",
- type="string",
action="append",
- dest="sourceBreakpoints",
default=[],
- help=(
- "Specify source breakpoints to set in the format of "
- "<source>:<line>. "
- "Can be specified more than once."
- ),
+ metavar="SOURCE:LINE",
+ help="Specify source breakpoints to set. Can be specified more than once.",
)
- parser.add_option(
- "--attachCommand",
- type="string",
+ parser.add_argument(
+ "--attach-command",
action="append",
- dest="attachCmds",
default=[],
help=(
"Specify a LLDB command that will attach to a process. "
@@ -1866,11 +1818,9 @@ def main():
),
)
- parser.add_option(
- "--initCommand",
- type="string",
+ parser.add_argument(
+ "--init-command",
action="append",
- dest="initCmds",
default=[],
help=(
"Specify a LLDB command that will be executed before the target "
@@ -1878,11 +1828,9 @@ def main():
),
)
- parser.add_option(
- "--preRunCommand",
- type="string",
+ parser.add_argument(
+ "--pre-run-command",
action="append",
- dest="preRunCmds",
default=[],
help=(
"Specify a LLDB command that will be executed after the target "
@@ -1890,11 +1838,9 @@ def main():
),
)
- parser.add_option(
- "--stopCommand",
- type="string",
+ parser.add_argument(
+ "--stop-command",
action="append",
- dest="stopCmds",
default=[],
help=(
"Specify a LLDB command that will be executed each time the"
@@ -1902,11 +1848,9 @@ def main():
),
)
- parser.add_option(
- "--exitCommand",
- type="string",
+ parser.add_argument(
+ "--exit-command",
action="append",
- dest="exitCmds",
default=[],
help=(
"Specify a LLDB command that will be executed when the process "
@@ -1914,11 +1858,9 @@ def main():
),
)
- parser.add_option(
- "--terminateCommand",
- type="string",
+ parser.add_argument(
+ "--terminate-command",
action="append",
- dest="terminateCmds",
default=[],
help=(
"Specify a LLDB command that will be executed when the debugging "
@@ -1926,20 +1868,18 @@ def main():
),
)
- parser.add_option(
+ parser.add_argument(
"--env",
- type="string",
action="append",
- dest="envs",
default=[],
- help=("Specify environment variables to pass to the launched " "process."),
+ metavar="NAME=VALUE",
+ help="Specify environment variables to pass to the launched process. Can be specified more than once.",
)
- parser.add_option(
- "--waitFor",
+ parser.add_argument(
+ "-w",
+ "--wait-for",
action="store_true",
- dest="waitFor",
- default=False,
help=(
"Wait for the next process to be launched whose name matches "
"the basename of the program specified with the --program "
@@ -1947,24 +1887,36 @@ def main():
),
)
- (options, args) = parser.parse_args(sys.argv[1:])
+ parser.add_argument(
+ "-v", "--verbose", help="Verbosity level.", action="count", default=0
+ )
- if options.vscode_path is None and options.connection is None:
+ parser.add_argument(
+ "args",
+ nargs="*",
+ help="A list containing all the arguments to be passed to the executable when it is run.",
+ )
+
+ opts = parser.parse_args()
+
+ if opts.adapter is None and opts.connection is None:
print(
- "error: must either specify a path to a Visual Studio Code "
- "Debug Adapter vscode executable path using the --vscode "
- "option, or using the --connection option"
+ "error: must either specify a path to a Debug Protocol Adapter "
+ "executable using the --adapter option, or using the --connection "
+ "option"
)
return
dbg = DebugAdapterServer(
- executable=options.vscode_path, connection=options.connection
+ executable=opts.adapter,
+ connection=opts.connection,
+ additional_args=opts.adapter_arg,
)
- if options.debug:
- raw_input('Waiting for debugger to attach pid "%i"' % (dbg.get_pid()))
- if options.replay:
- dbg.replay_packets(options.replay)
+ if opts.debug:
+ input('Waiting for debugger to attach pid "%i"' % (dbg.get_pid()))
+ if opts.replay:
+ dbg.replay_packets(opts.replay)
else:
- run_vscode(dbg, args, options)
+ run_adapter(dbg, opts)
dbg.terminate()
>From 9d896a2d855d091cc12218877beccf57e7405d76 Mon Sep 17 00:00:00 2001
From: John Harrison <harjohn at google.com>
Date: Thu, 13 Nov 2025 09:20:15 -0800
Subject: [PATCH 2/2] Make sure we only split 1 time on '-->' or '<--' when
replaying a log file.
---
.../Python/lldbsuite/test/tools/lldb-dap/dap_server.py | 6 ++++--
1 file changed, 4 insertions(+), 2 deletions(-)
diff --git a/lldb/packages/Python/lldbsuite/test/tools/lldb-dap/dap_server.py b/lldb/packages/Python/lldbsuite/test/tools/lldb-dap/dap_server.py
index f0aef30b3cd1d..583b3204df457 100644
--- a/lldb/packages/Python/lldbsuite/test/tools/lldb-dap/dap_server.py
+++ b/lldb/packages/Python/lldbsuite/test/tools/lldb-dap/dap_server.py
@@ -729,7 +729,8 @@ def replay_packets(self, file: pathlib.Path, verbosity: int) -> None:
with open(file, "r") as f:
for line in f:
if "-->" in line:
- command_dict = json.loads(line.split("--> ")[1])
+ packet = line.split("--> ", maxsplit=1)[1]
+ command_dict = json.loads(packet)
if verbosity > 0:
print("Sending:")
pprint.PrettyPrinter(indent=2).pprint(command_dict)
@@ -737,7 +738,8 @@ def replay_packets(self, file: pathlib.Path, verbosity: int) -> None:
if command_dict["type"] == "request":
inflight[seq] = command_dict
elif "<--" in line:
- replay_response = json.loads(line.split("<-- ")[1])
+ packet = line.split("<-- ", maxsplit=1)[1]
+ replay_response = json.loads(packet)
print("Replay response:")
pprint.PrettyPrinter(indent=2).pprint(replay_response)
actual_response = self._recv_packet(
More information about the lldb-commits
mailing list