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