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