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