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