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