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