1#!/usr/bin/env python 2 3import binascii 4import json 5import optparse 6import os 7import pprint 8import socket 9import string 10import subprocess 11import sys 12import threading 13import time 14 15 16def dump_memory(base_addr, data, num_per_line, outfile): 17 data_len = len(data) 18 hex_string = binascii.hexlify(data) 19 addr = base_addr 20 ascii_str = "" 21 i = 0 22 while i < data_len: 23 outfile.write("0x%8.8x: " % (addr + i)) 24 bytes_left = data_len - i 25 if bytes_left >= num_per_line: 26 curr_data_len = num_per_line 27 else: 28 curr_data_len = bytes_left 29 hex_start_idx = i * 2 30 hex_end_idx = hex_start_idx + curr_data_len * 2 31 curr_hex_str = hex_string[hex_start_idx:hex_end_idx] 32 # 'curr_hex_str' now contains the hex byte string for the 33 # current line with no spaces between bytes 34 t = iter(curr_hex_str) 35 # Print hex bytes separated by space 36 outfile.write(" ".join(a + b for a, b in zip(t, t))) 37 # Print two spaces 38 outfile.write(" ") 39 # Calculate ASCII string for bytes into 'ascii_str' 40 ascii_str = "" 41 for j in range(i, i + curr_data_len): 42 ch = data[j] 43 if ch in string.printable and ch not in string.whitespace: 44 ascii_str += "%c" % (ch) 45 else: 46 ascii_str += "." 47 # Print ASCII representation and newline 48 outfile.write(ascii_str) 49 i = i + curr_data_len 50 outfile.write("\n") 51 52 53def read_packet(f, verbose=False, trace_file=None): 54 """Decode a JSON packet that starts with the content length and is 55 followed by the JSON bytes from a file 'f'. Returns None on EOF. 56 """ 57 line = f.readline().decode("utf-8") 58 if len(line) == 0: 59 return None # EOF. 60 61 # Watch for line that starts with the prefix 62 prefix = "Content-Length: " 63 if line.startswith(prefix): 64 # Decode length of JSON bytes 65 if verbose: 66 print('content: "%s"' % (line)) 67 length = int(line[len(prefix) :]) 68 if verbose: 69 print('length: "%u"' % (length)) 70 # Skip empty line 71 line = f.readline() 72 if verbose: 73 print('empty: "%s"' % (line)) 74 # Read JSON bytes 75 json_str = f.read(length) 76 if verbose: 77 print('json: "%s"' % (json_str)) 78 if trace_file: 79 trace_file.write("from adaptor:\n%s\n" % (json_str)) 80 # Decode the JSON bytes into a python dictionary 81 return json.loads(json_str) 82 83 raise Exception("unexpected malformed message from lldb-dap: " + line) 84 85 86def packet_type_is(packet, packet_type): 87 return "type" in packet and packet["type"] == packet_type 88 89 90def dump_dap_log(log_file): 91 print("========= DEBUG ADAPTER PROTOCOL LOGS =========") 92 if log_file is None: 93 print("no log file available") 94 else: 95 with open(log_file, "r") as file: 96 print(file.read()) 97 print("========= END =========") 98 99 100def read_packet_thread(vs_comm, log_file): 101 done = False 102 try: 103 while not done: 104 packet = read_packet(vs_comm.recv, trace_file=vs_comm.trace_file) 105 # `packet` will be `None` on EOF. We want to pass it down to 106 # handle_recv_packet anyway so the main thread can handle unexpected 107 # termination of lldb-dap and stop waiting for new packets. 108 done = not vs_comm.handle_recv_packet(packet) 109 finally: 110 dump_dap_log(log_file) 111 112 113class DebugCommunication(object): 114 def __init__(self, recv, send, init_commands, log_file=None): 115 self.trace_file = None 116 self.send = send 117 self.recv = recv 118 self.recv_packets = [] 119 self.recv_condition = threading.Condition() 120 self.recv_thread = threading.Thread( 121 target=read_packet_thread, args=(self, log_file) 122 ) 123 self.process_event_body = None 124 self.exit_status = None 125 self.initialize_body = None 126 self.thread_stop_reasons = {} 127 self.breakpoint_events = [] 128 self.progress_events = [] 129 self.reverse_requests = [] 130 self.sequence = 1 131 self.threads = None 132 self.recv_thread.start() 133 self.output_condition = threading.Condition() 134 self.output = {} 135 self.configuration_done_sent = False 136 self.frame_scopes = {} 137 self.init_commands = init_commands 138 self.disassembled_instructions = {} 139 140 @classmethod 141 def encode_content(cls, s): 142 return ("Content-Length: %u\r\n\r\n%s" % (len(s), s)).encode("utf-8") 143 144 @classmethod 145 def validate_response(cls, command, response): 146 if command["command"] != response["command"]: 147 raise ValueError("command mismatch in response") 148 if command["seq"] != response["request_seq"]: 149 raise ValueError("seq mismatch in response") 150 151 def get_modules(self): 152 module_list = self.request_modules()["body"]["modules"] 153 modules = {} 154 for module in module_list: 155 modules[module["name"]] = module 156 return modules 157 158 def get_output(self, category, timeout=0.0, clear=True): 159 self.output_condition.acquire() 160 output = None 161 if category in self.output: 162 output = self.output[category] 163 if clear: 164 del self.output[category] 165 elif timeout != 0.0: 166 self.output_condition.wait(timeout) 167 if category in self.output: 168 output = self.output[category] 169 if clear: 170 del self.output[category] 171 self.output_condition.release() 172 return output 173 174 def collect_output(self, category, duration, clear=True): 175 end_time = time.time() + duration 176 collected_output = "" 177 while end_time > time.time(): 178 output = self.get_output(category, timeout=0.25, clear=clear) 179 if output: 180 collected_output += output 181 return collected_output if collected_output else None 182 183 def enqueue_recv_packet(self, packet): 184 self.recv_condition.acquire() 185 self.recv_packets.append(packet) 186 self.recv_condition.notify() 187 self.recv_condition.release() 188 189 def handle_recv_packet(self, packet): 190 """Called by the read thread that is waiting for all incoming packets 191 to store the incoming packet in "self.recv_packets" in a thread safe 192 way. This function will then signal the "self.recv_condition" to 193 indicate a new packet is available. Returns True if the caller 194 should keep calling this function for more packets. 195 """ 196 # If EOF, notify the read thread by enqueuing a None. 197 if not packet: 198 self.enqueue_recv_packet(None) 199 return False 200 201 # Check the packet to see if is an event packet 202 keepGoing = True 203 packet_type = packet["type"] 204 if packet_type == "event": 205 event = packet["event"] 206 body = None 207 if "body" in packet: 208 body = packet["body"] 209 # Handle the event packet and cache information from these packets 210 # as they come in 211 if event == "output": 212 # Store any output we receive so clients can retrieve it later. 213 category = body["category"] 214 output = body["output"] 215 self.output_condition.acquire() 216 if category in self.output: 217 self.output[category] += output 218 else: 219 self.output[category] = output 220 self.output_condition.notify() 221 self.output_condition.release() 222 # no need to add 'output' event packets to our packets list 223 return keepGoing 224 elif event == "process": 225 # When a new process is attached or launched, remember the 226 # details that are available in the body of the event 227 self.process_event_body = body 228 elif event == "stopped": 229 # Each thread that stops with a reason will send a 230 # 'stopped' event. We need to remember the thread stop 231 # reasons since the 'threads' command doesn't return 232 # that information. 233 self._process_stopped() 234 tid = body["threadId"] 235 self.thread_stop_reasons[tid] = body 236 elif event == "breakpoint": 237 # Breakpoint events come in when a breakpoint has locations 238 # added or removed. Keep track of them so we can look for them 239 # in tests. 240 self.breakpoint_events.append(packet) 241 # no need to add 'breakpoint' event packets to our packets list 242 return keepGoing 243 elif event.startswith("progress"): 244 # Progress events come in as 'progressStart', 'progressUpdate', 245 # and 'progressEnd' events. Keep these around in case test 246 # cases want to verify them. 247 self.progress_events.append(packet) 248 # No need to add 'progress' event packets to our packets list. 249 return keepGoing 250 251 elif packet_type == "response": 252 if packet["command"] == "disconnect": 253 keepGoing = False 254 self.enqueue_recv_packet(packet) 255 return keepGoing 256 257 def send_packet(self, command_dict, set_sequence=True): 258 """Take the "command_dict" python dictionary and encode it as a JSON 259 string and send the contents as a packet to the VSCode debug 260 adaptor""" 261 # Set the sequence ID for this command automatically 262 if set_sequence: 263 command_dict["seq"] = self.sequence 264 self.sequence += 1 265 # Encode our command dictionary as a JSON string 266 json_str = json.dumps(command_dict, separators=(",", ":")) 267 if self.trace_file: 268 self.trace_file.write("to adaptor:\n%s\n" % (json_str)) 269 length = len(json_str) 270 if length > 0: 271 # Send the encoded JSON packet and flush the 'send' file 272 self.send.write(self.encode_content(json_str)) 273 self.send.flush() 274 275 def recv_packet(self, filter_type=None, filter_event=None, timeout=None): 276 """Get a JSON packet from the VSCode debug adaptor. This function 277 assumes a thread that reads packets is running and will deliver 278 any received packets by calling handle_recv_packet(...). This 279 function will wait for the packet to arrive and return it when 280 it does.""" 281 while True: 282 try: 283 self.recv_condition.acquire() 284 packet = None 285 while True: 286 for i, curr_packet in enumerate(self.recv_packets): 287 if not curr_packet: 288 raise EOFError 289 packet_type = curr_packet["type"] 290 if filter_type is None or packet_type in filter_type: 291 if filter_event is None or ( 292 packet_type == "event" 293 and curr_packet["event"] in filter_event 294 ): 295 packet = self.recv_packets.pop(i) 296 break 297 if packet: 298 break 299 # Sleep until packet is received 300 len_before = len(self.recv_packets) 301 self.recv_condition.wait(timeout) 302 len_after = len(self.recv_packets) 303 if len_before == len_after: 304 return None # Timed out 305 return packet 306 except EOFError: 307 return None 308 finally: 309 self.recv_condition.release() 310 311 return None 312 313 def send_recv(self, command): 314 """Send a command python dictionary as JSON and receive the JSON 315 response. Validates that the response is the correct sequence and 316 command in the reply. Any events that are received are added to the 317 events list in this object""" 318 self.send_packet(command) 319 done = False 320 while not done: 321 response_or_request = self.recv_packet(filter_type=["response", "request"]) 322 if response_or_request is None: 323 desc = 'no response for "%s"' % (command["command"]) 324 raise ValueError(desc) 325 if response_or_request["type"] == "response": 326 self.validate_response(command, response_or_request) 327 return response_or_request 328 else: 329 self.reverse_requests.append(response_or_request) 330 if response_or_request["command"] == "runInTerminal": 331 subprocess.Popen( 332 response_or_request["arguments"]["args"], 333 env=response_or_request["arguments"]["env"], 334 ) 335 self.send_packet( 336 { 337 "type": "response", 338 "seq": -1, 339 "request_seq": response_or_request["seq"], 340 "success": True, 341 "command": "runInTerminal", 342 "body": {}, 343 }, 344 set_sequence=False, 345 ) 346 elif response_or_request["command"] == "startDebugging": 347 self.send_packet( 348 { 349 "type": "response", 350 "seq": -1, 351 "request_seq": response_or_request["seq"], 352 "success": True, 353 "command": "startDebugging", 354 "body": {}, 355 }, 356 set_sequence=False, 357 ) 358 else: 359 desc = 'unknown reverse request "%s"' % ( 360 response_or_request["command"] 361 ) 362 raise ValueError(desc) 363 364 return None 365 366 def wait_for_event(self, filter=None, timeout=None): 367 while True: 368 return self.recv_packet( 369 filter_type="event", filter_event=filter, timeout=timeout 370 ) 371 return None 372 373 def wait_for_stopped(self, timeout=None): 374 stopped_events = [] 375 stopped_event = self.wait_for_event( 376 filter=["stopped", "exited"], timeout=timeout 377 ) 378 exited = False 379 while stopped_event: 380 stopped_events.append(stopped_event) 381 # If we exited, then we are done 382 if stopped_event["event"] == "exited": 383 self.exit_status = stopped_event["body"]["exitCode"] 384 exited = True 385 break 386 # Otherwise we stopped and there might be one or more 'stopped' 387 # events for each thread that stopped with a reason, so keep 388 # checking for more 'stopped' events and return all of them 389 stopped_event = self.wait_for_event(filter="stopped", timeout=0.25) 390 if exited: 391 self.threads = [] 392 return stopped_events 393 394 def wait_for_exited(self): 395 event_dict = self.wait_for_event("exited") 396 if event_dict is None: 397 raise ValueError("didn't get exited event") 398 return event_dict 399 400 def wait_for_terminated(self): 401 event_dict = self.wait_for_event("terminated") 402 if event_dict is None: 403 raise ValueError("didn't get terminated event") 404 return event_dict 405 406 def get_initialize_value(self, key): 407 """Get a value for the given key if it there is a key/value pair in 408 the "initialize" request response body. 409 """ 410 if self.initialize_body and key in self.initialize_body: 411 return self.initialize_body[key] 412 return None 413 414 def get_threads(self): 415 if self.threads is None: 416 self.request_threads() 417 return self.threads 418 419 def get_thread_id(self, threadIndex=0): 420 """Utility function to get the first thread ID in the thread list. 421 If the thread list is empty, then fetch the threads. 422 """ 423 if self.threads is None: 424 self.request_threads() 425 if self.threads and threadIndex < len(self.threads): 426 return self.threads[threadIndex]["id"] 427 return None 428 429 def get_stackFrame(self, frameIndex=0, threadId=None): 430 """Get a single "StackFrame" object from a "stackTrace" request and 431 return the "StackFrame" as a python dictionary, or None on failure 432 """ 433 if threadId is None: 434 threadId = self.get_thread_id() 435 if threadId is None: 436 print("invalid threadId") 437 return None 438 response = self.request_stackTrace(threadId, startFrame=frameIndex, levels=1) 439 if response: 440 return response["body"]["stackFrames"][0] 441 print("invalid response") 442 return None 443 444 def get_completions(self, text, frameId=None): 445 if frameId is None: 446 stackFrame = self.get_stackFrame() 447 frameId = stackFrame["id"] 448 response = self.request_completions(text, frameId) 449 return response["body"]["targets"] 450 451 def get_scope_variables(self, scope_name, frameIndex=0, threadId=None): 452 stackFrame = self.get_stackFrame(frameIndex=frameIndex, threadId=threadId) 453 if stackFrame is None: 454 return [] 455 frameId = stackFrame["id"] 456 if frameId in self.frame_scopes: 457 frame_scopes = self.frame_scopes[frameId] 458 else: 459 scopes_response = self.request_scopes(frameId) 460 frame_scopes = scopes_response["body"]["scopes"] 461 self.frame_scopes[frameId] = frame_scopes 462 for scope in frame_scopes: 463 if scope["name"] == scope_name: 464 varRef = scope["variablesReference"] 465 variables_response = self.request_variables(varRef) 466 if variables_response: 467 if "body" in variables_response: 468 body = variables_response["body"] 469 if "variables" in body: 470 vars = body["variables"] 471 return vars 472 return [] 473 474 def get_global_variables(self, frameIndex=0, threadId=None): 475 return self.get_scope_variables( 476 "Globals", frameIndex=frameIndex, threadId=threadId 477 ) 478 479 def get_local_variables(self, frameIndex=0, threadId=None): 480 return self.get_scope_variables( 481 "Locals", frameIndex=frameIndex, threadId=threadId 482 ) 483 484 def get_registers(self, frameIndex=0, threadId=None): 485 return self.get_scope_variables( 486 "Registers", frameIndex=frameIndex, threadId=threadId 487 ) 488 489 def get_local_variable(self, name, frameIndex=0, threadId=None): 490 locals = self.get_local_variables(frameIndex=frameIndex, threadId=threadId) 491 for local in locals: 492 if "name" in local and local["name"] == name: 493 return local 494 return None 495 496 def get_local_variable_value(self, name, frameIndex=0, threadId=None): 497 variable = self.get_local_variable( 498 name, frameIndex=frameIndex, threadId=threadId 499 ) 500 if variable and "value" in variable: 501 return variable["value"] 502 return None 503 504 def replay_packets(self, replay_file_path): 505 f = open(replay_file_path, "r") 506 mode = "invalid" 507 set_sequence = False 508 command_dict = None 509 while mode != "eof": 510 if mode == "invalid": 511 line = f.readline() 512 if line.startswith("to adapter:"): 513 mode = "send" 514 elif line.startswith("from adapter:"): 515 mode = "recv" 516 elif mode == "send": 517 command_dict = read_packet(f) 518 # Skip the end of line that follows the JSON 519 f.readline() 520 if command_dict is None: 521 raise ValueError("decode packet failed from replay file") 522 print("Sending:") 523 pprint.PrettyPrinter(indent=2).pprint(command_dict) 524 # raw_input('Press ENTER to send:') 525 self.send_packet(command_dict, set_sequence) 526 mode = "invalid" 527 elif mode == "recv": 528 print("Replay response:") 529 replay_response = read_packet(f) 530 # Skip the end of line that follows the JSON 531 f.readline() 532 pprint.PrettyPrinter(indent=2).pprint(replay_response) 533 actual_response = self.recv_packet() 534 if actual_response: 535 type = actual_response["type"] 536 print("Actual response:") 537 if type == "response": 538 self.validate_response(command_dict, actual_response) 539 pprint.PrettyPrinter(indent=2).pprint(actual_response) 540 else: 541 print("error: didn't get a valid response") 542 mode = "invalid" 543 544 def request_attach( 545 self, 546 program=None, 547 pid=None, 548 waitFor=None, 549 trace=None, 550 initCommands=None, 551 preRunCommands=None, 552 stopCommands=None, 553 exitCommands=None, 554 attachCommands=None, 555 terminateCommands=None, 556 coreFile=None, 557 postRunCommands=None, 558 sourceMap=None, 559 ): 560 args_dict = {} 561 if pid is not None: 562 args_dict["pid"] = pid 563 if program is not None: 564 args_dict["program"] = program 565 if waitFor is not None: 566 args_dict["waitFor"] = waitFor 567 if trace: 568 args_dict["trace"] = trace 569 args_dict["initCommands"] = self.init_commands 570 if initCommands: 571 args_dict["initCommands"].extend(initCommands) 572 if preRunCommands: 573 args_dict["preRunCommands"] = preRunCommands 574 if stopCommands: 575 args_dict["stopCommands"] = stopCommands 576 if exitCommands: 577 args_dict["exitCommands"] = exitCommands 578 if terminateCommands: 579 args_dict["terminateCommands"] = terminateCommands 580 if attachCommands: 581 args_dict["attachCommands"] = attachCommands 582 if coreFile: 583 args_dict["coreFile"] = coreFile 584 if postRunCommands: 585 args_dict["postRunCommands"] = postRunCommands 586 if sourceMap: 587 args_dict["sourceMap"] = sourceMap 588 command_dict = {"command": "attach", "type": "request", "arguments": args_dict} 589 return self.send_recv(command_dict) 590 591 def request_configurationDone(self): 592 command_dict = { 593 "command": "configurationDone", 594 "type": "request", 595 "arguments": {}, 596 } 597 response = self.send_recv(command_dict) 598 if response: 599 self.configuration_done_sent = True 600 return response 601 602 def _process_stopped(self): 603 self.threads = None 604 self.frame_scopes = {} 605 606 def request_continue(self, threadId=None): 607 if self.exit_status is not None: 608 raise ValueError("request_continue called after process exited") 609 # If we have launched or attached, then the first continue is done by 610 # sending the 'configurationDone' request 611 if not self.configuration_done_sent: 612 return self.request_configurationDone() 613 args_dict = {} 614 if threadId is None: 615 threadId = self.get_thread_id() 616 args_dict["threadId"] = threadId 617 command_dict = { 618 "command": "continue", 619 "type": "request", 620 "arguments": args_dict, 621 } 622 response = self.send_recv(command_dict) 623 # Caller must still call wait_for_stopped. 624 return response 625 626 def request_restart(self, restartArguments=None): 627 command_dict = { 628 "command": "restart", 629 "type": "request", 630 } 631 if restartArguments: 632 command_dict["arguments"] = restartArguments 633 634 response = self.send_recv(command_dict) 635 # Caller must still call wait_for_stopped. 636 return response 637 638 def request_disconnect(self, terminateDebuggee=None): 639 args_dict = {} 640 if terminateDebuggee is not None: 641 if terminateDebuggee: 642 args_dict["terminateDebuggee"] = True 643 else: 644 args_dict["terminateDebuggee"] = False 645 command_dict = { 646 "command": "disconnect", 647 "type": "request", 648 "arguments": args_dict, 649 } 650 return self.send_recv(command_dict) 651 652 def request_disassemble( 653 self, memoryReference, offset=-50, instructionCount=200, resolveSymbols=True 654 ): 655 args_dict = { 656 "memoryReference": memoryReference, 657 "offset": offset, 658 "instructionCount": instructionCount, 659 "resolveSymbols": resolveSymbols, 660 } 661 command_dict = { 662 "command": "disassemble", 663 "type": "request", 664 "arguments": args_dict, 665 } 666 instructions = self.send_recv(command_dict)["body"]["instructions"] 667 for inst in instructions: 668 self.disassembled_instructions[inst["address"]] = inst 669 670 def request_evaluate(self, expression, frameIndex=0, threadId=None, context=None): 671 stackFrame = self.get_stackFrame(frameIndex=frameIndex, threadId=threadId) 672 if stackFrame is None: 673 return [] 674 args_dict = { 675 "expression": expression, 676 "context": context, 677 "frameId": stackFrame["id"], 678 } 679 command_dict = { 680 "command": "evaluate", 681 "type": "request", 682 "arguments": args_dict, 683 } 684 return self.send_recv(command_dict) 685 686 def request_initialize(self, sourceInitFile): 687 command_dict = { 688 "command": "initialize", 689 "type": "request", 690 "arguments": { 691 "adapterID": "lldb-native", 692 "clientID": "vscode", 693 "columnsStartAt1": True, 694 "linesStartAt1": True, 695 "locale": "en-us", 696 "pathFormat": "path", 697 "supportsRunInTerminalRequest": True, 698 "supportsVariablePaging": True, 699 "supportsVariableType": True, 700 "supportsStartDebuggingRequest": True, 701 "sourceInitFile": sourceInitFile, 702 }, 703 } 704 response = self.send_recv(command_dict) 705 if response: 706 if "body" in response: 707 self.initialize_body = response["body"] 708 return response 709 710 def request_launch( 711 self, 712 program, 713 args=None, 714 cwd=None, 715 env=None, 716 stopOnEntry=False, 717 disableASLR=True, 718 disableSTDIO=False, 719 shellExpandArguments=False, 720 trace=False, 721 initCommands=None, 722 preRunCommands=None, 723 stopCommands=None, 724 exitCommands=None, 725 terminateCommands=None, 726 sourcePath=None, 727 debuggerRoot=None, 728 launchCommands=None, 729 sourceMap=None, 730 runInTerminal=False, 731 postRunCommands=None, 732 enableAutoVariableSummaries=False, 733 enableSyntheticChildDebugging=False, 734 ): 735 args_dict = {"program": program} 736 if args: 737 args_dict["args"] = args 738 if cwd: 739 args_dict["cwd"] = cwd 740 if env: 741 args_dict["env"] = env 742 if stopOnEntry: 743 args_dict["stopOnEntry"] = stopOnEntry 744 if disableASLR: 745 args_dict["disableASLR"] = disableASLR 746 if disableSTDIO: 747 args_dict["disableSTDIO"] = disableSTDIO 748 if shellExpandArguments: 749 args_dict["shellExpandArguments"] = shellExpandArguments 750 if trace: 751 args_dict["trace"] = trace 752 args_dict["initCommands"] = self.init_commands 753 if initCommands: 754 args_dict["initCommands"].extend(initCommands) 755 if preRunCommands: 756 args_dict["preRunCommands"] = preRunCommands 757 if stopCommands: 758 args_dict["stopCommands"] = stopCommands 759 if exitCommands: 760 args_dict["exitCommands"] = exitCommands 761 if terminateCommands: 762 args_dict["terminateCommands"] = terminateCommands 763 if sourcePath: 764 args_dict["sourcePath"] = sourcePath 765 if debuggerRoot: 766 args_dict["debuggerRoot"] = debuggerRoot 767 if launchCommands: 768 args_dict["launchCommands"] = launchCommands 769 if sourceMap: 770 args_dict["sourceMap"] = sourceMap 771 if runInTerminal: 772 args_dict["runInTerminal"] = runInTerminal 773 if postRunCommands: 774 args_dict["postRunCommands"] = postRunCommands 775 args_dict["enableAutoVariableSummaries"] = enableAutoVariableSummaries 776 args_dict["enableSyntheticChildDebugging"] = enableSyntheticChildDebugging 777 command_dict = {"command": "launch", "type": "request", "arguments": args_dict} 778 response = self.send_recv(command_dict) 779 780 if response["success"]: 781 # Wait for a 'process' and 'initialized' event in any order 782 self.wait_for_event(filter=["process", "initialized"]) 783 self.wait_for_event(filter=["process", "initialized"]) 784 return response 785 786 def request_next(self, threadId): 787 if self.exit_status is not None: 788 raise ValueError("request_continue called after process exited") 789 args_dict = {"threadId": threadId} 790 command_dict = {"command": "next", "type": "request", "arguments": args_dict} 791 return self.send_recv(command_dict) 792 793 def request_stepIn(self, threadId): 794 if self.exit_status is not None: 795 raise ValueError("request_continue called after process exited") 796 args_dict = {"threadId": threadId} 797 command_dict = {"command": "stepIn", "type": "request", "arguments": args_dict} 798 return self.send_recv(command_dict) 799 800 def request_stepOut(self, threadId): 801 if self.exit_status is not None: 802 raise ValueError("request_continue called after process exited") 803 args_dict = {"threadId": threadId} 804 command_dict = {"command": "stepOut", "type": "request", "arguments": args_dict} 805 return self.send_recv(command_dict) 806 807 def request_pause(self, threadId=None): 808 if self.exit_status is not None: 809 raise ValueError("request_continue called after process exited") 810 if threadId is None: 811 threadId = self.get_thread_id() 812 args_dict = {"threadId": threadId} 813 command_dict = {"command": "pause", "type": "request", "arguments": args_dict} 814 return self.send_recv(command_dict) 815 816 def request_scopes(self, frameId): 817 args_dict = {"frameId": frameId} 818 command_dict = {"command": "scopes", "type": "request", "arguments": args_dict} 819 return self.send_recv(command_dict) 820 821 def request_setBreakpoints(self, file_path, line_array, data=None): 822 """data is array of parameters for breakpoints in line_array. 823 Each parameter object is 1:1 mapping with entries in line_entry. 824 It contains optional location/hitCondition/logMessage parameters. 825 """ 826 (dir, base) = os.path.split(file_path) 827 source_dict = {"name": base, "path": file_path} 828 args_dict = { 829 "source": source_dict, 830 "sourceModified": False, 831 } 832 if line_array is not None: 833 args_dict["lines"] = "%s" % line_array 834 breakpoints = [] 835 for i, line in enumerate(line_array): 836 breakpoint_data = None 837 if data is not None and i < len(data): 838 breakpoint_data = data[i] 839 bp = {"line": line} 840 if breakpoint_data is not None: 841 if "condition" in breakpoint_data and breakpoint_data["condition"]: 842 bp["condition"] = breakpoint_data["condition"] 843 if ( 844 "hitCondition" in breakpoint_data 845 and breakpoint_data["hitCondition"] 846 ): 847 bp["hitCondition"] = breakpoint_data["hitCondition"] 848 if ( 849 "logMessage" in breakpoint_data 850 and breakpoint_data["logMessage"] 851 ): 852 bp["logMessage"] = breakpoint_data["logMessage"] 853 breakpoints.append(bp) 854 args_dict["breakpoints"] = breakpoints 855 856 command_dict = { 857 "command": "setBreakpoints", 858 "type": "request", 859 "arguments": args_dict, 860 } 861 return self.send_recv(command_dict) 862 863 def request_setExceptionBreakpoints(self, filters): 864 args_dict = {"filters": filters} 865 command_dict = { 866 "command": "setExceptionBreakpoints", 867 "type": "request", 868 "arguments": args_dict, 869 } 870 return self.send_recv(command_dict) 871 872 def request_setFunctionBreakpoints(self, names, condition=None, hitCondition=None): 873 breakpoints = [] 874 for name in names: 875 bp = {"name": name} 876 if condition is not None: 877 bp["condition"] = condition 878 if hitCondition is not None: 879 bp["hitCondition"] = hitCondition 880 breakpoints.append(bp) 881 args_dict = {"breakpoints": breakpoints} 882 command_dict = { 883 "command": "setFunctionBreakpoints", 884 "type": "request", 885 "arguments": args_dict, 886 } 887 return self.send_recv(command_dict) 888 889 def request_compileUnits(self, moduleId): 890 args_dict = {"moduleId": moduleId} 891 command_dict = { 892 "command": "compileUnits", 893 "type": "request", 894 "arguments": args_dict, 895 } 896 response = self.send_recv(command_dict) 897 return response 898 899 def request_completions(self, text, frameId=None): 900 args_dict = {"text": text, "column": len(text)} 901 if frameId: 902 args_dict["frameId"] = frameId 903 command_dict = { 904 "command": "completions", 905 "type": "request", 906 "arguments": args_dict, 907 } 908 return self.send_recv(command_dict) 909 910 def request_modules(self): 911 return self.send_recv({"command": "modules", "type": "request"}) 912 913 def request_stackTrace( 914 self, threadId=None, startFrame=None, levels=None, dump=False 915 ): 916 if threadId is None: 917 threadId = self.get_thread_id() 918 args_dict = {"threadId": threadId} 919 if startFrame is not None: 920 args_dict["startFrame"] = startFrame 921 if levels is not None: 922 args_dict["levels"] = levels 923 command_dict = { 924 "command": "stackTrace", 925 "type": "request", 926 "arguments": args_dict, 927 } 928 response = self.send_recv(command_dict) 929 if dump: 930 for idx, frame in enumerate(response["body"]["stackFrames"]): 931 name = frame["name"] 932 if "line" in frame and "source" in frame: 933 source = frame["source"] 934 if "sourceReference" not in source: 935 if "name" in source: 936 source_name = source["name"] 937 line = frame["line"] 938 print("[%3u] %s @ %s:%u" % (idx, name, source_name, line)) 939 continue 940 print("[%3u] %s" % (idx, name)) 941 return response 942 943 def request_threads(self): 944 """Request a list of all threads and combine any information from any 945 "stopped" events since those contain more information about why a 946 thread actually stopped. Returns an array of thread dictionaries 947 with information about all threads""" 948 command_dict = {"command": "threads", "type": "request", "arguments": {}} 949 response = self.send_recv(command_dict) 950 body = response["body"] 951 # Fill in "self.threads" correctly so that clients that call 952 # self.get_threads() or self.get_thread_id(...) can get information 953 # on threads when the process is stopped. 954 if "threads" in body: 955 self.threads = body["threads"] 956 for thread in self.threads: 957 # Copy the thread dictionary so we can add key/value pairs to 958 # it without affecting the original info from the "threads" 959 # command. 960 tid = thread["id"] 961 if tid in self.thread_stop_reasons: 962 thread_stop_info = self.thread_stop_reasons[tid] 963 copy_keys = ["reason", "description", "text"] 964 for key in copy_keys: 965 if key in thread_stop_info: 966 thread[key] = thread_stop_info[key] 967 else: 968 self.threads = None 969 return response 970 971 def request_variables(self, variablesReference, start=None, count=None): 972 args_dict = {"variablesReference": variablesReference} 973 if start is not None: 974 args_dict["start"] = start 975 if count is not None: 976 args_dict["count"] = count 977 command_dict = { 978 "command": "variables", 979 "type": "request", 980 "arguments": args_dict, 981 } 982 return self.send_recv(command_dict) 983 984 def request_setVariable(self, containingVarRef, name, value, id=None): 985 args_dict = { 986 "variablesReference": containingVarRef, 987 "name": name, 988 "value": str(value), 989 } 990 if id is not None: 991 args_dict["id"] = id 992 command_dict = { 993 "command": "setVariable", 994 "type": "request", 995 "arguments": args_dict, 996 } 997 return self.send_recv(command_dict) 998 999 def request_testGetTargetBreakpoints(self): 1000 """A request packet used in the LLDB test suite to get all currently 1001 set breakpoint infos for all breakpoints currently set in the 1002 target. 1003 """ 1004 command_dict = { 1005 "command": "_testGetTargetBreakpoints", 1006 "type": "request", 1007 "arguments": {}, 1008 } 1009 return self.send_recv(command_dict) 1010 1011 def terminate(self): 1012 self.send.close() 1013 # self.recv.close() 1014 1015 1016class DebugAdaptorServer(DebugCommunication): 1017 def __init__( 1018 self, executable=None, port=None, init_commands=[], log_file=None, env=None 1019 ): 1020 self.process = None 1021 if executable is not None: 1022 adaptor_env = os.environ.copy() 1023 if env is not None: 1024 adaptor_env.update(env) 1025 1026 if log_file: 1027 adaptor_env["LLDBDAP_LOG"] = log_file 1028 self.process = subprocess.Popen( 1029 [executable], 1030 stdin=subprocess.PIPE, 1031 stdout=subprocess.PIPE, 1032 stderr=subprocess.PIPE, 1033 env=adaptor_env, 1034 ) 1035 DebugCommunication.__init__( 1036 self, self.process.stdout, self.process.stdin, init_commands, log_file 1037 ) 1038 elif port is not None: 1039 s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 1040 s.connect(("127.0.0.1", port)) 1041 DebugCommunication.__init__( 1042 self, s.makefile("r"), s.makefile("w"), init_commands 1043 ) 1044 1045 def get_pid(self): 1046 if self.process: 1047 return self.process.pid 1048 return -1 1049 1050 def terminate(self): 1051 super(DebugAdaptorServer, self).terminate() 1052 if self.process is not None: 1053 self.process.terminate() 1054 self.process.wait() 1055 self.process = None 1056 1057 1058def attach_options_specified(options): 1059 if options.pid is not None: 1060 return True 1061 if options.waitFor: 1062 return True 1063 if options.attach: 1064 return True 1065 if options.attachCmds: 1066 return True 1067 return False 1068 1069 1070def run_vscode(dbg, args, options): 1071 dbg.request_initialize(options.sourceInitFile) 1072 if attach_options_specified(options): 1073 response = dbg.request_attach( 1074 program=options.program, 1075 pid=options.pid, 1076 waitFor=options.waitFor, 1077 attachCommands=options.attachCmds, 1078 initCommands=options.initCmds, 1079 preRunCommands=options.preRunCmds, 1080 stopCommands=options.stopCmds, 1081 exitCommands=options.exitCmds, 1082 terminateCommands=options.terminateCmds, 1083 ) 1084 else: 1085 response = dbg.request_launch( 1086 options.program, 1087 args=args, 1088 env=options.envs, 1089 cwd=options.workingDir, 1090 debuggerRoot=options.debuggerRoot, 1091 sourcePath=options.sourcePath, 1092 initCommands=options.initCmds, 1093 preRunCommands=options.preRunCmds, 1094 stopCommands=options.stopCmds, 1095 exitCommands=options.exitCmds, 1096 terminateCommands=options.terminateCmds, 1097 ) 1098 1099 if response["success"]: 1100 if options.sourceBreakpoints: 1101 source_to_lines = {} 1102 for file_line in options.sourceBreakpoints: 1103 (path, line) = file_line.split(":") 1104 if len(path) == 0 or len(line) == 0: 1105 print('error: invalid source with line "%s"' % (file_line)) 1106 1107 else: 1108 if path in source_to_lines: 1109 source_to_lines[path].append(int(line)) 1110 else: 1111 source_to_lines[path] = [int(line)] 1112 for source in source_to_lines: 1113 dbg.request_setBreakpoints(source, source_to_lines[source]) 1114 if options.funcBreakpoints: 1115 dbg.request_setFunctionBreakpoints(options.funcBreakpoints) 1116 dbg.request_configurationDone() 1117 dbg.wait_for_stopped() 1118 else: 1119 if "message" in response: 1120 print(response["message"]) 1121 dbg.request_disconnect(terminateDebuggee=True) 1122 1123 1124def main(): 1125 parser = optparse.OptionParser( 1126 description=( 1127 "A testing framework for the Visual Studio Code Debug " "Adaptor protocol" 1128 ) 1129 ) 1130 1131 parser.add_option( 1132 "--vscode", 1133 type="string", 1134 dest="vscode_path", 1135 help=( 1136 "The path to the command line program that implements the " 1137 "Visual Studio Code Debug Adaptor protocol." 1138 ), 1139 default=None, 1140 ) 1141 1142 parser.add_option( 1143 "--program", 1144 type="string", 1145 dest="program", 1146 help="The path to the program to debug.", 1147 default=None, 1148 ) 1149 1150 parser.add_option( 1151 "--workingDir", 1152 type="string", 1153 dest="workingDir", 1154 default=None, 1155 help="Set the working directory for the process we launch.", 1156 ) 1157 1158 parser.add_option( 1159 "--sourcePath", 1160 type="string", 1161 dest="sourcePath", 1162 default=None, 1163 help=( 1164 "Set the relative source root for any debug info that has " 1165 "relative paths in it." 1166 ), 1167 ) 1168 1169 parser.add_option( 1170 "--debuggerRoot", 1171 type="string", 1172 dest="debuggerRoot", 1173 default=None, 1174 help=( 1175 "Set the working directory for lldb-dap for any object files " 1176 "with relative paths in the Mach-o debug map." 1177 ), 1178 ) 1179 1180 parser.add_option( 1181 "-r", 1182 "--replay", 1183 type="string", 1184 dest="replay", 1185 help=( 1186 "Specify a file containing a packet log to replay with the " 1187 "current Visual Studio Code Debug Adaptor executable." 1188 ), 1189 default=None, 1190 ) 1191 1192 parser.add_option( 1193 "-g", 1194 "--debug", 1195 action="store_true", 1196 dest="debug", 1197 default=False, 1198 help="Pause waiting for a debugger to attach to the debug adaptor", 1199 ) 1200 1201 parser.add_option( 1202 "--sourceInitFile", 1203 action="store_true", 1204 dest="sourceInitFile", 1205 default=False, 1206 help="Whether lldb-dap should source .lldbinit file or not", 1207 ) 1208 1209 parser.add_option( 1210 "--port", 1211 type="int", 1212 dest="port", 1213 help="Attach a socket to a port instead of using STDIN for VSCode", 1214 default=None, 1215 ) 1216 1217 parser.add_option( 1218 "--pid", 1219 type="int", 1220 dest="pid", 1221 help="The process ID to attach to", 1222 default=None, 1223 ) 1224 1225 parser.add_option( 1226 "--attach", 1227 action="store_true", 1228 dest="attach", 1229 default=False, 1230 help=( 1231 "Specify this option to attach to a process by name. The " 1232 "process name is the basename of the executable specified with " 1233 "the --program option." 1234 ), 1235 ) 1236 1237 parser.add_option( 1238 "-f", 1239 "--function-bp", 1240 type="string", 1241 action="append", 1242 dest="funcBreakpoints", 1243 help=( 1244 "Specify the name of a function to break at. " 1245 "Can be specified more than once." 1246 ), 1247 default=[], 1248 ) 1249 1250 parser.add_option( 1251 "-s", 1252 "--source-bp", 1253 type="string", 1254 action="append", 1255 dest="sourceBreakpoints", 1256 default=[], 1257 help=( 1258 "Specify source breakpoints to set in the format of " 1259 "<source>:<line>. " 1260 "Can be specified more than once." 1261 ), 1262 ) 1263 1264 parser.add_option( 1265 "--attachCommand", 1266 type="string", 1267 action="append", 1268 dest="attachCmds", 1269 default=[], 1270 help=( 1271 "Specify a LLDB command that will attach to a process. " 1272 "Can be specified more than once." 1273 ), 1274 ) 1275 1276 parser.add_option( 1277 "--initCommand", 1278 type="string", 1279 action="append", 1280 dest="initCmds", 1281 default=[], 1282 help=( 1283 "Specify a LLDB command that will be executed before the target " 1284 "is created. Can be specified more than once." 1285 ), 1286 ) 1287 1288 parser.add_option( 1289 "--preRunCommand", 1290 type="string", 1291 action="append", 1292 dest="preRunCmds", 1293 default=[], 1294 help=( 1295 "Specify a LLDB command that will be executed after the target " 1296 "has been created. Can be specified more than once." 1297 ), 1298 ) 1299 1300 parser.add_option( 1301 "--stopCommand", 1302 type="string", 1303 action="append", 1304 dest="stopCmds", 1305 default=[], 1306 help=( 1307 "Specify a LLDB command that will be executed each time the" 1308 "process stops. Can be specified more than once." 1309 ), 1310 ) 1311 1312 parser.add_option( 1313 "--exitCommand", 1314 type="string", 1315 action="append", 1316 dest="exitCmds", 1317 default=[], 1318 help=( 1319 "Specify a LLDB command that will be executed when the process " 1320 "exits. Can be specified more than once." 1321 ), 1322 ) 1323 1324 parser.add_option( 1325 "--terminateCommand", 1326 type="string", 1327 action="append", 1328 dest="terminateCmds", 1329 default=[], 1330 help=( 1331 "Specify a LLDB command that will be executed when the debugging " 1332 "session is terminated. Can be specified more than once." 1333 ), 1334 ) 1335 1336 parser.add_option( 1337 "--env", 1338 type="string", 1339 action="append", 1340 dest="envs", 1341 default=[], 1342 help=("Specify environment variables to pass to the launched " "process."), 1343 ) 1344 1345 parser.add_option( 1346 "--waitFor", 1347 action="store_true", 1348 dest="waitFor", 1349 default=False, 1350 help=( 1351 "Wait for the next process to be launched whose name matches " 1352 "the basename of the program specified with the --program " 1353 "option" 1354 ), 1355 ) 1356 1357 (options, args) = parser.parse_args(sys.argv[1:]) 1358 1359 if options.vscode_path is None and options.port is None: 1360 print( 1361 "error: must either specify a path to a Visual Studio Code " 1362 "Debug Adaptor vscode executable path using the --vscode " 1363 "option, or a port to attach to for an existing lldb-dap " 1364 "using the --port option" 1365 ) 1366 return 1367 dbg = DebugAdaptorServer(executable=options.vscode_path, port=options.port) 1368 if options.debug: 1369 raw_input('Waiting for debugger to attach pid "%i"' % (dbg.get_pid())) 1370 if options.replay: 1371 dbg.replay_packets(options.replay) 1372 else: 1373 run_vscode(dbg, args, options) 1374 dbg.terminate() 1375 1376 1377if __name__ == "__main__": 1378 main() 1379