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