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