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