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