11#!/usr/bin/env python3
22
3+ import argparse
34import binascii
5+ import copy
6+ import dataclasses
7+ import enum
48import json
5- import argparse
9+ import logging
610import os
11+ import pathlib
712import pprint
13+ import re
14+ import signal
815import socket
916import string
1017import subprocess
11- import signal
1218import sys
13- import pathlib
1419import threading
15- import warnings
1620import time
21+ import warnings
1722from 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.
3239DEFAULT_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+
163220class 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
19252046if __name__ == "__main__" :
0 commit comments