Skip to content

Commit 8af0bd2

Browse files
committed
Re-write the replay method to only replay requests.
Additionally, I updated the replay logic to try to re-write some stateful values from requests/responses, otherwise we end up making requests with bad values like 'threadId' or 'frameId'.
1 parent 9d896a2 commit 8af0bd2

File tree

1 file changed

+177
-56
lines changed

1 file changed

+177
-56
lines changed

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

Lines changed: 177 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -1,32 +1,39 @@
11
#!/usr/bin/env python3
22

3+
import argparse
34
import binascii
5+
import copy
6+
import dataclasses
7+
import enum
48
import json
5-
import argparse
9+
import logging
610
import os
11+
import pathlib
712
import pprint
13+
import re
14+
import signal
815
import socket
916
import string
1017
import subprocess
11-
import signal
1218
import sys
13-
import pathlib
1419
import threading
15-
import warnings
1620
import time
21+
import warnings
1722
from typing import (
1823
Any,
19-
Optional,
20-
Dict,
24+
BinaryIO,
25+
Callable,
2126
cast,
27+
Dict,
2228
List,
23-
Callable,
24-
Union,
25-
BinaryIO,
26-
TypedDict,
2729
Literal,
30+
Optional,
31+
Tuple,
32+
TypedDict,
33+
Union,
2834
)
2935

36+
3037
# set timeout based on whether ASAN was enabled or not. Increase
3138
# timeout by a factor of 10 if ASAN is enabled.
3239
DEFAULT_TIMEOUT = 10 * (10 if ("ASAN_OPTIONS" in os.environ) else 1)
@@ -160,7 +167,62 @@ class NotSupportedError(KeyError):
160167
"""Raised if a feature is not supported due to its capabilities."""
161168

162169

170+
class ReplayMods(TypedDict, total=False):
171+
"""Fields that can be overwritten in requests during a replay."""
172+
173+
frameId: Optional[int]
174+
threadId: Optional[int]
175+
176+
177+
@dataclasses.dataclass
178+
class Log:
179+
class Dir(enum.Enum):
180+
SENT = 1
181+
RECV = 2
182+
183+
@property
184+
def requests(self) -> List[Tuple[Dir, Request]]:
185+
"""All requests in the log, in order."""
186+
return [m for m in self.messages if m[1]["type"] == "request"]
187+
188+
@property
189+
def events(self) -> List[Tuple[Dir, Event]]:
190+
"""All events in the log, in order."""
191+
return [m for m in self.messages if m[1]["type"] == "event"]
192+
193+
@property
194+
def responses(self) -> List[Tuple[Dir, Response]]:
195+
"""All responses in the log, in order."""
196+
return [m for m in self.messages if m[1]["type"] == "response"]
197+
198+
messages: List[Tuple[Dir, ProtocolMessage]] = dataclasses.field(
199+
default_factory=list
200+
)
201+
202+
@classmethod
203+
def load(cls, file: pathlib.Path) -> "Log":
204+
"""Load the file and parse any log messages. Returns (sent, recv)."""
205+
sent_pattern = re.compile(r"\d+\.\d+ \(.+\) --> ")
206+
recv_pattern = re.compile(r"\d+\.\d+ \(.+\) <-- ")
207+
208+
log = Log()
209+
with open(file, "r") as f:
210+
for line in f:
211+
if sent_pattern.match(line):
212+
packet = line.split("--> ", maxsplit=1)[1]
213+
log.messages.append((Log.Dir.SENT, json.loads(packet)))
214+
elif recv_pattern.match(line):
215+
packet = line.split("<-- ", maxsplit=1)[1]
216+
log.messages.append((Log.Dir.RECV, json.loads(packet)))
217+
return log
218+
219+
163220
class DebugCommunication(object):
221+
@property
222+
def is_stopped(self):
223+
"""Returns True if the debuggee is in a stopped state, otherwise False."""
224+
return len(self.thread_stop_reasons) > 0 or self.exit_status is not None
225+
164226
def __init__(
165227
self,
166228
recv: BinaryIO,
@@ -169,6 +231,7 @@ def __init__(
169231
log_file: Optional[str] = None,
170232
spawn_helper: Optional[SpawnHelperCallback] = None,
171233
):
234+
self._log = Log()
172235
self.log_file = log_file
173236
self.send = send
174237
self.recv = recv
@@ -203,11 +266,16 @@ def __init__(
203266

204267
# debuggee state
205268
self.threads: Optional[dict] = None
269+
self.stopped_thread: Optional[dict] = None
270+
self.thread_stacks: Optional[Dict[int, List[dict]]]
206271
self.thread_stop_reasons: Dict[str, Any] = {}
207272
self.frame_scopes: Dict[str, Any] = {}
208273
# keyed by breakpoint id
209274
self.resolved_breakpoints: dict[str, Breakpoint] = {}
210275

276+
# Modifiers used when replaying a log file.
277+
self._mod = ReplayMods()
278+
211279
# trigger enqueue thread
212280
self._recv_thread.start()
213281

@@ -251,16 +319,13 @@ def _read_packet(self) -> Optional[ProtocolMessage]:
251319
raise Exception("unexpected malformed message from lldb-dap: " + line)
252320

253321
def _read_packet_thread(self):
254-
try:
255-
while True:
256-
packet = self._read_packet()
257-
# `packet` will be `None` on EOF. We want to pass it down to
258-
# handle_recv_packet anyway so the main thread can handle unexpected
259-
# termination of lldb-dap and stop waiting for new packets.
260-
if not self._handle_recv_packet(packet):
261-
break
262-
finally:
263-
dump_dap_log(self.log_file)
322+
while True:
323+
packet = self._read_packet()
324+
# `packet` will be `None` on EOF. We want to pass it down to
325+
# handle_recv_packet anyway so the main thread can handle unexpected
326+
# termination of lldb-dap and stop waiting for new packets.
327+
if not self._handle_recv_packet(packet):
328+
break
264329

265330
def get_modules(
266331
self, start_module: Optional[int] = None, module_count: Optional[int] = None
@@ -381,6 +446,8 @@ def _process_recv_packets(self) -> None:
381446
warnings.warn(
382447
f"received a malformed packet, expected 'seq != 0' for {packet!r}"
383448
)
449+
if packet:
450+
self._log.messages.append((Log.Dir.RECV, packet))
384451
# Handle events that may modify any stateful properties of
385452
# the DAP session.
386453
if packet and packet["type"] == "event":
@@ -519,6 +586,8 @@ def send_packet(self, packet: ProtocolMessage) -> int:
519586
self.send.write(self.encode_content(json_str))
520587
self.send.flush()
521588

589+
self._log.messages.append((Log.Dir.SENT, packet))
590+
522591
return packet["seq"]
523592

524593
def _send_recv(self, request: Request) -> Optional[Response]:
@@ -724,32 +793,69 @@ def get_local_variable_child(
724793
return child
725794
return None
726795

727-
def replay_packets(self, file: pathlib.Path, verbosity: int) -> None:
728-
inflight: Dict[int, dict] = {} # requests, keyed by seq
729-
with open(file, "r") as f:
730-
for line in f:
731-
if "-->" in line:
732-
packet = line.split("--> ", maxsplit=1)[1]
733-
command_dict = json.loads(packet)
734-
if verbosity > 0:
735-
print("Sending:")
736-
pprint.PrettyPrinter(indent=2).pprint(command_dict)
737-
seq = self.send_packet(command_dict)
738-
if command_dict["type"] == "request":
739-
inflight[seq] = command_dict
740-
elif "<--" in line:
741-
packet = line.split("<-- ", maxsplit=1)[1]
742-
replay_response = json.loads(packet)
743-
print("Replay response:")
744-
pprint.PrettyPrinter(indent=2).pprint(replay_response)
745-
actual_response = self._recv_packet(
746-
predicate=lambda packet: replay_response == packet
747-
)
748-
print("Actual response:")
749-
pprint.PrettyPrinter(indent=2).pprint(actual_response)
750-
if actual_response and actual_response["type"] == "response":
751-
command_dict = inflight[actual_response["request_seq"]]
752-
self.validate_response(command_dict, actual_response)
796+
def _preconditions(self, req: Request) -> None:
797+
"""Validate any preconditions for the given command, potentially waiting
798+
for the debuggee to be in a specific state.
799+
"""
800+
if req["command"] == "threads":
801+
logging.debug("Waiting on precondition: stopped")
802+
self._recv_packet(predicate=lambda _: self.is_stopped)
803+
804+
# Apply any modifications to arguments.
805+
args = req["arguments"]
806+
if "threadId" in args and "threadId" in self._mod:
807+
args["threadId"] = self._mod["threadId"]
808+
if "frameId" in args and "frameId" in self._mod:
809+
args["frameId"] = self._mod["frameId"]
810+
811+
def _postconditions(self, resp: Response) -> None:
812+
"""Validate any postconditions for the given response, potentially
813+
waiting for the debuggee to be in a specific state.
814+
"""
815+
if resp["command"] == "launch":
816+
logging.debug("Waiting on postcondition: initialized")
817+
self._recv_packet(predicate=lambda _: self.initialized)
818+
elif resp["command"] == "configurationDone":
819+
logging.debug("Waiting on postcondition: process")
820+
self._recv_packet(predicate=lambda _: self.process_event_body is not None)
821+
822+
# Store some modifications related to replayed requests.
823+
if resp["command"] == "threads":
824+
self._mod["threadId"] = resp["body"]["threads"][0]["id"]
825+
if resp["command"] in ["continue", "next", "stepIn", "stepOut", "pause"]:
826+
self._mod.clear()
827+
self._recv_packet(predicate=lambda _: self.is_stopped)
828+
if resp["command"] == "stackTrace" and not self._mod.get("frameId", None):
829+
self._mod["frameId"] = next(
830+
(frame["id"] for frame in resp["body"]["stackFrames"]), None
831+
)
832+
833+
def replay(self, file: pathlib.Path) -> None:
834+
"""Replay a log file."""
835+
log = Log.load(file)
836+
responses = {
837+
r["request_seq"]: r for (dir, r) in log.responses if dir == Log.Dir.RECV
838+
}
839+
for dir, packet in log.messages:
840+
if dir != Log.Dir.SENT or packet["type"] != "request":
841+
continue
842+
req = packet
843+
want = responses[req["seq"]]
844+
845+
self._preconditions(req)
846+
847+
logging.info("Sending req %r", req)
848+
got = self._send_recv(req)
849+
logging.info("Received resp %r", got)
850+
851+
assert (
852+
got["command"] == want["command"] == req["command"]
853+
), f"got {got} want {want} for req {req}"
854+
assert (
855+
got["success"] == want["success"]
856+
), f"got {got} want {want} for req {req}"
857+
858+
self._postconditions(got)
753859

754860
def request_attach(
755861
self,
@@ -1447,6 +1553,8 @@ def terminate(self):
14471553
self.send.close()
14481554
if self._recv_thread.is_alive():
14491555
self._recv_thread.join()
1556+
if self.log_file:
1557+
dump_dap_log(self.log_file)
14501558

14511559
def request_setInstructionBreakpoints(self, memory_reference=[]):
14521560
breakpoints = []
@@ -1640,7 +1748,7 @@ def run_adapter(dbg: DebugCommunication, opts: argparse.Namespace) -> None:
16401748
source_to_lines: Dict[str, List[int]] = {}
16411749
for sbp in cast(List[str], opts.source_bp):
16421750
if ":" not in sbp:
1643-
print('error: invalid source with line "%s"' % (sbp))
1751+
print(f"error: invalid source with line {sbp!r}", file=sys.stderr)
16441752
continue
16451753
path, line = sbp.split(":")
16461754
if path in source_to_lines:
@@ -1684,8 +1792,7 @@ def run_adapter(dbg: DebugCommunication, opts: argparse.Namespace) -> None:
16841792
if response["success"]:
16851793
dbg.wait_for_stopped()
16861794
else:
1687-
if "message" in response:
1688-
print(response["message"])
1795+
print("failed to launch/attach: ", response)
16891796
dbg.request_disconnect(terminateDebuggee=True)
16901797

16911798

@@ -1901,11 +2008,23 @@ def main():
19012008

19022009
opts = parser.parse_args()
19032010

2011+
logging.basicConfig(
2012+
format="%(asctime)s - %(levelname)s - %(message)s",
2013+
level=(
2014+
logging.DEBUG
2015+
if opts.verbose > 1
2016+
else logging.INFO
2017+
if opts.verbose > 0
2018+
else logging.WARNING
2019+
),
2020+
)
2021+
19042022
if opts.adapter is None and opts.connection is None:
19052023
print(
19062024
"error: must either specify a path to a Debug Protocol Adapter "
19072025
"executable using the --adapter option, or using the --connection "
1908-
"option"
2026+
"option",
2027+
file=sys.stderr,
19092028
)
19102029
return
19112030
dbg = DebugAdapterServer(
@@ -1914,12 +2033,14 @@ def main():
19142033
additional_args=opts.adapter_arg,
19152034
)
19162035
if opts.debug:
1917-
input('Waiting for debugger to attach pid "%i"' % (dbg.get_pid()))
1918-
if opts.replay:
1919-
dbg.replay_packets(opts.replay)
1920-
else:
1921-
run_adapter(dbg, opts)
1922-
dbg.terminate()
2036+
input(f"Waiting for debugger to attach pid '{dbg.get_pid()}'")
2037+
try:
2038+
if opts.replay:
2039+
dbg.replay(opts.replay)
2040+
else:
2041+
run_adapter(dbg, opts)
2042+
finally:
2043+
dbg.terminate()
19232044

19242045

19252046
if __name__ == "__main__":

0 commit comments

Comments
 (0)