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