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 get_local_variable_child(self, name, child_name, frameIndex=0, threadId=None): 505 local = self.get_local_variable(name, frameIndex, threadId) 506 if local["variablesReference"] == 0: 507 return None 508 children = self.request_variables(local["variablesReference"])["body"][ 509 "variables" 510 ] 511 for child in children: 512 if child["name"] == child_name: 513 return child 514 return None 515 516 def replay_packets(self, replay_file_path): 517 f = open(replay_file_path, "r") 518 mode = "invalid" 519 set_sequence = False 520 command_dict = None 521 while mode != "eof": 522 if mode == "invalid": 523 line = f.readline() 524 if line.startswith("to adapter:"): 525 mode = "send" 526 elif line.startswith("from adapter:"): 527 mode = "recv" 528 elif mode == "send": 529 command_dict = read_packet(f) 530 # Skip the end of line that follows the JSON 531 f.readline() 532 if command_dict is None: 533 raise ValueError("decode packet failed from replay file") 534 print("Sending:") 535 pprint.PrettyPrinter(indent=2).pprint(command_dict) 536 # raw_input('Press ENTER to send:') 537 self.send_packet(command_dict, set_sequence) 538 mode = "invalid" 539 elif mode == "recv": 540 print("Replay response:") 541 replay_response = read_packet(f) 542 # Skip the end of line that follows the JSON 543 f.readline() 544 pprint.PrettyPrinter(indent=2).pprint(replay_response) 545 actual_response = self.recv_packet() 546 if actual_response: 547 type = actual_response["type"] 548 print("Actual response:") 549 if type == "response": 550 self.validate_response(command_dict, actual_response) 551 pprint.PrettyPrinter(indent=2).pprint(actual_response) 552 else: 553 print("error: didn't get a valid response") 554 mode = "invalid" 555 556 def request_attach( 557 self, 558 program=None, 559 pid=None, 560 waitFor=None, 561 trace=None, 562 initCommands=None, 563 preRunCommands=None, 564 stopCommands=None, 565 exitCommands=None, 566 attachCommands=None, 567 terminateCommands=None, 568 coreFile=None, 569 postRunCommands=None, 570 sourceMap=None, 571 ): 572 args_dict = {} 573 if pid is not None: 574 args_dict["pid"] = pid 575 if program is not None: 576 args_dict["program"] = program 577 if waitFor is not None: 578 args_dict["waitFor"] = waitFor 579 if trace: 580 args_dict["trace"] = trace 581 args_dict["initCommands"] = self.init_commands 582 if initCommands: 583 args_dict["initCommands"].extend(initCommands) 584 if preRunCommands: 585 args_dict["preRunCommands"] = preRunCommands 586 if stopCommands: 587 args_dict["stopCommands"] = stopCommands 588 if exitCommands: 589 args_dict["exitCommands"] = exitCommands 590 if terminateCommands: 591 args_dict["terminateCommands"] = terminateCommands 592 if attachCommands: 593 args_dict["attachCommands"] = attachCommands 594 if coreFile: 595 args_dict["coreFile"] = coreFile 596 if postRunCommands: 597 args_dict["postRunCommands"] = postRunCommands 598 if sourceMap: 599 args_dict["sourceMap"] = sourceMap 600 command_dict = {"command": "attach", "type": "request", "arguments": args_dict} 601 return self.send_recv(command_dict) 602 603 def request_configurationDone(self): 604 command_dict = { 605 "command": "configurationDone", 606 "type": "request", 607 "arguments": {}, 608 } 609 response = self.send_recv(command_dict) 610 if response: 611 self.configuration_done_sent = True 612 return response 613 614 def _process_stopped(self): 615 self.threads = None 616 self.frame_scopes = {} 617 618 def request_continue(self, threadId=None): 619 if self.exit_status is not None: 620 raise ValueError("request_continue called after process exited") 621 # If we have launched or attached, then the first continue is done by 622 # sending the 'configurationDone' request 623 if not self.configuration_done_sent: 624 return self.request_configurationDone() 625 args_dict = {} 626 if threadId is None: 627 threadId = self.get_thread_id() 628 args_dict["threadId"] = threadId 629 command_dict = { 630 "command": "continue", 631 "type": "request", 632 "arguments": args_dict, 633 } 634 response = self.send_recv(command_dict) 635 # Caller must still call wait_for_stopped. 636 return response 637 638 def request_restart(self, restartArguments=None): 639 command_dict = { 640 "command": "restart", 641 "type": "request", 642 } 643 if restartArguments: 644 command_dict["arguments"] = restartArguments 645 646 response = self.send_recv(command_dict) 647 # Caller must still call wait_for_stopped. 648 return response 649 650 def request_disconnect(self, terminateDebuggee=None): 651 args_dict = {} 652 if terminateDebuggee is not None: 653 if terminateDebuggee: 654 args_dict["terminateDebuggee"] = True 655 else: 656 args_dict["terminateDebuggee"] = False 657 command_dict = { 658 "command": "disconnect", 659 "type": "request", 660 "arguments": args_dict, 661 } 662 return self.send_recv(command_dict) 663 664 def request_disassemble( 665 self, memoryReference, offset=-50, instructionCount=200, resolveSymbols=True 666 ): 667 args_dict = { 668 "memoryReference": memoryReference, 669 "offset": offset, 670 "instructionCount": instructionCount, 671 "resolveSymbols": resolveSymbols, 672 } 673 command_dict = { 674 "command": "disassemble", 675 "type": "request", 676 "arguments": args_dict, 677 } 678 instructions = self.send_recv(command_dict)["body"]["instructions"] 679 for inst in instructions: 680 self.disassembled_instructions[inst["address"]] = inst 681 682 def request_evaluate(self, expression, frameIndex=0, threadId=None, context=None): 683 stackFrame = self.get_stackFrame(frameIndex=frameIndex, threadId=threadId) 684 if stackFrame is None: 685 return [] 686 args_dict = { 687 "expression": expression, 688 "context": context, 689 "frameId": stackFrame["id"], 690 } 691 command_dict = { 692 "command": "evaluate", 693 "type": "request", 694 "arguments": args_dict, 695 } 696 return self.send_recv(command_dict) 697 698 def request_initialize(self, sourceInitFile): 699 command_dict = { 700 "command": "initialize", 701 "type": "request", 702 "arguments": { 703 "adapterID": "lldb-native", 704 "clientID": "vscode", 705 "columnsStartAt1": True, 706 "linesStartAt1": True, 707 "locale": "en-us", 708 "pathFormat": "path", 709 "supportsRunInTerminalRequest": True, 710 "supportsVariablePaging": True, 711 "supportsVariableType": True, 712 "supportsStartDebuggingRequest": True, 713 "sourceInitFile": sourceInitFile, 714 }, 715 } 716 response = self.send_recv(command_dict) 717 if response: 718 if "body" in response: 719 self.initialize_body = response["body"] 720 return response 721 722 def request_launch( 723 self, 724 program, 725 args=None, 726 cwd=None, 727 env=None, 728 stopOnEntry=False, 729 disableASLR=True, 730 disableSTDIO=False, 731 shellExpandArguments=False, 732 trace=False, 733 initCommands=None, 734 preRunCommands=None, 735 stopCommands=None, 736 exitCommands=None, 737 terminateCommands=None, 738 sourcePath=None, 739 debuggerRoot=None, 740 launchCommands=None, 741 sourceMap=None, 742 runInTerminal=False, 743 postRunCommands=None, 744 enableAutoVariableSummaries=False, 745 enableSyntheticChildDebugging=False, 746 commandEscapePrefix=None, 747 customFrameFormat=None, 748 customThreadFormat=None, 749 ): 750 args_dict = {"program": program} 751 if args: 752 args_dict["args"] = args 753 if cwd: 754 args_dict["cwd"] = cwd 755 if env: 756 args_dict["env"] = env 757 if stopOnEntry: 758 args_dict["stopOnEntry"] = stopOnEntry 759 if disableASLR: 760 args_dict["disableASLR"] = disableASLR 761 if disableSTDIO: 762 args_dict["disableSTDIO"] = disableSTDIO 763 if shellExpandArguments: 764 args_dict["shellExpandArguments"] = shellExpandArguments 765 if trace: 766 args_dict["trace"] = trace 767 args_dict["initCommands"] = self.init_commands 768 if initCommands: 769 args_dict["initCommands"].extend(initCommands) 770 if preRunCommands: 771 args_dict["preRunCommands"] = preRunCommands 772 if stopCommands: 773 args_dict["stopCommands"] = stopCommands 774 if exitCommands: 775 args_dict["exitCommands"] = exitCommands 776 if terminateCommands: 777 args_dict["terminateCommands"] = terminateCommands 778 if sourcePath: 779 args_dict["sourcePath"] = sourcePath 780 if debuggerRoot: 781 args_dict["debuggerRoot"] = debuggerRoot 782 if launchCommands: 783 args_dict["launchCommands"] = launchCommands 784 if sourceMap: 785 args_dict["sourceMap"] = sourceMap 786 if runInTerminal: 787 args_dict["runInTerminal"] = runInTerminal 788 if postRunCommands: 789 args_dict["postRunCommands"] = postRunCommands 790 if customFrameFormat: 791 args_dict["customFrameFormat"] = customFrameFormat 792 if customThreadFormat: 793 args_dict["customThreadFormat"] = customThreadFormat 794 795 args_dict["enableAutoVariableSummaries"] = enableAutoVariableSummaries 796 args_dict["enableSyntheticChildDebugging"] = enableSyntheticChildDebugging 797 args_dict["commandEscapePrefix"] = commandEscapePrefix 798 command_dict = {"command": "launch", "type": "request", "arguments": args_dict} 799 response = self.send_recv(command_dict) 800 801 if response["success"]: 802 # Wait for a 'process' and 'initialized' event in any order 803 self.wait_for_event(filter=["process", "initialized"]) 804 self.wait_for_event(filter=["process", "initialized"]) 805 return response 806 807 def request_next(self, threadId): 808 if self.exit_status is not None: 809 raise ValueError("request_continue called after process exited") 810 args_dict = {"threadId": threadId} 811 command_dict = {"command": "next", "type": "request", "arguments": args_dict} 812 return self.send_recv(command_dict) 813 814 def request_stepIn(self, threadId): 815 if self.exit_status is not None: 816 raise ValueError("request_continue called after process exited") 817 args_dict = {"threadId": threadId} 818 command_dict = {"command": "stepIn", "type": "request", "arguments": args_dict} 819 return self.send_recv(command_dict) 820 821 def request_stepOut(self, threadId): 822 if self.exit_status is not None: 823 raise ValueError("request_continue called after process exited") 824 args_dict = {"threadId": threadId} 825 command_dict = {"command": "stepOut", "type": "request", "arguments": args_dict} 826 return self.send_recv(command_dict) 827 828 def request_pause(self, threadId=None): 829 if self.exit_status is not None: 830 raise ValueError("request_continue called after process exited") 831 if threadId is None: 832 threadId = self.get_thread_id() 833 args_dict = {"threadId": threadId} 834 command_dict = {"command": "pause", "type": "request", "arguments": args_dict} 835 return self.send_recv(command_dict) 836 837 def request_scopes(self, frameId): 838 args_dict = {"frameId": frameId} 839 command_dict = {"command": "scopes", "type": "request", "arguments": args_dict} 840 return self.send_recv(command_dict) 841 842 def request_setBreakpoints(self, file_path, line_array, data=None): 843 """data is array of parameters for breakpoints in line_array. 844 Each parameter object is 1:1 mapping with entries in line_entry. 845 It contains optional location/hitCondition/logMessage parameters. 846 """ 847 (dir, base) = os.path.split(file_path) 848 source_dict = {"name": base, "path": file_path} 849 args_dict = { 850 "source": source_dict, 851 "sourceModified": False, 852 } 853 if line_array is not None: 854 args_dict["lines"] = "%s" % line_array 855 breakpoints = [] 856 for i, line in enumerate(line_array): 857 breakpoint_data = None 858 if data is not None and i < len(data): 859 breakpoint_data = data[i] 860 bp = {"line": line} 861 if breakpoint_data is not None: 862 if "condition" in breakpoint_data and breakpoint_data["condition"]: 863 bp["condition"] = breakpoint_data["condition"] 864 if ( 865 "hitCondition" in breakpoint_data 866 and breakpoint_data["hitCondition"] 867 ): 868 bp["hitCondition"] = breakpoint_data["hitCondition"] 869 if ( 870 "logMessage" in breakpoint_data 871 and breakpoint_data["logMessage"] 872 ): 873 bp["logMessage"] = breakpoint_data["logMessage"] 874 breakpoints.append(bp) 875 args_dict["breakpoints"] = breakpoints 876 877 command_dict = { 878 "command": "setBreakpoints", 879 "type": "request", 880 "arguments": args_dict, 881 } 882 return self.send_recv(command_dict) 883 884 def request_setExceptionBreakpoints(self, filters): 885 args_dict = {"filters": filters} 886 command_dict = { 887 "command": "setExceptionBreakpoints", 888 "type": "request", 889 "arguments": args_dict, 890 } 891 return self.send_recv(command_dict) 892 893 def request_setFunctionBreakpoints(self, names, condition=None, hitCondition=None): 894 breakpoints = [] 895 for name in names: 896 bp = {"name": name} 897 if condition is not None: 898 bp["condition"] = condition 899 if hitCondition is not None: 900 bp["hitCondition"] = hitCondition 901 breakpoints.append(bp) 902 args_dict = {"breakpoints": breakpoints} 903 command_dict = { 904 "command": "setFunctionBreakpoints", 905 "type": "request", 906 "arguments": args_dict, 907 } 908 return self.send_recv(command_dict) 909 910 def request_dataBreakpointInfo( 911 self, variablesReference, name, frameIndex=0, threadId=None 912 ): 913 stackFrame = self.get_stackFrame(frameIndex=frameIndex, threadId=threadId) 914 if stackFrame is None: 915 return [] 916 args_dict = { 917 "variablesReference": variablesReference, 918 "name": name, 919 "frameId": stackFrame["id"], 920 } 921 command_dict = { 922 "command": "dataBreakpointInfo", 923 "type": "request", 924 "arguments": args_dict, 925 } 926 return self.send_recv(command_dict) 927 928 def request_setDataBreakpoint(self, dataBreakpoints): 929 """dataBreakpoints is a list of dictionary with following fields: 930 { 931 dataId: (address in hex)/(size in bytes) 932 accessType: read/write/readWrite 933 [condition]: string 934 [hitCondition]: string 935 } 936 """ 937 args_dict = {"breakpoints": dataBreakpoints} 938 command_dict = { 939 "command": "setDataBreakpoints", 940 "type": "request", 941 "arguments": args_dict, 942 } 943 return self.send_recv(command_dict) 944 945 def request_compileUnits(self, moduleId): 946 args_dict = {"moduleId": moduleId} 947 command_dict = { 948 "command": "compileUnits", 949 "type": "request", 950 "arguments": args_dict, 951 } 952 response = self.send_recv(command_dict) 953 return response 954 955 def request_completions(self, text, frameId=None): 956 args_dict = {"text": text, "column": len(text)} 957 if frameId: 958 args_dict["frameId"] = frameId 959 command_dict = { 960 "command": "completions", 961 "type": "request", 962 "arguments": args_dict, 963 } 964 return self.send_recv(command_dict) 965 966 def request_modules(self): 967 return self.send_recv({"command": "modules", "type": "request"}) 968 969 def request_stackTrace( 970 self, threadId=None, startFrame=None, levels=None, dump=False 971 ): 972 if threadId is None: 973 threadId = self.get_thread_id() 974 args_dict = {"threadId": threadId} 975 if startFrame is not None: 976 args_dict["startFrame"] = startFrame 977 if levels is not None: 978 args_dict["levels"] = levels 979 command_dict = { 980 "command": "stackTrace", 981 "type": "request", 982 "arguments": args_dict, 983 } 984 response = self.send_recv(command_dict) 985 if dump: 986 for idx, frame in enumerate(response["body"]["stackFrames"]): 987 name = frame["name"] 988 if "line" in frame and "source" in frame: 989 source = frame["source"] 990 if "sourceReference" not in source: 991 if "name" in source: 992 source_name = source["name"] 993 line = frame["line"] 994 print("[%3u] %s @ %s:%u" % (idx, name, source_name, line)) 995 continue 996 print("[%3u] %s" % (idx, name)) 997 return response 998 999 def request_threads(self): 1000 """Request a list of all threads and combine any information from any 1001 "stopped" events since those contain more information about why a 1002 thread actually stopped. Returns an array of thread dictionaries 1003 with information about all threads""" 1004 command_dict = {"command": "threads", "type": "request", "arguments": {}} 1005 response = self.send_recv(command_dict) 1006 body = response["body"] 1007 # Fill in "self.threads" correctly so that clients that call 1008 # self.get_threads() or self.get_thread_id(...) can get information 1009 # on threads when the process is stopped. 1010 if "threads" in body: 1011 self.threads = body["threads"] 1012 for thread in self.threads: 1013 # Copy the thread dictionary so we can add key/value pairs to 1014 # it without affecting the original info from the "threads" 1015 # command. 1016 tid = thread["id"] 1017 if tid in self.thread_stop_reasons: 1018 thread_stop_info = self.thread_stop_reasons[tid] 1019 copy_keys = ["reason", "description", "text"] 1020 for key in copy_keys: 1021 if key in thread_stop_info: 1022 thread[key] = thread_stop_info[key] 1023 else: 1024 self.threads = None 1025 return response 1026 1027 def request_variables(self, variablesReference, start=None, count=None): 1028 args_dict = {"variablesReference": variablesReference} 1029 if start is not None: 1030 args_dict["start"] = start 1031 if count is not None: 1032 args_dict["count"] = count 1033 command_dict = { 1034 "command": "variables", 1035 "type": "request", 1036 "arguments": args_dict, 1037 } 1038 return self.send_recv(command_dict) 1039 1040 def request_setVariable(self, containingVarRef, name, value, id=None): 1041 args_dict = { 1042 "variablesReference": containingVarRef, 1043 "name": name, 1044 "value": str(value), 1045 } 1046 if id is not None: 1047 args_dict["id"] = id 1048 command_dict = { 1049 "command": "setVariable", 1050 "type": "request", 1051 "arguments": args_dict, 1052 } 1053 return self.send_recv(command_dict) 1054 1055 def request_testGetTargetBreakpoints(self): 1056 """A request packet used in the LLDB test suite to get all currently 1057 set breakpoint infos for all breakpoints currently set in the 1058 target. 1059 """ 1060 command_dict = { 1061 "command": "_testGetTargetBreakpoints", 1062 "type": "request", 1063 "arguments": {}, 1064 } 1065 return self.send_recv(command_dict) 1066 1067 def terminate(self): 1068 self.send.close() 1069 # self.recv.close() 1070 1071 1072class DebugAdaptorServer(DebugCommunication): 1073 def __init__( 1074 self, 1075 executable=None, 1076 port=None, 1077 init_commands=[], 1078 log_file=None, 1079 env=None, 1080 ): 1081 self.process = None 1082 if executable is not None: 1083 adaptor_env = os.environ.copy() 1084 if env is not None: 1085 adaptor_env.update(env) 1086 1087 if log_file: 1088 adaptor_env["LLDBDAP_LOG"] = log_file 1089 self.process = subprocess.Popen( 1090 [executable], 1091 stdin=subprocess.PIPE, 1092 stdout=subprocess.PIPE, 1093 stderr=subprocess.PIPE, 1094 env=adaptor_env, 1095 ) 1096 DebugCommunication.__init__( 1097 self, self.process.stdout, self.process.stdin, init_commands, log_file 1098 ) 1099 elif port is not None: 1100 s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 1101 s.connect(("127.0.0.1", port)) 1102 DebugCommunication.__init__( 1103 self, s.makefile("r"), s.makefile("w"), init_commands 1104 ) 1105 1106 def get_pid(self): 1107 if self.process: 1108 return self.process.pid 1109 return -1 1110 1111 def terminate(self): 1112 super(DebugAdaptorServer, self).terminate() 1113 if self.process is not None: 1114 self.process.terminate() 1115 self.process.wait() 1116 self.process = None 1117 1118 1119def attach_options_specified(options): 1120 if options.pid is not None: 1121 return True 1122 if options.waitFor: 1123 return True 1124 if options.attach: 1125 return True 1126 if options.attachCmds: 1127 return True 1128 return False 1129 1130 1131def run_vscode(dbg, args, options): 1132 dbg.request_initialize(options.sourceInitFile) 1133 if attach_options_specified(options): 1134 response = dbg.request_attach( 1135 program=options.program, 1136 pid=options.pid, 1137 waitFor=options.waitFor, 1138 attachCommands=options.attachCmds, 1139 initCommands=options.initCmds, 1140 preRunCommands=options.preRunCmds, 1141 stopCommands=options.stopCmds, 1142 exitCommands=options.exitCmds, 1143 terminateCommands=options.terminateCmds, 1144 ) 1145 else: 1146 response = dbg.request_launch( 1147 options.program, 1148 args=args, 1149 env=options.envs, 1150 cwd=options.workingDir, 1151 debuggerRoot=options.debuggerRoot, 1152 sourcePath=options.sourcePath, 1153 initCommands=options.initCmds, 1154 preRunCommands=options.preRunCmds, 1155 stopCommands=options.stopCmds, 1156 exitCommands=options.exitCmds, 1157 terminateCommands=options.terminateCmds, 1158 ) 1159 1160 if response["success"]: 1161 if options.sourceBreakpoints: 1162 source_to_lines = {} 1163 for file_line in options.sourceBreakpoints: 1164 (path, line) = file_line.split(":") 1165 if len(path) == 0 or len(line) == 0: 1166 print('error: invalid source with line "%s"' % (file_line)) 1167 1168 else: 1169 if path in source_to_lines: 1170 source_to_lines[path].append(int(line)) 1171 else: 1172 source_to_lines[path] = [int(line)] 1173 for source in source_to_lines: 1174 dbg.request_setBreakpoints(source, source_to_lines[source]) 1175 if options.funcBreakpoints: 1176 dbg.request_setFunctionBreakpoints(options.funcBreakpoints) 1177 dbg.request_configurationDone() 1178 dbg.wait_for_stopped() 1179 else: 1180 if "message" in response: 1181 print(response["message"]) 1182 dbg.request_disconnect(terminateDebuggee=True) 1183 1184 1185def main(): 1186 parser = optparse.OptionParser( 1187 description=( 1188 "A testing framework for the Visual Studio Code Debug " "Adaptor protocol" 1189 ) 1190 ) 1191 1192 parser.add_option( 1193 "--vscode", 1194 type="string", 1195 dest="vscode_path", 1196 help=( 1197 "The path to the command line program that implements the " 1198 "Visual Studio Code Debug Adaptor protocol." 1199 ), 1200 default=None, 1201 ) 1202 1203 parser.add_option( 1204 "--program", 1205 type="string", 1206 dest="program", 1207 help="The path to the program to debug.", 1208 default=None, 1209 ) 1210 1211 parser.add_option( 1212 "--workingDir", 1213 type="string", 1214 dest="workingDir", 1215 default=None, 1216 help="Set the working directory for the process we launch.", 1217 ) 1218 1219 parser.add_option( 1220 "--sourcePath", 1221 type="string", 1222 dest="sourcePath", 1223 default=None, 1224 help=( 1225 "Set the relative source root for any debug info that has " 1226 "relative paths in it." 1227 ), 1228 ) 1229 1230 parser.add_option( 1231 "--debuggerRoot", 1232 type="string", 1233 dest="debuggerRoot", 1234 default=None, 1235 help=( 1236 "Set the working directory for lldb-dap for any object files " 1237 "with relative paths in the Mach-o debug map." 1238 ), 1239 ) 1240 1241 parser.add_option( 1242 "-r", 1243 "--replay", 1244 type="string", 1245 dest="replay", 1246 help=( 1247 "Specify a file containing a packet log to replay with the " 1248 "current Visual Studio Code Debug Adaptor executable." 1249 ), 1250 default=None, 1251 ) 1252 1253 parser.add_option( 1254 "-g", 1255 "--debug", 1256 action="store_true", 1257 dest="debug", 1258 default=False, 1259 help="Pause waiting for a debugger to attach to the debug adaptor", 1260 ) 1261 1262 parser.add_option( 1263 "--sourceInitFile", 1264 action="store_true", 1265 dest="sourceInitFile", 1266 default=False, 1267 help="Whether lldb-dap should source .lldbinit file or not", 1268 ) 1269 1270 parser.add_option( 1271 "--port", 1272 type="int", 1273 dest="port", 1274 help="Attach a socket to a port instead of using STDIN for VSCode", 1275 default=None, 1276 ) 1277 1278 parser.add_option( 1279 "--pid", 1280 type="int", 1281 dest="pid", 1282 help="The process ID to attach to", 1283 default=None, 1284 ) 1285 1286 parser.add_option( 1287 "--attach", 1288 action="store_true", 1289 dest="attach", 1290 default=False, 1291 help=( 1292 "Specify this option to attach to a process by name. The " 1293 "process name is the basename of the executable specified with " 1294 "the --program option." 1295 ), 1296 ) 1297 1298 parser.add_option( 1299 "-f", 1300 "--function-bp", 1301 type="string", 1302 action="append", 1303 dest="funcBreakpoints", 1304 help=( 1305 "Specify the name of a function to break at. " 1306 "Can be specified more than once." 1307 ), 1308 default=[], 1309 ) 1310 1311 parser.add_option( 1312 "-s", 1313 "--source-bp", 1314 type="string", 1315 action="append", 1316 dest="sourceBreakpoints", 1317 default=[], 1318 help=( 1319 "Specify source breakpoints to set in the format of " 1320 "<source>:<line>. " 1321 "Can be specified more than once." 1322 ), 1323 ) 1324 1325 parser.add_option( 1326 "--attachCommand", 1327 type="string", 1328 action="append", 1329 dest="attachCmds", 1330 default=[], 1331 help=( 1332 "Specify a LLDB command that will attach to a process. " 1333 "Can be specified more than once." 1334 ), 1335 ) 1336 1337 parser.add_option( 1338 "--initCommand", 1339 type="string", 1340 action="append", 1341 dest="initCmds", 1342 default=[], 1343 help=( 1344 "Specify a LLDB command that will be executed before the target " 1345 "is created. Can be specified more than once." 1346 ), 1347 ) 1348 1349 parser.add_option( 1350 "--preRunCommand", 1351 type="string", 1352 action="append", 1353 dest="preRunCmds", 1354 default=[], 1355 help=( 1356 "Specify a LLDB command that will be executed after the target " 1357 "has been created. Can be specified more than once." 1358 ), 1359 ) 1360 1361 parser.add_option( 1362 "--stopCommand", 1363 type="string", 1364 action="append", 1365 dest="stopCmds", 1366 default=[], 1367 help=( 1368 "Specify a LLDB command that will be executed each time the" 1369 "process stops. Can be specified more than once." 1370 ), 1371 ) 1372 1373 parser.add_option( 1374 "--exitCommand", 1375 type="string", 1376 action="append", 1377 dest="exitCmds", 1378 default=[], 1379 help=( 1380 "Specify a LLDB command that will be executed when the process " 1381 "exits. Can be specified more than once." 1382 ), 1383 ) 1384 1385 parser.add_option( 1386 "--terminateCommand", 1387 type="string", 1388 action="append", 1389 dest="terminateCmds", 1390 default=[], 1391 help=( 1392 "Specify a LLDB command that will be executed when the debugging " 1393 "session is terminated. Can be specified more than once." 1394 ), 1395 ) 1396 1397 parser.add_option( 1398 "--env", 1399 type="string", 1400 action="append", 1401 dest="envs", 1402 default=[], 1403 help=("Specify environment variables to pass to the launched " "process."), 1404 ) 1405 1406 parser.add_option( 1407 "--waitFor", 1408 action="store_true", 1409 dest="waitFor", 1410 default=False, 1411 help=( 1412 "Wait for the next process to be launched whose name matches " 1413 "the basename of the program specified with the --program " 1414 "option" 1415 ), 1416 ) 1417 1418 (options, args) = parser.parse_args(sys.argv[1:]) 1419 1420 if options.vscode_path is None and options.port is None: 1421 print( 1422 "error: must either specify a path to a Visual Studio Code " 1423 "Debug Adaptor vscode executable path using the --vscode " 1424 "option, or a port to attach to for an existing lldb-dap " 1425 "using the --port option" 1426 ) 1427 return 1428 dbg = DebugAdaptorServer(executable=options.vscode_path, port=options.port) 1429 if options.debug: 1430 raw_input('Waiting for debugger to attach pid "%i"' % (dbg.get_pid())) 1431 if options.replay: 1432 dbg.replay_packets(options.replay) 1433 else: 1434 run_vscode(dbg, args, options) 1435 dbg.terminate() 1436 1437 1438if __name__ == "__main__": 1439 main() 1440