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 customThreadFormat=None, 737 ): 738 args_dict = {"program": program} 739 if args: 740 args_dict["args"] = args 741 if cwd: 742 args_dict["cwd"] = cwd 743 if env: 744 args_dict["env"] = env 745 if stopOnEntry: 746 args_dict["stopOnEntry"] = stopOnEntry 747 if disableASLR: 748 args_dict["disableASLR"] = disableASLR 749 if disableSTDIO: 750 args_dict["disableSTDIO"] = disableSTDIO 751 if shellExpandArguments: 752 args_dict["shellExpandArguments"] = shellExpandArguments 753 if trace: 754 args_dict["trace"] = trace 755 args_dict["initCommands"] = self.init_commands 756 if initCommands: 757 args_dict["initCommands"].extend(initCommands) 758 if preRunCommands: 759 args_dict["preRunCommands"] = preRunCommands 760 if stopCommands: 761 args_dict["stopCommands"] = stopCommands 762 if exitCommands: 763 args_dict["exitCommands"] = exitCommands 764 if terminateCommands: 765 args_dict["terminateCommands"] = terminateCommands 766 if sourcePath: 767 args_dict["sourcePath"] = sourcePath 768 if debuggerRoot: 769 args_dict["debuggerRoot"] = debuggerRoot 770 if launchCommands: 771 args_dict["launchCommands"] = launchCommands 772 if sourceMap: 773 args_dict["sourceMap"] = sourceMap 774 if runInTerminal: 775 args_dict["runInTerminal"] = runInTerminal 776 if postRunCommands: 777 args_dict["postRunCommands"] = postRunCommands 778 if customFrameFormat: 779 args_dict["customFrameFormat"] = customFrameFormat 780 if customThreadFormat: 781 args_dict["customThreadFormat"] = customThreadFormat 782 783 args_dict["enableAutoVariableSummaries"] = enableAutoVariableSummaries 784 args_dict["enableSyntheticChildDebugging"] = enableSyntheticChildDebugging 785 args_dict["commandEscapePrefix"] = commandEscapePrefix 786 command_dict = {"command": "launch", "type": "request", "arguments": args_dict} 787 response = self.send_recv(command_dict) 788 789 if response["success"]: 790 # Wait for a 'process' and 'initialized' event in any order 791 self.wait_for_event(filter=["process", "initialized"]) 792 self.wait_for_event(filter=["process", "initialized"]) 793 return response 794 795 def request_next(self, threadId): 796 if self.exit_status is not None: 797 raise ValueError("request_continue called after process exited") 798 args_dict = {"threadId": threadId} 799 command_dict = {"command": "next", "type": "request", "arguments": args_dict} 800 return self.send_recv(command_dict) 801 802 def request_stepIn(self, threadId): 803 if self.exit_status is not None: 804 raise ValueError("request_continue called after process exited") 805 args_dict = {"threadId": threadId} 806 command_dict = {"command": "stepIn", "type": "request", "arguments": args_dict} 807 return self.send_recv(command_dict) 808 809 def request_stepOut(self, threadId): 810 if self.exit_status is not None: 811 raise ValueError("request_continue called after process exited") 812 args_dict = {"threadId": threadId} 813 command_dict = {"command": "stepOut", "type": "request", "arguments": args_dict} 814 return self.send_recv(command_dict) 815 816 def request_pause(self, threadId=None): 817 if self.exit_status is not None: 818 raise ValueError("request_continue called after process exited") 819 if threadId is None: 820 threadId = self.get_thread_id() 821 args_dict = {"threadId": threadId} 822 command_dict = {"command": "pause", "type": "request", "arguments": args_dict} 823 return self.send_recv(command_dict) 824 825 def request_scopes(self, frameId): 826 args_dict = {"frameId": frameId} 827 command_dict = {"command": "scopes", "type": "request", "arguments": args_dict} 828 return self.send_recv(command_dict) 829 830 def request_setBreakpoints(self, file_path, line_array, data=None): 831 """data is array of parameters for breakpoints in line_array. 832 Each parameter object is 1:1 mapping with entries in line_entry. 833 It contains optional location/hitCondition/logMessage parameters. 834 """ 835 (dir, base) = os.path.split(file_path) 836 source_dict = {"name": base, "path": file_path} 837 args_dict = { 838 "source": source_dict, 839 "sourceModified": False, 840 } 841 if line_array is not None: 842 args_dict["lines"] = "%s" % line_array 843 breakpoints = [] 844 for i, line in enumerate(line_array): 845 breakpoint_data = None 846 if data is not None and i < len(data): 847 breakpoint_data = data[i] 848 bp = {"line": line} 849 if breakpoint_data is not None: 850 if "condition" in breakpoint_data and breakpoint_data["condition"]: 851 bp["condition"] = breakpoint_data["condition"] 852 if ( 853 "hitCondition" in breakpoint_data 854 and breakpoint_data["hitCondition"] 855 ): 856 bp["hitCondition"] = breakpoint_data["hitCondition"] 857 if ( 858 "logMessage" in breakpoint_data 859 and breakpoint_data["logMessage"] 860 ): 861 bp["logMessage"] = breakpoint_data["logMessage"] 862 breakpoints.append(bp) 863 args_dict["breakpoints"] = breakpoints 864 865 command_dict = { 866 "command": "setBreakpoints", 867 "type": "request", 868 "arguments": args_dict, 869 } 870 return self.send_recv(command_dict) 871 872 def request_setExceptionBreakpoints(self, filters): 873 args_dict = {"filters": filters} 874 command_dict = { 875 "command": "setExceptionBreakpoints", 876 "type": "request", 877 "arguments": args_dict, 878 } 879 return self.send_recv(command_dict) 880 881 def request_setFunctionBreakpoints(self, names, condition=None, hitCondition=None): 882 breakpoints = [] 883 for name in names: 884 bp = {"name": name} 885 if condition is not None: 886 bp["condition"] = condition 887 if hitCondition is not None: 888 bp["hitCondition"] = hitCondition 889 breakpoints.append(bp) 890 args_dict = {"breakpoints": breakpoints} 891 command_dict = { 892 "command": "setFunctionBreakpoints", 893 "type": "request", 894 "arguments": args_dict, 895 } 896 return self.send_recv(command_dict) 897 898 def request_compileUnits(self, moduleId): 899 args_dict = {"moduleId": moduleId} 900 command_dict = { 901 "command": "compileUnits", 902 "type": "request", 903 "arguments": args_dict, 904 } 905 response = self.send_recv(command_dict) 906 return response 907 908 def request_completions(self, text, frameId=None): 909 args_dict = {"text": text, "column": len(text)} 910 if frameId: 911 args_dict["frameId"] = frameId 912 command_dict = { 913 "command": "completions", 914 "type": "request", 915 "arguments": args_dict, 916 } 917 return self.send_recv(command_dict) 918 919 def request_modules(self): 920 return self.send_recv({"command": "modules", "type": "request"}) 921 922 def request_stackTrace( 923 self, threadId=None, startFrame=None, levels=None, dump=False 924 ): 925 if threadId is None: 926 threadId = self.get_thread_id() 927 args_dict = {"threadId": threadId} 928 if startFrame is not None: 929 args_dict["startFrame"] = startFrame 930 if levels is not None: 931 args_dict["levels"] = levels 932 command_dict = { 933 "command": "stackTrace", 934 "type": "request", 935 "arguments": args_dict, 936 } 937 response = self.send_recv(command_dict) 938 if dump: 939 for idx, frame in enumerate(response["body"]["stackFrames"]): 940 name = frame["name"] 941 if "line" in frame and "source" in frame: 942 source = frame["source"] 943 if "sourceReference" not in source: 944 if "name" in source: 945 source_name = source["name"] 946 line = frame["line"] 947 print("[%3u] %s @ %s:%u" % (idx, name, source_name, line)) 948 continue 949 print("[%3u] %s" % (idx, name)) 950 return response 951 952 def request_threads(self): 953 """Request a list of all threads and combine any information from any 954 "stopped" events since those contain more information about why a 955 thread actually stopped. Returns an array of thread dictionaries 956 with information about all threads""" 957 command_dict = {"command": "threads", "type": "request", "arguments": {}} 958 response = self.send_recv(command_dict) 959 body = response["body"] 960 # Fill in "self.threads" correctly so that clients that call 961 # self.get_threads() or self.get_thread_id(...) can get information 962 # on threads when the process is stopped. 963 if "threads" in body: 964 self.threads = body["threads"] 965 for thread in self.threads: 966 # Copy the thread dictionary so we can add key/value pairs to 967 # it without affecting the original info from the "threads" 968 # command. 969 tid = thread["id"] 970 if tid in self.thread_stop_reasons: 971 thread_stop_info = self.thread_stop_reasons[tid] 972 copy_keys = ["reason", "description", "text"] 973 for key in copy_keys: 974 if key in thread_stop_info: 975 thread[key] = thread_stop_info[key] 976 else: 977 self.threads = None 978 return response 979 980 def request_variables(self, variablesReference, start=None, count=None): 981 args_dict = {"variablesReference": variablesReference} 982 if start is not None: 983 args_dict["start"] = start 984 if count is not None: 985 args_dict["count"] = count 986 command_dict = { 987 "command": "variables", 988 "type": "request", 989 "arguments": args_dict, 990 } 991 return self.send_recv(command_dict) 992 993 def request_setVariable(self, containingVarRef, name, value, id=None): 994 args_dict = { 995 "variablesReference": containingVarRef, 996 "name": name, 997 "value": str(value), 998 } 999 if id is not None: 1000 args_dict["id"] = id 1001 command_dict = { 1002 "command": "setVariable", 1003 "type": "request", 1004 "arguments": args_dict, 1005 } 1006 return self.send_recv(command_dict) 1007 1008 def request_testGetTargetBreakpoints(self): 1009 """A request packet used in the LLDB test suite to get all currently 1010 set breakpoint infos for all breakpoints currently set in the 1011 target. 1012 """ 1013 command_dict = { 1014 "command": "_testGetTargetBreakpoints", 1015 "type": "request", 1016 "arguments": {}, 1017 } 1018 return self.send_recv(command_dict) 1019 1020 def terminate(self): 1021 self.send.close() 1022 # self.recv.close() 1023 1024 1025class DebugAdaptorServer(DebugCommunication): 1026 def __init__( 1027 self, 1028 executable=None, 1029 port=None, 1030 init_commands=[], 1031 log_file=None, 1032 env=None, 1033 ): 1034 self.process = None 1035 if executable is not None: 1036 adaptor_env = os.environ.copy() 1037 if env is not None: 1038 adaptor_env.update(env) 1039 1040 if log_file: 1041 adaptor_env["LLDBDAP_LOG"] = log_file 1042 self.process = subprocess.Popen( 1043 [executable], 1044 stdin=subprocess.PIPE, 1045 stdout=subprocess.PIPE, 1046 stderr=subprocess.PIPE, 1047 env=adaptor_env, 1048 ) 1049 DebugCommunication.__init__( 1050 self, self.process.stdout, self.process.stdin, init_commands, log_file 1051 ) 1052 elif port is not None: 1053 s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 1054 s.connect(("127.0.0.1", port)) 1055 DebugCommunication.__init__( 1056 self, s.makefile("r"), s.makefile("w"), init_commands 1057 ) 1058 1059 def get_pid(self): 1060 if self.process: 1061 return self.process.pid 1062 return -1 1063 1064 def terminate(self): 1065 super(DebugAdaptorServer, self).terminate() 1066 if self.process is not None: 1067 self.process.terminate() 1068 self.process.wait() 1069 self.process = None 1070 1071 1072def attach_options_specified(options): 1073 if options.pid is not None: 1074 return True 1075 if options.waitFor: 1076 return True 1077 if options.attach: 1078 return True 1079 if options.attachCmds: 1080 return True 1081 return False 1082 1083 1084def run_vscode(dbg, args, options): 1085 dbg.request_initialize(options.sourceInitFile) 1086 if attach_options_specified(options): 1087 response = dbg.request_attach( 1088 program=options.program, 1089 pid=options.pid, 1090 waitFor=options.waitFor, 1091 attachCommands=options.attachCmds, 1092 initCommands=options.initCmds, 1093 preRunCommands=options.preRunCmds, 1094 stopCommands=options.stopCmds, 1095 exitCommands=options.exitCmds, 1096 terminateCommands=options.terminateCmds, 1097 ) 1098 else: 1099 response = dbg.request_launch( 1100 options.program, 1101 args=args, 1102 env=options.envs, 1103 cwd=options.workingDir, 1104 debuggerRoot=options.debuggerRoot, 1105 sourcePath=options.sourcePath, 1106 initCommands=options.initCmds, 1107 preRunCommands=options.preRunCmds, 1108 stopCommands=options.stopCmds, 1109 exitCommands=options.exitCmds, 1110 terminateCommands=options.terminateCmds, 1111 ) 1112 1113 if response["success"]: 1114 if options.sourceBreakpoints: 1115 source_to_lines = {} 1116 for file_line in options.sourceBreakpoints: 1117 (path, line) = file_line.split(":") 1118 if len(path) == 0 or len(line) == 0: 1119 print('error: invalid source with line "%s"' % (file_line)) 1120 1121 else: 1122 if path in source_to_lines: 1123 source_to_lines[path].append(int(line)) 1124 else: 1125 source_to_lines[path] = [int(line)] 1126 for source in source_to_lines: 1127 dbg.request_setBreakpoints(source, source_to_lines[source]) 1128 if options.funcBreakpoints: 1129 dbg.request_setFunctionBreakpoints(options.funcBreakpoints) 1130 dbg.request_configurationDone() 1131 dbg.wait_for_stopped() 1132 else: 1133 if "message" in response: 1134 print(response["message"]) 1135 dbg.request_disconnect(terminateDebuggee=True) 1136 1137 1138def main(): 1139 parser = optparse.OptionParser( 1140 description=( 1141 "A testing framework for the Visual Studio Code Debug " "Adaptor protocol" 1142 ) 1143 ) 1144 1145 parser.add_option( 1146 "--vscode", 1147 type="string", 1148 dest="vscode_path", 1149 help=( 1150 "The path to the command line program that implements the " 1151 "Visual Studio Code Debug Adaptor protocol." 1152 ), 1153 default=None, 1154 ) 1155 1156 parser.add_option( 1157 "--program", 1158 type="string", 1159 dest="program", 1160 help="The path to the program to debug.", 1161 default=None, 1162 ) 1163 1164 parser.add_option( 1165 "--workingDir", 1166 type="string", 1167 dest="workingDir", 1168 default=None, 1169 help="Set the working directory for the process we launch.", 1170 ) 1171 1172 parser.add_option( 1173 "--sourcePath", 1174 type="string", 1175 dest="sourcePath", 1176 default=None, 1177 help=( 1178 "Set the relative source root for any debug info that has " 1179 "relative paths in it." 1180 ), 1181 ) 1182 1183 parser.add_option( 1184 "--debuggerRoot", 1185 type="string", 1186 dest="debuggerRoot", 1187 default=None, 1188 help=( 1189 "Set the working directory for lldb-dap for any object files " 1190 "with relative paths in the Mach-o debug map." 1191 ), 1192 ) 1193 1194 parser.add_option( 1195 "-r", 1196 "--replay", 1197 type="string", 1198 dest="replay", 1199 help=( 1200 "Specify a file containing a packet log to replay with the " 1201 "current Visual Studio Code Debug Adaptor executable." 1202 ), 1203 default=None, 1204 ) 1205 1206 parser.add_option( 1207 "-g", 1208 "--debug", 1209 action="store_true", 1210 dest="debug", 1211 default=False, 1212 help="Pause waiting for a debugger to attach to the debug adaptor", 1213 ) 1214 1215 parser.add_option( 1216 "--sourceInitFile", 1217 action="store_true", 1218 dest="sourceInitFile", 1219 default=False, 1220 help="Whether lldb-dap should source .lldbinit file or not", 1221 ) 1222 1223 parser.add_option( 1224 "--port", 1225 type="int", 1226 dest="port", 1227 help="Attach a socket to a port instead of using STDIN for VSCode", 1228 default=None, 1229 ) 1230 1231 parser.add_option( 1232 "--pid", 1233 type="int", 1234 dest="pid", 1235 help="The process ID to attach to", 1236 default=None, 1237 ) 1238 1239 parser.add_option( 1240 "--attach", 1241 action="store_true", 1242 dest="attach", 1243 default=False, 1244 help=( 1245 "Specify this option to attach to a process by name. The " 1246 "process name is the basename of the executable specified with " 1247 "the --program option." 1248 ), 1249 ) 1250 1251 parser.add_option( 1252 "-f", 1253 "--function-bp", 1254 type="string", 1255 action="append", 1256 dest="funcBreakpoints", 1257 help=( 1258 "Specify the name of a function to break at. " 1259 "Can be specified more than once." 1260 ), 1261 default=[], 1262 ) 1263 1264 parser.add_option( 1265 "-s", 1266 "--source-bp", 1267 type="string", 1268 action="append", 1269 dest="sourceBreakpoints", 1270 default=[], 1271 help=( 1272 "Specify source breakpoints to set in the format of " 1273 "<source>:<line>. " 1274 "Can be specified more than once." 1275 ), 1276 ) 1277 1278 parser.add_option( 1279 "--attachCommand", 1280 type="string", 1281 action="append", 1282 dest="attachCmds", 1283 default=[], 1284 help=( 1285 "Specify a LLDB command that will attach to a process. " 1286 "Can be specified more than once." 1287 ), 1288 ) 1289 1290 parser.add_option( 1291 "--initCommand", 1292 type="string", 1293 action="append", 1294 dest="initCmds", 1295 default=[], 1296 help=( 1297 "Specify a LLDB command that will be executed before the target " 1298 "is created. Can be specified more than once." 1299 ), 1300 ) 1301 1302 parser.add_option( 1303 "--preRunCommand", 1304 type="string", 1305 action="append", 1306 dest="preRunCmds", 1307 default=[], 1308 help=( 1309 "Specify a LLDB command that will be executed after the target " 1310 "has been created. Can be specified more than once." 1311 ), 1312 ) 1313 1314 parser.add_option( 1315 "--stopCommand", 1316 type="string", 1317 action="append", 1318 dest="stopCmds", 1319 default=[], 1320 help=( 1321 "Specify a LLDB command that will be executed each time the" 1322 "process stops. Can be specified more than once." 1323 ), 1324 ) 1325 1326 parser.add_option( 1327 "--exitCommand", 1328 type="string", 1329 action="append", 1330 dest="exitCmds", 1331 default=[], 1332 help=( 1333 "Specify a LLDB command that will be executed when the process " 1334 "exits. Can be specified more than once." 1335 ), 1336 ) 1337 1338 parser.add_option( 1339 "--terminateCommand", 1340 type="string", 1341 action="append", 1342 dest="terminateCmds", 1343 default=[], 1344 help=( 1345 "Specify a LLDB command that will be executed when the debugging " 1346 "session is terminated. Can be specified more than once." 1347 ), 1348 ) 1349 1350 parser.add_option( 1351 "--env", 1352 type="string", 1353 action="append", 1354 dest="envs", 1355 default=[], 1356 help=("Specify environment variables to pass to the launched " "process."), 1357 ) 1358 1359 parser.add_option( 1360 "--waitFor", 1361 action="store_true", 1362 dest="waitFor", 1363 default=False, 1364 help=( 1365 "Wait for the next process to be launched whose name matches " 1366 "the basename of the program specified with the --program " 1367 "option" 1368 ), 1369 ) 1370 1371 (options, args) = parser.parse_args(sys.argv[1:]) 1372 1373 if options.vscode_path is None and options.port is None: 1374 print( 1375 "error: must either specify a path to a Visual Studio Code " 1376 "Debug Adaptor vscode executable path using the --vscode " 1377 "option, or a port to attach to for an existing lldb-dap " 1378 "using the --port option" 1379 ) 1380 return 1381 dbg = DebugAdaptorServer(executable=options.vscode_path, port=options.port) 1382 if options.debug: 1383 raw_input('Waiting for debugger to attach pid "%i"' % (dbg.get_pid())) 1384 if options.replay: 1385 dbg.replay_packets(options.replay) 1386 else: 1387 run_vscode(dbg, args, options) 1388 dbg.terminate() 1389 1390 1391if __name__ == "__main__": 1392 main() 1393