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