1#!/usr/bin/env python 2 3import binascii 4import json 5import optparse 6import os 7import pprint 8import socket 9import string 10import subprocess 11import sys 12import threading 13import time 14 15 16def dump_memory(base_addr, data, num_per_line, outfile): 17 data_len = len(data) 18 hex_string = binascii.hexlify(data) 19 addr = base_addr 20 ascii_str = "" 21 i = 0 22 while i < data_len: 23 outfile.write("0x%8.8x: " % (addr + i)) 24 bytes_left = data_len - i 25 if bytes_left >= num_per_line: 26 curr_data_len = num_per_line 27 else: 28 curr_data_len = bytes_left 29 hex_start_idx = i * 2 30 hex_end_idx = hex_start_idx + curr_data_len * 2 31 curr_hex_str = hex_string[hex_start_idx:hex_end_idx] 32 # 'curr_hex_str' now contains the hex byte string for the 33 # current line with no spaces between bytes 34 t = iter(curr_hex_str) 35 # Print hex bytes separated by space 36 outfile.write(" ".join(a + b for a, b in zip(t, t))) 37 # Print two spaces 38 outfile.write(" ") 39 # Calculate ASCII string for bytes into 'ascii_str' 40 ascii_str = "" 41 for j in range(i, i + curr_data_len): 42 ch = data[j] 43 if ch in string.printable and ch not in string.whitespace: 44 ascii_str += "%c" % (ch) 45 else: 46 ascii_str += "." 47 # Print ASCII representation and newline 48 outfile.write(ascii_str) 49 i = i + curr_data_len 50 outfile.write("\n") 51 52 53def read_packet(f, verbose=False, trace_file=None): 54 """Decode a JSON packet that starts with the content length and is 55 followed by the JSON bytes from a file 'f'. Returns None on EOF. 56 """ 57 line = f.readline().decode("utf-8") 58 if len(line) == 0: 59 return None # EOF. 60 61 # Watch for line that starts with the prefix 62 prefix = "Content-Length: " 63 if line.startswith(prefix): 64 # Decode length of JSON bytes 65 if verbose: 66 print('content: "%s"' % (line)) 67 length = int(line[len(prefix) :]) 68 if verbose: 69 print('length: "%u"' % (length)) 70 # Skip empty line 71 line = f.readline() 72 if verbose: 73 print('empty: "%s"' % (line)) 74 # Read JSON bytes 75 json_str = f.read(length) 76 if verbose: 77 print('json: "%s"' % (json_str)) 78 if trace_file: 79 trace_file.write("from adaptor:\n%s\n" % (json_str)) 80 # Decode the JSON bytes into a python dictionary 81 return json.loads(json_str) 82 83 raise Exception("unexpected malformed message from lldb-dap: " + line) 84 85 86def packet_type_is(packet, packet_type): 87 return "type" in packet and packet["type"] == packet_type 88 89 90def dump_dap_log(log_file): 91 print("========= DEBUG ADAPTER PROTOCOL LOGS =========") 92 if log_file is None: 93 print("no log file available") 94 else: 95 with open(log_file, "r") as file: 96 print(file.read()) 97 print("========= END =========") 98 99 100def read_packet_thread(vs_comm, log_file): 101 done = False 102 try: 103 while not done: 104 packet = read_packet(vs_comm.recv, trace_file=vs_comm.trace_file) 105 # `packet` will be `None` on EOF. We want to pass it down to 106 # handle_recv_packet anyway so the main thread can handle unexpected 107 # termination of lldb-dap and stop waiting for new packets. 108 done = not vs_comm.handle_recv_packet(packet) 109 finally: 110 dump_dap_log(log_file) 111 112 113class DebugCommunication(object): 114 def __init__(self, recv, send, init_commands, log_file=None): 115 self.trace_file = None 116 self.send = send 117 self.recv = recv 118 self.recv_packets = [] 119 self.recv_condition = threading.Condition() 120 self.recv_thread = threading.Thread( 121 target=read_packet_thread, args=(self, log_file) 122 ) 123 self.process_event_body = None 124 self.exit_status = None 125 self.initialize_body = None 126 self.thread_stop_reasons = {} 127 self.breakpoint_events = [] 128 self.progress_events = [] 129 self.reverse_requests = [] 130 self.sequence = 1 131 self.threads = None 132 self.recv_thread.start() 133 self.output_condition = threading.Condition() 134 self.output = {} 135 self.configuration_done_sent = False 136 self.frame_scopes = {} 137 self.init_commands = init_commands 138 self.disassembled_instructions = {} 139 140 @classmethod 141 def encode_content(cls, s): 142 return ("Content-Length: %u\r\n\r\n%s" % (len(s), s)).encode("utf-8") 143 144 @classmethod 145 def validate_response(cls, command, response): 146 if command["command"] != response["command"]: 147 raise ValueError("command mismatch in response") 148 if command["seq"] != response["request_seq"]: 149 raise ValueError("seq mismatch in response") 150 151 def get_modules(self): 152 module_list = self.request_modules()["body"]["modules"] 153 modules = {} 154 for module in module_list: 155 modules[module["name"]] = module 156 return modules 157 158 def get_output(self, category, timeout=0.0, clear=True): 159 self.output_condition.acquire() 160 output = None 161 if category in self.output: 162 output = self.output[category] 163 if clear: 164 del self.output[category] 165 elif timeout != 0.0: 166 self.output_condition.wait(timeout) 167 if category in self.output: 168 output = self.output[category] 169 if clear: 170 del self.output[category] 171 self.output_condition.release() 172 return output 173 174 def collect_output(self, category, duration, clear=True): 175 end_time = time.time() + duration 176 collected_output = "" 177 while end_time > time.time(): 178 output = self.get_output(category, timeout=0.25, clear=clear) 179 if output: 180 collected_output += output 181 return collected_output if collected_output else None 182 183 def enqueue_recv_packet(self, packet): 184 self.recv_condition.acquire() 185 self.recv_packets.append(packet) 186 self.recv_condition.notify() 187 self.recv_condition.release() 188 189 def handle_recv_packet(self, packet): 190 """Called by the read thread that is waiting for all incoming packets 191 to store the incoming packet in "self.recv_packets" in a thread safe 192 way. This function will then signal the "self.recv_condition" to 193 indicate a new packet is available. Returns True if the caller 194 should keep calling this function for more packets. 195 """ 196 # If EOF, notify the read thread by enqueuing a None. 197 if not packet: 198 self.enqueue_recv_packet(None) 199 return False 200 201 # Check the packet to see if is an event packet 202 keepGoing = True 203 packet_type = packet["type"] 204 if packet_type == "event": 205 event = packet["event"] 206 body = None 207 if "body" in packet: 208 body = packet["body"] 209 # Handle the event packet and cache information from these packets 210 # as they come in 211 if event == "output": 212 # Store any output we receive so clients can retrieve it later. 213 category = body["category"] 214 output = body["output"] 215 self.output_condition.acquire() 216 if category in self.output: 217 self.output[category] += output 218 else: 219 self.output[category] = output 220 self.output_condition.notify() 221 self.output_condition.release() 222 # no need to add 'output' event packets to our packets list 223 return keepGoing 224 elif event == "process": 225 # When a new process is attached or launched, remember the 226 # details that are available in the body of the event 227 self.process_event_body = body 228 elif event == "stopped": 229 # Each thread that stops with a reason will send a 230 # 'stopped' event. We need to remember the thread stop 231 # reasons since the 'threads' command doesn't return 232 # that information. 233 self._process_stopped() 234 tid = body["threadId"] 235 self.thread_stop_reasons[tid] = body 236 elif event == "breakpoint": 237 # Breakpoint events come in when a breakpoint has locations 238 # added or removed. Keep track of them so we can look for them 239 # in tests. 240 self.breakpoint_events.append(packet) 241 # no need to add 'breakpoint' event packets to our packets list 242 return keepGoing 243 elif event.startswith("progress"): 244 # Progress events come in as 'progressStart', 'progressUpdate', 245 # and 'progressEnd' events. Keep these around in case test 246 # cases want to verify them. 247 self.progress_events.append(packet) 248 # No need to add 'progress' event packets to our packets list. 249 return keepGoing 250 251 elif packet_type == "response": 252 if packet["command"] == "disconnect": 253 keepGoing = False 254 self.enqueue_recv_packet(packet) 255 return keepGoing 256 257 def send_packet(self, command_dict, set_sequence=True): 258 """Take the "command_dict" python dictionary and encode it as a JSON 259 string and send the contents as a packet to the VSCode debug 260 adaptor""" 261 # Set the sequence ID for this command automatically 262 if set_sequence: 263 command_dict["seq"] = self.sequence 264 self.sequence += 1 265 # Encode our command dictionary as a JSON string 266 json_str = json.dumps(command_dict, separators=(",", ":")) 267 if self.trace_file: 268 self.trace_file.write("to adaptor:\n%s\n" % (json_str)) 269 length = len(json_str) 270 if length > 0: 271 # Send the encoded JSON packet and flush the 'send' file 272 self.send.write(self.encode_content(json_str)) 273 self.send.flush() 274 275 def recv_packet(self, filter_type=None, filter_event=None, timeout=None): 276 """Get a JSON packet from the VSCode debug adaptor. This function 277 assumes a thread that reads packets is running and will deliver 278 any received packets by calling handle_recv_packet(...). This 279 function will wait for the packet to arrive and return it when 280 it does.""" 281 while True: 282 try: 283 self.recv_condition.acquire() 284 packet = None 285 while True: 286 for i, curr_packet in enumerate(self.recv_packets): 287 if not curr_packet: 288 raise EOFError 289 packet_type = curr_packet["type"] 290 if filter_type is None or packet_type in filter_type: 291 if filter_event is None or ( 292 packet_type == "event" 293 and curr_packet["event"] in filter_event 294 ): 295 packet = self.recv_packets.pop(i) 296 break 297 if packet: 298 break 299 # Sleep until packet is received 300 len_before = len(self.recv_packets) 301 self.recv_condition.wait(timeout) 302 len_after = len(self.recv_packets) 303 if len_before == len_after: 304 return None # Timed out 305 return packet 306 except EOFError: 307 return None 308 finally: 309 self.recv_condition.release() 310 311 return None 312 313 def send_recv(self, command): 314 """Send a command python dictionary as JSON and receive the JSON 315 response. Validates that the response is the correct sequence and 316 command in the reply. Any events that are received are added to the 317 events list in this object""" 318 self.send_packet(command) 319 done = False 320 while not done: 321 response_or_request = self.recv_packet(filter_type=["response", "request"]) 322 if response_or_request is None: 323 desc = 'no response for "%s"' % (command["command"]) 324 raise ValueError(desc) 325 if response_or_request["type"] == "response": 326 self.validate_response(command, response_or_request) 327 return response_or_request 328 else: 329 self.reverse_requests.append(response_or_request) 330 if response_or_request["command"] == "runInTerminal": 331 subprocess.Popen( 332 response_or_request["arguments"]["args"], 333 env=response_or_request["arguments"]["env"], 334 ) 335 self.send_packet( 336 { 337 "type": "response", 338 "seq": -1, 339 "request_seq": response_or_request["seq"], 340 "success": True, 341 "command": "runInTerminal", 342 "body": {}, 343 }, 344 set_sequence=False, 345 ) 346 elif response_or_request["command"] == "startDebugging": 347 self.send_packet( 348 { 349 "type": "response", 350 "seq": -1, 351 "request_seq": response_or_request["seq"], 352 "success": True, 353 "command": "startDebugging", 354 "body": {}, 355 }, 356 set_sequence=False, 357 ) 358 else: 359 desc = 'unknown reverse request "%s"' % ( 360 response_or_request["command"] 361 ) 362 raise ValueError(desc) 363 364 return None 365 366 def wait_for_event(self, filter=None, timeout=None): 367 while True: 368 return self.recv_packet( 369 filter_type="event", filter_event=filter, timeout=timeout 370 ) 371 return None 372 373 def wait_for_stopped(self, timeout=None): 374 stopped_events = [] 375 stopped_event = self.wait_for_event( 376 filter=["stopped", "exited"], timeout=timeout 377 ) 378 exited = False 379 while stopped_event: 380 stopped_events.append(stopped_event) 381 # If we exited, then we are done 382 if stopped_event["event"] == "exited": 383 self.exit_status = stopped_event["body"]["exitCode"] 384 exited = True 385 break 386 # Otherwise we stopped and there might be one or more 'stopped' 387 # events for each thread that stopped with a reason, so keep 388 # checking for more 'stopped' events and return all of them 389 stopped_event = self.wait_for_event(filter="stopped", timeout=0.25) 390 if exited: 391 self.threads = [] 392 return stopped_events 393 394 def wait_for_exited(self): 395 event_dict = self.wait_for_event("exited") 396 if event_dict is None: 397 raise ValueError("didn't get exited event") 398 return event_dict 399 400 def wait_for_terminated(self): 401 event_dict = self.wait_for_event("terminated") 402 if event_dict is None: 403 raise ValueError("didn't get terminated event") 404 return event_dict 405 406 def get_initialize_value(self, key): 407 """Get a value for the given key if it there is a key/value pair in 408 the "initialize" request response body. 409 """ 410 if self.initialize_body and key in self.initialize_body: 411 return self.initialize_body[key] 412 return None 413 414 def get_threads(self): 415 if self.threads is None: 416 self.request_threads() 417 return self.threads 418 419 def get_thread_id(self, threadIndex=0): 420 """Utility function to get the first thread ID in the thread list. 421 If the thread list is empty, then fetch the threads. 422 """ 423 if self.threads is None: 424 self.request_threads() 425 if self.threads and threadIndex < len(self.threads): 426 return self.threads[threadIndex]["id"] 427 return None 428 429 def get_stackFrame(self, frameIndex=0, threadId=None): 430 """Get a single "StackFrame" object from a "stackTrace" request and 431 return the "StackFrame" as a python dictionary, or None on failure 432 """ 433 if threadId is None: 434 threadId = self.get_thread_id() 435 if threadId is None: 436 print("invalid threadId") 437 return None 438 response = self.request_stackTrace(threadId, startFrame=frameIndex, levels=1) 439 if response: 440 return response["body"]["stackFrames"][0] 441 print("invalid response") 442 return None 443 444 def get_completions(self, text, frameId=None): 445 if frameId is None: 446 stackFrame = self.get_stackFrame() 447 frameId = stackFrame["id"] 448 response = self.request_completions(text, frameId) 449 return response["body"]["targets"] 450 451 def get_scope_variables(self, scope_name, frameIndex=0, threadId=None): 452 stackFrame = self.get_stackFrame(frameIndex=frameIndex, threadId=threadId) 453 if stackFrame is None: 454 return [] 455 frameId = stackFrame["id"] 456 if frameId in self.frame_scopes: 457 frame_scopes = self.frame_scopes[frameId] 458 else: 459 scopes_response = self.request_scopes(frameId) 460 frame_scopes = scopes_response["body"]["scopes"] 461 self.frame_scopes[frameId] = frame_scopes 462 for scope in frame_scopes: 463 if scope["name"] == scope_name: 464 varRef = scope["variablesReference"] 465 variables_response = self.request_variables(varRef) 466 if variables_response: 467 if "body" in variables_response: 468 body = variables_response["body"] 469 if "variables" in body: 470 vars = body["variables"] 471 return vars 472 return [] 473 474 def get_global_variables(self, frameIndex=0, threadId=None): 475 return self.get_scope_variables( 476 "Globals", frameIndex=frameIndex, threadId=threadId 477 ) 478 479 def get_local_variables(self, frameIndex=0, threadId=None): 480 return self.get_scope_variables( 481 "Locals", frameIndex=frameIndex, threadId=threadId 482 ) 483 484 def get_registers(self, frameIndex=0, threadId=None): 485 return self.get_scope_variables( 486 "Registers", frameIndex=frameIndex, threadId=threadId 487 ) 488 489 def get_local_variable(self, name, frameIndex=0, threadId=None): 490 locals = self.get_local_variables(frameIndex=frameIndex, threadId=threadId) 491 for local in locals: 492 if "name" in local and local["name"] == name: 493 return local 494 return None 495 496 def get_local_variable_value(self, name, frameIndex=0, threadId=None): 497 variable = self.get_local_variable( 498 name, frameIndex=frameIndex, threadId=threadId 499 ) 500 if variable and "value" in variable: 501 return variable["value"] 502 return None 503 504 def replay_packets(self, replay_file_path): 505 f = open(replay_file_path, "r") 506 mode = "invalid" 507 set_sequence = False 508 command_dict = None 509 while mode != "eof": 510 if mode == "invalid": 511 line = f.readline() 512 if line.startswith("to adapter:"): 513 mode = "send" 514 elif line.startswith("from adapter:"): 515 mode = "recv" 516 elif mode == "send": 517 command_dict = read_packet(f) 518 # Skip the end of line that follows the JSON 519 f.readline() 520 if command_dict is None: 521 raise ValueError("decode packet failed from replay file") 522 print("Sending:") 523 pprint.PrettyPrinter(indent=2).pprint(command_dict) 524 # raw_input('Press ENTER to send:') 525 self.send_packet(command_dict, set_sequence) 526 mode = "invalid" 527 elif mode == "recv": 528 print("Replay response:") 529 replay_response = read_packet(f) 530 # Skip the end of line that follows the JSON 531 f.readline() 532 pprint.PrettyPrinter(indent=2).pprint(replay_response) 533 actual_response = self.recv_packet() 534 if actual_response: 535 type = actual_response["type"] 536 print("Actual response:") 537 if type == "response": 538 self.validate_response(command_dict, actual_response) 539 pprint.PrettyPrinter(indent=2).pprint(actual_response) 540 else: 541 print("error: didn't get a valid response") 542 mode = "invalid" 543 544 def request_attach( 545 self, 546 program=None, 547 pid=None, 548 waitFor=None, 549 trace=None, 550 initCommands=None, 551 preRunCommands=None, 552 stopCommands=None, 553 exitCommands=None, 554 attachCommands=None, 555 terminateCommands=None, 556 coreFile=None, 557 postRunCommands=None, 558 sourceMap=None, 559 ): 560 args_dict = {} 561 if pid is not None: 562 args_dict["pid"] = pid 563 if program is not None: 564 args_dict["program"] = program 565 if waitFor is not None: 566 args_dict["waitFor"] = waitFor 567 if trace: 568 args_dict["trace"] = trace 569 args_dict["initCommands"] = self.init_commands 570 if initCommands: 571 args_dict["initCommands"].extend(initCommands) 572 if preRunCommands: 573 args_dict["preRunCommands"] = preRunCommands 574 if stopCommands: 575 args_dict["stopCommands"] = stopCommands 576 if exitCommands: 577 args_dict["exitCommands"] = exitCommands 578 if terminateCommands: 579 args_dict["terminateCommands"] = terminateCommands 580 if attachCommands: 581 args_dict["attachCommands"] = attachCommands 582 if coreFile: 583 args_dict["coreFile"] = coreFile 584 if postRunCommands: 585 args_dict["postRunCommands"] = postRunCommands 586 if sourceMap: 587 args_dict["sourceMap"] = sourceMap 588 command_dict = {"command": "attach", "type": "request", "arguments": args_dict} 589 return self.send_recv(command_dict) 590 591 def request_configurationDone(self): 592 command_dict = { 593 "command": "configurationDone", 594 "type": "request", 595 "arguments": {}, 596 } 597 response = self.send_recv(command_dict) 598 if response: 599 self.configuration_done_sent = True 600 return response 601 602 def _process_stopped(self): 603 self.threads = None 604 self.frame_scopes = {} 605 606 def request_continue(self, threadId=None): 607 if self.exit_status is not None: 608 raise ValueError("request_continue called after process exited") 609 # If we have launched or attached, then the first continue is done by 610 # sending the 'configurationDone' request 611 if not self.configuration_done_sent: 612 return self.request_configurationDone() 613 args_dict = {} 614 if threadId is None: 615 threadId = self.get_thread_id() 616 args_dict["threadId"] = threadId 617 command_dict = { 618 "command": "continue", 619 "type": "request", 620 "arguments": args_dict, 621 } 622 response = self.send_recv(command_dict) 623 # Caller must still call wait_for_stopped. 624 return response 625 626 def request_restart(self, restartArguments=None): 627 command_dict = { 628 "command": "restart", 629 "type": "request", 630 } 631 if restartArguments: 632 command_dict["arguments"] = restartArguments 633 634 response = self.send_recv(command_dict) 635 # Caller must still call wait_for_stopped. 636 return response 637 638 def request_disconnect(self, terminateDebuggee=None): 639 args_dict = {} 640 if terminateDebuggee is not None: 641 if terminateDebuggee: 642 args_dict["terminateDebuggee"] = True 643 else: 644 args_dict["terminateDebuggee"] = False 645 command_dict = { 646 "command": "disconnect", 647 "type": "request", 648 "arguments": args_dict, 649 } 650 return self.send_recv(command_dict) 651 652 def request_disassemble( 653 self, memoryReference, offset=-50, instructionCount=200, resolveSymbols=True 654 ): 655 args_dict = { 656 "memoryReference": memoryReference, 657 "offset": offset, 658 "instructionCount": instructionCount, 659 "resolveSymbols": resolveSymbols, 660 } 661 command_dict = { 662 "command": "disassemble", 663 "type": "request", 664 "arguments": args_dict, 665 } 666 instructions = self.send_recv(command_dict)["body"]["instructions"] 667 for inst in instructions: 668 self.disassembled_instructions[inst["address"]] = inst 669 670 def request_evaluate(self, expression, frameIndex=0, threadId=None, context=None): 671 stackFrame = self.get_stackFrame(frameIndex=frameIndex, threadId=threadId) 672 if stackFrame is None: 673 return [] 674 args_dict = { 675 "expression": expression, 676 "context": context, 677 "frameId": stackFrame["id"], 678 } 679 command_dict = { 680 "command": "evaluate", 681 "type": "request", 682 "arguments": args_dict, 683 } 684 return self.send_recv(command_dict) 685 686 def request_initialize(self, sourceInitFile): 687 command_dict = { 688 "command": "initialize", 689 "type": "request", 690 "arguments": { 691 "adapterID": "lldb-native", 692 "clientID": "vscode", 693 "columnsStartAt1": True, 694 "linesStartAt1": True, 695 "locale": "en-us", 696 "pathFormat": "path", 697 "supportsRunInTerminalRequest": True, 698 "supportsVariablePaging": True, 699 "supportsVariableType": True, 700 "supportsStartDebuggingRequest": True, 701 "sourceInitFile": sourceInitFile, 702 }, 703 } 704 response = self.send_recv(command_dict) 705 if response: 706 if "body" in response: 707 self.initialize_body = response["body"] 708 return response 709 710 def request_launch( 711 self, 712 program, 713 args=None, 714 cwd=None, 715 env=None, 716 stopOnEntry=False, 717 disableASLR=True, 718 disableSTDIO=False, 719 shellExpandArguments=False, 720 trace=False, 721 initCommands=None, 722 preRunCommands=None, 723 stopCommands=None, 724 exitCommands=None, 725 terminateCommands=None, 726 sourcePath=None, 727 debuggerRoot=None, 728 launchCommands=None, 729 sourceMap=None, 730 runInTerminal=False, 731 postRunCommands=None, 732 enableAutoVariableSummaries=False, 733 enableSyntheticChildDebugging=False, 734 commandEscapePrefix="`", 735 customFrameFormat=None, 736 ): 737 args_dict = {"program": program} 738 if args: 739 args_dict["args"] = args 740 if cwd: 741 args_dict["cwd"] = cwd 742 if env: 743 args_dict["env"] = env 744 if stopOnEntry: 745 args_dict["stopOnEntry"] = stopOnEntry 746 if disableASLR: 747 args_dict["disableASLR"] = disableASLR 748 if disableSTDIO: 749 args_dict["disableSTDIO"] = disableSTDIO 750 if shellExpandArguments: 751 args_dict["shellExpandArguments"] = shellExpandArguments 752 if trace: 753 args_dict["trace"] = trace 754 args_dict["initCommands"] = self.init_commands 755 if initCommands: 756 args_dict["initCommands"].extend(initCommands) 757 if preRunCommands: 758 args_dict["preRunCommands"] = preRunCommands 759 if stopCommands: 760 args_dict["stopCommands"] = stopCommands 761 if exitCommands: 762 args_dict["exitCommands"] = exitCommands 763 if terminateCommands: 764 args_dict["terminateCommands"] = terminateCommands 765 if sourcePath: 766 args_dict["sourcePath"] = sourcePath 767 if debuggerRoot: 768 args_dict["debuggerRoot"] = debuggerRoot 769 if launchCommands: 770 args_dict["launchCommands"] = launchCommands 771 if sourceMap: 772 args_dict["sourceMap"] = sourceMap 773 if runInTerminal: 774 args_dict["runInTerminal"] = runInTerminal 775 if postRunCommands: 776 args_dict["postRunCommands"] = postRunCommands 777 if customFrameFormat: 778 args_dict["customFrameFormat"] = customFrameFormat 779 780 args_dict["enableAutoVariableSummaries"] = enableAutoVariableSummaries 781 args_dict["enableSyntheticChildDebugging"] = enableSyntheticChildDebugging 782 args_dict["commandEscapePrefix"] = commandEscapePrefix 783 command_dict = {"command": "launch", "type": "request", "arguments": args_dict} 784 response = self.send_recv(command_dict) 785 786 if response["success"]: 787 # Wait for a 'process' and 'initialized' event in any order 788 self.wait_for_event(filter=["process", "initialized"]) 789 self.wait_for_event(filter=["process", "initialized"]) 790 return response 791 792 def request_next(self, threadId): 793 if self.exit_status is not None: 794 raise ValueError("request_continue called after process exited") 795 args_dict = {"threadId": threadId} 796 command_dict = {"command": "next", "type": "request", "arguments": args_dict} 797 return self.send_recv(command_dict) 798 799 def request_stepIn(self, threadId): 800 if self.exit_status is not None: 801 raise ValueError("request_continue called after process exited") 802 args_dict = {"threadId": threadId} 803 command_dict = {"command": "stepIn", "type": "request", "arguments": args_dict} 804 return self.send_recv(command_dict) 805 806 def request_stepOut(self, threadId): 807 if self.exit_status is not None: 808 raise ValueError("request_continue called after process exited") 809 args_dict = {"threadId": threadId} 810 command_dict = {"command": "stepOut", "type": "request", "arguments": args_dict} 811 return self.send_recv(command_dict) 812 813 def request_pause(self, threadId=None): 814 if self.exit_status is not None: 815 raise ValueError("request_continue called after process exited") 816 if threadId is None: 817 threadId = self.get_thread_id() 818 args_dict = {"threadId": threadId} 819 command_dict = {"command": "pause", "type": "request", "arguments": args_dict} 820 return self.send_recv(command_dict) 821 822 def request_scopes(self, frameId): 823 args_dict = {"frameId": frameId} 824 command_dict = {"command": "scopes", "type": "request", "arguments": args_dict} 825 return self.send_recv(command_dict) 826 827 def request_setBreakpoints(self, file_path, line_array, data=None): 828 """data is array of parameters for breakpoints in line_array. 829 Each parameter object is 1:1 mapping with entries in line_entry. 830 It contains optional location/hitCondition/logMessage parameters. 831 """ 832 (dir, base) = os.path.split(file_path) 833 source_dict = {"name": base, "path": file_path} 834 args_dict = { 835 "source": source_dict, 836 "sourceModified": False, 837 } 838 if line_array is not None: 839 args_dict["lines"] = "%s" % line_array 840 breakpoints = [] 841 for i, line in enumerate(line_array): 842 breakpoint_data = None 843 if data is not None and i < len(data): 844 breakpoint_data = data[i] 845 bp = {"line": line} 846 if breakpoint_data is not None: 847 if "condition" in breakpoint_data and breakpoint_data["condition"]: 848 bp["condition"] = breakpoint_data["condition"] 849 if ( 850 "hitCondition" in breakpoint_data 851 and breakpoint_data["hitCondition"] 852 ): 853 bp["hitCondition"] = breakpoint_data["hitCondition"] 854 if ( 855 "logMessage" in breakpoint_data 856 and breakpoint_data["logMessage"] 857 ): 858 bp["logMessage"] = breakpoint_data["logMessage"] 859 breakpoints.append(bp) 860 args_dict["breakpoints"] = breakpoints 861 862 command_dict = { 863 "command": "setBreakpoints", 864 "type": "request", 865 "arguments": args_dict, 866 } 867 return self.send_recv(command_dict) 868 869 def request_setExceptionBreakpoints(self, filters): 870 args_dict = {"filters": filters} 871 command_dict = { 872 "command": "setExceptionBreakpoints", 873 "type": "request", 874 "arguments": args_dict, 875 } 876 return self.send_recv(command_dict) 877 878 def request_setFunctionBreakpoints(self, names, condition=None, hitCondition=None): 879 breakpoints = [] 880 for name in names: 881 bp = {"name": name} 882 if condition is not None: 883 bp["condition"] = condition 884 if hitCondition is not None: 885 bp["hitCondition"] = hitCondition 886 breakpoints.append(bp) 887 args_dict = {"breakpoints": breakpoints} 888 command_dict = { 889 "command": "setFunctionBreakpoints", 890 "type": "request", 891 "arguments": args_dict, 892 } 893 return self.send_recv(command_dict) 894 895 def request_compileUnits(self, moduleId): 896 args_dict = {"moduleId": moduleId} 897 command_dict = { 898 "command": "compileUnits", 899 "type": "request", 900 "arguments": args_dict, 901 } 902 response = self.send_recv(command_dict) 903 return response 904 905 def request_completions(self, text, frameId=None): 906 args_dict = {"text": text, "column": len(text)} 907 if frameId: 908 args_dict["frameId"] = frameId 909 command_dict = { 910 "command": "completions", 911 "type": "request", 912 "arguments": args_dict, 913 } 914 return self.send_recv(command_dict) 915 916 def request_modules(self): 917 return self.send_recv({"command": "modules", "type": "request"}) 918 919 def request_stackTrace( 920 self, threadId=None, startFrame=None, levels=None, dump=False 921 ): 922 if threadId is None: 923 threadId = self.get_thread_id() 924 args_dict = {"threadId": threadId} 925 if startFrame is not None: 926 args_dict["startFrame"] = startFrame 927 if levels is not None: 928 args_dict["levels"] = levels 929 command_dict = { 930 "command": "stackTrace", 931 "type": "request", 932 "arguments": args_dict, 933 } 934 response = self.send_recv(command_dict) 935 if dump: 936 for idx, frame in enumerate(response["body"]["stackFrames"]): 937 name = frame["name"] 938 if "line" in frame and "source" in frame: 939 source = frame["source"] 940 if "sourceReference" not in source: 941 if "name" in source: 942 source_name = source["name"] 943 line = frame["line"] 944 print("[%3u] %s @ %s:%u" % (idx, name, source_name, line)) 945 continue 946 print("[%3u] %s" % (idx, name)) 947 return response 948 949 def request_threads(self): 950 """Request a list of all threads and combine any information from any 951 "stopped" events since those contain more information about why a 952 thread actually stopped. Returns an array of thread dictionaries 953 with information about all threads""" 954 command_dict = {"command": "threads", "type": "request", "arguments": {}} 955 response = self.send_recv(command_dict) 956 body = response["body"] 957 # Fill in "self.threads" correctly so that clients that call 958 # self.get_threads() or self.get_thread_id(...) can get information 959 # on threads when the process is stopped. 960 if "threads" in body: 961 self.threads = body["threads"] 962 for thread in self.threads: 963 # Copy the thread dictionary so we can add key/value pairs to 964 # it without affecting the original info from the "threads" 965 # command. 966 tid = thread["id"] 967 if tid in self.thread_stop_reasons: 968 thread_stop_info = self.thread_stop_reasons[tid] 969 copy_keys = ["reason", "description", "text"] 970 for key in copy_keys: 971 if key in thread_stop_info: 972 thread[key] = thread_stop_info[key] 973 else: 974 self.threads = None 975 return response 976 977 def request_variables(self, variablesReference, start=None, count=None): 978 args_dict = {"variablesReference": variablesReference} 979 if start is not None: 980 args_dict["start"] = start 981 if count is not None: 982 args_dict["count"] = count 983 command_dict = { 984 "command": "variables", 985 "type": "request", 986 "arguments": args_dict, 987 } 988 return self.send_recv(command_dict) 989 990 def request_setVariable(self, containingVarRef, name, value, id=None): 991 args_dict = { 992 "variablesReference": containingVarRef, 993 "name": name, 994 "value": str(value), 995 } 996 if id is not None: 997 args_dict["id"] = id 998 command_dict = { 999 "command": "setVariable", 1000 "type": "request", 1001 "arguments": args_dict, 1002 } 1003 return self.send_recv(command_dict) 1004 1005 def request_testGetTargetBreakpoints(self): 1006 """A request packet used in the LLDB test suite to get all currently 1007 set breakpoint infos for all breakpoints currently set in the 1008 target. 1009 """ 1010 command_dict = { 1011 "command": "_testGetTargetBreakpoints", 1012 "type": "request", 1013 "arguments": {}, 1014 } 1015 return self.send_recv(command_dict) 1016 1017 def terminate(self): 1018 self.send.close() 1019 # self.recv.close() 1020 1021 1022class DebugAdaptorServer(DebugCommunication): 1023 def __init__( 1024 self, 1025 executable=None, 1026 port=None, 1027 init_commands=[], 1028 log_file=None, 1029 env=None, 1030 ): 1031 self.process = None 1032 if executable is not None: 1033 adaptor_env = os.environ.copy() 1034 if env is not None: 1035 adaptor_env.update(env) 1036 1037 if log_file: 1038 adaptor_env["LLDBDAP_LOG"] = log_file 1039 self.process = subprocess.Popen( 1040 [executable], 1041 stdin=subprocess.PIPE, 1042 stdout=subprocess.PIPE, 1043 stderr=subprocess.PIPE, 1044 env=adaptor_env, 1045 ) 1046 DebugCommunication.__init__( 1047 self, self.process.stdout, self.process.stdin, init_commands, log_file 1048 ) 1049 elif port is not None: 1050 s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 1051 s.connect(("127.0.0.1", port)) 1052 DebugCommunication.__init__( 1053 self, s.makefile("r"), s.makefile("w"), init_commands 1054 ) 1055 1056 def get_pid(self): 1057 if self.process: 1058 return self.process.pid 1059 return -1 1060 1061 def terminate(self): 1062 super(DebugAdaptorServer, self).terminate() 1063 if self.process is not None: 1064 self.process.terminate() 1065 self.process.wait() 1066 self.process = None 1067 1068 1069def attach_options_specified(options): 1070 if options.pid is not None: 1071 return True 1072 if options.waitFor: 1073 return True 1074 if options.attach: 1075 return True 1076 if options.attachCmds: 1077 return True 1078 return False 1079 1080 1081def run_vscode(dbg, args, options): 1082 dbg.request_initialize(options.sourceInitFile) 1083 if attach_options_specified(options): 1084 response = dbg.request_attach( 1085 program=options.program, 1086 pid=options.pid, 1087 waitFor=options.waitFor, 1088 attachCommands=options.attachCmds, 1089 initCommands=options.initCmds, 1090 preRunCommands=options.preRunCmds, 1091 stopCommands=options.stopCmds, 1092 exitCommands=options.exitCmds, 1093 terminateCommands=options.terminateCmds, 1094 ) 1095 else: 1096 response = dbg.request_launch( 1097 options.program, 1098 args=args, 1099 env=options.envs, 1100 cwd=options.workingDir, 1101 debuggerRoot=options.debuggerRoot, 1102 sourcePath=options.sourcePath, 1103 initCommands=options.initCmds, 1104 preRunCommands=options.preRunCmds, 1105 stopCommands=options.stopCmds, 1106 exitCommands=options.exitCmds, 1107 terminateCommands=options.terminateCmds, 1108 ) 1109 1110 if response["success"]: 1111 if options.sourceBreakpoints: 1112 source_to_lines = {} 1113 for file_line in options.sourceBreakpoints: 1114 (path, line) = file_line.split(":") 1115 if len(path) == 0 or len(line) == 0: 1116 print('error: invalid source with line "%s"' % (file_line)) 1117 1118 else: 1119 if path in source_to_lines: 1120 source_to_lines[path].append(int(line)) 1121 else: 1122 source_to_lines[path] = [int(line)] 1123 for source in source_to_lines: 1124 dbg.request_setBreakpoints(source, source_to_lines[source]) 1125 if options.funcBreakpoints: 1126 dbg.request_setFunctionBreakpoints(options.funcBreakpoints) 1127 dbg.request_configurationDone() 1128 dbg.wait_for_stopped() 1129 else: 1130 if "message" in response: 1131 print(response["message"]) 1132 dbg.request_disconnect(terminateDebuggee=True) 1133 1134 1135def main(): 1136 parser = optparse.OptionParser( 1137 description=( 1138 "A testing framework for the Visual Studio Code Debug " "Adaptor protocol" 1139 ) 1140 ) 1141 1142 parser.add_option( 1143 "--vscode", 1144 type="string", 1145 dest="vscode_path", 1146 help=( 1147 "The path to the command line program that implements the " 1148 "Visual Studio Code Debug Adaptor protocol." 1149 ), 1150 default=None, 1151 ) 1152 1153 parser.add_option( 1154 "--program", 1155 type="string", 1156 dest="program", 1157 help="The path to the program to debug.", 1158 default=None, 1159 ) 1160 1161 parser.add_option( 1162 "--workingDir", 1163 type="string", 1164 dest="workingDir", 1165 default=None, 1166 help="Set the working directory for the process we launch.", 1167 ) 1168 1169 parser.add_option( 1170 "--sourcePath", 1171 type="string", 1172 dest="sourcePath", 1173 default=None, 1174 help=( 1175 "Set the relative source root for any debug info that has " 1176 "relative paths in it." 1177 ), 1178 ) 1179 1180 parser.add_option( 1181 "--debuggerRoot", 1182 type="string", 1183 dest="debuggerRoot", 1184 default=None, 1185 help=( 1186 "Set the working directory for lldb-dap for any object files " 1187 "with relative paths in the Mach-o debug map." 1188 ), 1189 ) 1190 1191 parser.add_option( 1192 "-r", 1193 "--replay", 1194 type="string", 1195 dest="replay", 1196 help=( 1197 "Specify a file containing a packet log to replay with the " 1198 "current Visual Studio Code Debug Adaptor executable." 1199 ), 1200 default=None, 1201 ) 1202 1203 parser.add_option( 1204 "-g", 1205 "--debug", 1206 action="store_true", 1207 dest="debug", 1208 default=False, 1209 help="Pause waiting for a debugger to attach to the debug adaptor", 1210 ) 1211 1212 parser.add_option( 1213 "--sourceInitFile", 1214 action="store_true", 1215 dest="sourceInitFile", 1216 default=False, 1217 help="Whether lldb-dap should source .lldbinit file or not", 1218 ) 1219 1220 parser.add_option( 1221 "--port", 1222 type="int", 1223 dest="port", 1224 help="Attach a socket to a port instead of using STDIN for VSCode", 1225 default=None, 1226 ) 1227 1228 parser.add_option( 1229 "--pid", 1230 type="int", 1231 dest="pid", 1232 help="The process ID to attach to", 1233 default=None, 1234 ) 1235 1236 parser.add_option( 1237 "--attach", 1238 action="store_true", 1239 dest="attach", 1240 default=False, 1241 help=( 1242 "Specify this option to attach to a process by name. The " 1243 "process name is the basename of the executable specified with " 1244 "the --program option." 1245 ), 1246 ) 1247 1248 parser.add_option( 1249 "-f", 1250 "--function-bp", 1251 type="string", 1252 action="append", 1253 dest="funcBreakpoints", 1254 help=( 1255 "Specify the name of a function to break at. " 1256 "Can be specified more than once." 1257 ), 1258 default=[], 1259 ) 1260 1261 parser.add_option( 1262 "-s", 1263 "--source-bp", 1264 type="string", 1265 action="append", 1266 dest="sourceBreakpoints", 1267 default=[], 1268 help=( 1269 "Specify source breakpoints to set in the format of " 1270 "<source>:<line>. " 1271 "Can be specified more than once." 1272 ), 1273 ) 1274 1275 parser.add_option( 1276 "--attachCommand", 1277 type="string", 1278 action="append", 1279 dest="attachCmds", 1280 default=[], 1281 help=( 1282 "Specify a LLDB command that will attach to a process. " 1283 "Can be specified more than once." 1284 ), 1285 ) 1286 1287 parser.add_option( 1288 "--initCommand", 1289 type="string", 1290 action="append", 1291 dest="initCmds", 1292 default=[], 1293 help=( 1294 "Specify a LLDB command that will be executed before the target " 1295 "is created. Can be specified more than once." 1296 ), 1297 ) 1298 1299 parser.add_option( 1300 "--preRunCommand", 1301 type="string", 1302 action="append", 1303 dest="preRunCmds", 1304 default=[], 1305 help=( 1306 "Specify a LLDB command that will be executed after the target " 1307 "has been created. Can be specified more than once." 1308 ), 1309 ) 1310 1311 parser.add_option( 1312 "--stopCommand", 1313 type="string", 1314 action="append", 1315 dest="stopCmds", 1316 default=[], 1317 help=( 1318 "Specify a LLDB command that will be executed each time the" 1319 "process stops. Can be specified more than once." 1320 ), 1321 ) 1322 1323 parser.add_option( 1324 "--exitCommand", 1325 type="string", 1326 action="append", 1327 dest="exitCmds", 1328 default=[], 1329 help=( 1330 "Specify a LLDB command that will be executed when the process " 1331 "exits. Can be specified more than once." 1332 ), 1333 ) 1334 1335 parser.add_option( 1336 "--terminateCommand", 1337 type="string", 1338 action="append", 1339 dest="terminateCmds", 1340 default=[], 1341 help=( 1342 "Specify a LLDB command that will be executed when the debugging " 1343 "session is terminated. Can be specified more than once." 1344 ), 1345 ) 1346 1347 parser.add_option( 1348 "--env", 1349 type="string", 1350 action="append", 1351 dest="envs", 1352 default=[], 1353 help=("Specify environment variables to pass to the launched " "process."), 1354 ) 1355 1356 parser.add_option( 1357 "--waitFor", 1358 action="store_true", 1359 dest="waitFor", 1360 default=False, 1361 help=( 1362 "Wait for the next process to be launched whose name matches " 1363 "the basename of the program specified with the --program " 1364 "option" 1365 ), 1366 ) 1367 1368 (options, args) = parser.parse_args(sys.argv[1:]) 1369 1370 if options.vscode_path is None and options.port is None: 1371 print( 1372 "error: must either specify a path to a Visual Studio Code " 1373 "Debug Adaptor vscode executable path using the --vscode " 1374 "option, or a port to attach to for an existing lldb-dap " 1375 "using the --port option" 1376 ) 1377 return 1378 dbg = DebugAdaptorServer(executable=options.vscode_path, port=options.port) 1379 if options.debug: 1380 raw_input('Waiting for debugger to attach pid "%i"' % (dbg.get_pid())) 1381 if options.replay: 1382 dbg.replay_packets(options.replay) 1383 else: 1384 run_vscode(dbg, args, options) 1385 dbg.terminate() 1386 1387 1388if __name__ == "__main__": 1389 main() 1390