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