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