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