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 sourceMap=None): 511 args_dict = {} 512 if pid is not None: 513 args_dict['pid'] = pid 514 if program is not None: 515 args_dict['program'] = program 516 if waitFor is not None: 517 args_dict['waitFor'] = waitFor 518 if trace: 519 args_dict['trace'] = trace 520 args_dict['initCommands'] = self.init_commands 521 if initCommands: 522 args_dict['initCommands'].extend(initCommands) 523 if preRunCommands: 524 args_dict['preRunCommands'] = preRunCommands 525 if stopCommands: 526 args_dict['stopCommands'] = stopCommands 527 if exitCommands: 528 args_dict['exitCommands'] = exitCommands 529 if terminateCommands: 530 args_dict['terminateCommands'] = terminateCommands 531 if attachCommands: 532 args_dict['attachCommands'] = attachCommands 533 if coreFile: 534 args_dict['coreFile'] = coreFile 535 if postRunCommands: 536 args_dict['postRunCommands'] = postRunCommands 537 if sourceMap: 538 args_dict['sourceMap'] = sourceMap 539 command_dict = { 540 'command': 'attach', 541 'type': 'request', 542 'arguments': args_dict 543 } 544 return self.send_recv(command_dict) 545 546 def request_configurationDone(self): 547 command_dict = { 548 'command': 'configurationDone', 549 'type': 'request', 550 'arguments': {} 551 } 552 response = self.send_recv(command_dict) 553 if response: 554 self.configuration_done_sent = True 555 return response 556 557 def _process_stopped(self): 558 self.threads = None 559 self.frame_scopes = {} 560 561 def request_continue(self, threadId=None): 562 if self.exit_status is not None: 563 raise ValueError('request_continue called after process exited') 564 # If we have launched or attached, then the first continue is done by 565 # sending the 'configurationDone' request 566 if not self.configuration_done_sent: 567 return self.request_configurationDone() 568 args_dict = {} 569 if threadId is None: 570 threadId = self.get_thread_id() 571 args_dict['threadId'] = threadId 572 command_dict = { 573 'command': 'continue', 574 'type': 'request', 575 'arguments': args_dict 576 } 577 response = self.send_recv(command_dict) 578 # Caller must still call wait_for_stopped. 579 return response 580 581 def request_disconnect(self, terminateDebuggee=None): 582 args_dict = {} 583 if terminateDebuggee is not None: 584 if terminateDebuggee: 585 args_dict['terminateDebuggee'] = True 586 else: 587 args_dict['terminateDebuggee'] = False 588 command_dict = { 589 'command': 'disconnect', 590 'type': 'request', 591 'arguments': args_dict 592 } 593 return self.send_recv(command_dict) 594 595 def request_evaluate(self, expression, frameIndex=0, threadId=None, context=None): 596 stackFrame = self.get_stackFrame(frameIndex=frameIndex, 597 threadId=threadId) 598 if stackFrame is None: 599 return [] 600 args_dict = { 601 'expression': expression, 602 'context': context, 603 'frameId': stackFrame['id'], 604 } 605 command_dict = { 606 'command': 'evaluate', 607 'type': 'request', 608 'arguments': args_dict 609 } 610 return self.send_recv(command_dict) 611 612 def request_initialize(self): 613 command_dict = { 614 'command': 'initialize', 615 'type': 'request', 616 'arguments': { 617 'adapterID': 'lldb-native', 618 'clientID': 'vscode', 619 'columnsStartAt1': True, 620 'linesStartAt1': True, 621 'locale': 'en-us', 622 'pathFormat': 'path', 623 'supportsRunInTerminalRequest': True, 624 'supportsVariablePaging': True, 625 'supportsVariableType': True 626 } 627 } 628 response = self.send_recv(command_dict) 629 if response: 630 if 'body' in response: 631 self.initialize_body = response['body'] 632 return response 633 634 def request_launch(self, program, args=None, cwd=None, env=None, 635 stopOnEntry=False, disableASLR=True, 636 disableSTDIO=False, shellExpandArguments=False, 637 trace=False, initCommands=None, preRunCommands=None, 638 stopCommands=None, exitCommands=None, 639 terminateCommands=None ,sourcePath=None, 640 debuggerRoot=None, launchCommands=None, sourceMap=None, 641 runInTerminal=False, expectFailure=False, 642 postRunCommands=None): 643 args_dict = { 644 'program': program 645 } 646 if args: 647 args_dict['args'] = args 648 if cwd: 649 args_dict['cwd'] = cwd 650 if env: 651 args_dict['env'] = env 652 if stopOnEntry: 653 args_dict['stopOnEntry'] = stopOnEntry 654 if disableASLR: 655 args_dict['disableASLR'] = disableASLR 656 if disableSTDIO: 657 args_dict['disableSTDIO'] = disableSTDIO 658 if shellExpandArguments: 659 args_dict['shellExpandArguments'] = shellExpandArguments 660 if trace: 661 args_dict['trace'] = trace 662 args_dict['initCommands'] = self.init_commands 663 if initCommands: 664 args_dict['initCommands'].extend(initCommands) 665 if preRunCommands: 666 args_dict['preRunCommands'] = preRunCommands 667 if stopCommands: 668 args_dict['stopCommands'] = stopCommands 669 if exitCommands: 670 args_dict['exitCommands'] = exitCommands 671 if terminateCommands: 672 args_dict['terminateCommands'] = terminateCommands 673 if sourcePath: 674 args_dict['sourcePath'] = sourcePath 675 if debuggerRoot: 676 args_dict['debuggerRoot'] = debuggerRoot 677 if launchCommands: 678 args_dict['launchCommands'] = launchCommands 679 if sourceMap: 680 args_dict['sourceMap'] = sourceMap 681 if runInTerminal: 682 args_dict['runInTerminal'] = runInTerminal 683 if postRunCommands: 684 args_dict['postRunCommands'] = postRunCommands 685 command_dict = { 686 'command': 'launch', 687 'type': 'request', 688 'arguments': args_dict 689 } 690 response = self.send_recv(command_dict) 691 692 if not expectFailure: 693 # Wait for a 'process' and 'initialized' event in any order 694 self.wait_for_event(filter=['process', 'initialized']) 695 self.wait_for_event(filter=['process', 'initialized']) 696 return response 697 698 def request_next(self, threadId): 699 if self.exit_status is not None: 700 raise ValueError('request_continue called after process exited') 701 args_dict = {'threadId': threadId} 702 command_dict = { 703 'command': 'next', 704 'type': 'request', 705 'arguments': args_dict 706 } 707 return self.send_recv(command_dict) 708 709 def request_stepIn(self, threadId): 710 if self.exit_status is not None: 711 raise ValueError('request_continue called after process exited') 712 args_dict = {'threadId': threadId} 713 command_dict = { 714 'command': 'stepIn', 715 'type': 'request', 716 'arguments': args_dict 717 } 718 return self.send_recv(command_dict) 719 720 def request_stepOut(self, threadId): 721 if self.exit_status is not None: 722 raise ValueError('request_continue called after process exited') 723 args_dict = {'threadId': threadId} 724 command_dict = { 725 'command': 'stepOut', 726 'type': 'request', 727 'arguments': args_dict 728 } 729 return self.send_recv(command_dict) 730 731 def request_pause(self, threadId=None): 732 if self.exit_status is not None: 733 raise ValueError('request_continue called after process exited') 734 if threadId is None: 735 threadId = self.get_thread_id() 736 args_dict = {'threadId': threadId} 737 command_dict = { 738 'command': 'pause', 739 'type': 'request', 740 'arguments': args_dict 741 } 742 return self.send_recv(command_dict) 743 744 def request_scopes(self, frameId): 745 args_dict = {'frameId': frameId} 746 command_dict = { 747 'command': 'scopes', 748 'type': 'request', 749 'arguments': args_dict 750 } 751 return self.send_recv(command_dict) 752 753 def request_setBreakpoints(self, file_path, line_array, condition=None, 754 hitCondition=None): 755 (dir, base) = os.path.split(file_path) 756 source_dict = { 757 'name': base, 758 'path': file_path 759 } 760 args_dict = { 761 'source': source_dict, 762 'sourceModified': False, 763 } 764 if line_array is not None: 765 args_dict['lines'] = '%s' % line_array 766 breakpoints = [] 767 for line in line_array: 768 bp = {'line': line} 769 if condition is not None: 770 bp['condition'] = condition 771 if hitCondition is not None: 772 bp['hitCondition'] = hitCondition 773 breakpoints.append(bp) 774 args_dict['breakpoints'] = breakpoints 775 776 command_dict = { 777 'command': 'setBreakpoints', 778 'type': 'request', 779 'arguments': args_dict 780 } 781 return self.send_recv(command_dict) 782 783 def request_setExceptionBreakpoints(self, filters): 784 args_dict = {'filters': filters} 785 command_dict = { 786 'command': 'setExceptionBreakpoints', 787 'type': 'request', 788 'arguments': args_dict 789 } 790 return self.send_recv(command_dict) 791 792 def request_setFunctionBreakpoints(self, names, condition=None, 793 hitCondition=None): 794 breakpoints = [] 795 for name in names: 796 bp = {'name': name} 797 if condition is not None: 798 bp['condition'] = condition 799 if hitCondition is not None: 800 bp['hitCondition'] = hitCondition 801 breakpoints.append(bp) 802 args_dict = {'breakpoints': breakpoints} 803 command_dict = { 804 'command': 'setFunctionBreakpoints', 805 'type': 'request', 806 'arguments': args_dict 807 } 808 return self.send_recv(command_dict) 809 810 def request_compileUnits(self, moduleId): 811 args_dict = {'moduleId': moduleId} 812 command_dict = { 813 'command': 'compileUnits', 814 'type': 'request', 815 'arguments': args_dict 816 } 817 response = self.send_recv(command_dict) 818 return response 819 820 def request_completions(self, text): 821 args_dict = { 822 'text': text, 823 'column': len(text) 824 } 825 command_dict = { 826 'command': 'completions', 827 'type': 'request', 828 'arguments': args_dict 829 } 830 return self.send_recv(command_dict) 831 832 def request_modules(self): 833 return self.send_recv({ 834 'command': 'modules', 835 'type': 'request' 836 }) 837 838 def request_stackTrace(self, threadId=None, startFrame=None, levels=None, 839 dump=False): 840 if threadId is None: 841 threadId = self.get_thread_id() 842 args_dict = {'threadId': threadId} 843 if startFrame is not None: 844 args_dict['startFrame'] = startFrame 845 if levels is not None: 846 args_dict['levels'] = levels 847 command_dict = { 848 'command': 'stackTrace', 849 'type': 'request', 850 'arguments': args_dict 851 } 852 response = self.send_recv(command_dict) 853 if dump: 854 for (idx, frame) in enumerate(response['body']['stackFrames']): 855 name = frame['name'] 856 if 'line' in frame and 'source' in frame: 857 source = frame['source'] 858 if 'sourceReference' not in source: 859 if 'name' in source: 860 source_name = source['name'] 861 line = frame['line'] 862 print("[%3u] %s @ %s:%u" % (idx, name, source_name, 863 line)) 864 continue 865 print("[%3u] %s" % (idx, name)) 866 return response 867 868 def request_threads(self): 869 '''Request a list of all threads and combine any information from any 870 "stopped" events since those contain more information about why a 871 thread actually stopped. Returns an array of thread dictionaries 872 with information about all threads''' 873 command_dict = { 874 'command': 'threads', 875 'type': 'request', 876 'arguments': {} 877 } 878 response = self.send_recv(command_dict) 879 body = response['body'] 880 # Fill in "self.threads" correctly so that clients that call 881 # self.get_threads() or self.get_thread_id(...) can get information 882 # on threads when the process is stopped. 883 if 'threads' in body: 884 self.threads = body['threads'] 885 for thread in self.threads: 886 # Copy the thread dictionary so we can add key/value pairs to 887 # it without affecting the original info from the "threads" 888 # command. 889 tid = thread['id'] 890 if tid in self.thread_stop_reasons: 891 thread_stop_info = self.thread_stop_reasons[tid] 892 copy_keys = ['reason', 'description', 'text'] 893 for key in copy_keys: 894 if key in thread_stop_info: 895 thread[key] = thread_stop_info[key] 896 else: 897 self.threads = None 898 return response 899 900 def request_variables(self, variablesReference, start=None, count=None): 901 args_dict = {'variablesReference': variablesReference} 902 if start is not None: 903 args_dict['start'] = start 904 if count is not None: 905 args_dict['count'] = count 906 command_dict = { 907 'command': 'variables', 908 'type': 'request', 909 'arguments': args_dict 910 } 911 return self.send_recv(command_dict) 912 913 def request_setVariable(self, containingVarRef, name, value, id=None): 914 args_dict = { 915 'variablesReference': containingVarRef, 916 'name': name, 917 'value': str(value) 918 } 919 if id is not None: 920 args_dict['id'] = id 921 command_dict = { 922 'command': 'setVariable', 923 'type': 'request', 924 'arguments': args_dict 925 } 926 return self.send_recv(command_dict) 927 928 def request_testGetTargetBreakpoints(self): 929 '''A request packet used in the LLDB test suite to get all currently 930 set breakpoint infos for all breakpoints currently set in the 931 target. 932 ''' 933 command_dict = { 934 'command': '_testGetTargetBreakpoints', 935 'type': 'request', 936 'arguments': {} 937 } 938 return self.send_recv(command_dict) 939 940 def terminate(self): 941 self.send.close() 942 # self.recv.close() 943 944 945class DebugAdaptor(DebugCommunication): 946 def __init__(self, executable=None, port=None, init_commands=[], log_file=None, env=None): 947 self.process = None 948 if executable is not None: 949 adaptor_env = os.environ.copy() 950 if env is not None: 951 adaptor_env.update(env) 952 953 if log_file: 954 adaptor_env['LLDBVSCODE_LOG'] = log_file 955 self.process = subprocess.Popen([executable], 956 stdin=subprocess.PIPE, 957 stdout=subprocess.PIPE, 958 stderr=subprocess.PIPE, 959 env=adaptor_env) 960 DebugCommunication.__init__(self, self.process.stdout, 961 self.process.stdin, init_commands, log_file) 962 elif port is not None: 963 s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 964 s.connect(('127.0.0.1', port)) 965 DebugCommunication.__init__(self, s.makefile('r'), s.makefile('w'), 966 init_commands) 967 968 def get_pid(self): 969 if self.process: 970 return self.process.pid 971 return -1 972 973 def terminate(self): 974 super(DebugAdaptor, self).terminate() 975 if self.process is not None: 976 self.process.terminate() 977 self.process.wait() 978 self.process = None 979 980 981def attach_options_specified(options): 982 if options.pid is not None: 983 return True 984 if options.waitFor: 985 return True 986 if options.attach: 987 return True 988 if options.attachCmds: 989 return True 990 return False 991 992 993def run_vscode(dbg, args, options): 994 dbg.request_initialize() 995 if attach_options_specified(options): 996 response = dbg.request_attach(program=options.program, 997 pid=options.pid, 998 waitFor=options.waitFor, 999 attachCommands=options.attachCmds, 1000 initCommands=options.initCmds, 1001 preRunCommands=options.preRunCmds, 1002 stopCommands=options.stopCmds, 1003 exitCommands=options.exitCmds, 1004 terminateCommands=options.terminateCmds) 1005 else: 1006 response = dbg.request_launch(options.program, 1007 args=args, 1008 env=options.envs, 1009 cwd=options.workingDir, 1010 debuggerRoot=options.debuggerRoot, 1011 sourcePath=options.sourcePath, 1012 initCommands=options.initCmds, 1013 preRunCommands=options.preRunCmds, 1014 stopCommands=options.stopCmds, 1015 exitCommands=options.exitCmds, 1016 terminateCommands=options.terminateCmds) 1017 1018 if response['success']: 1019 if options.sourceBreakpoints: 1020 source_to_lines = {} 1021 for file_line in options.sourceBreakpoints: 1022 (path, line) = file_line.split(':') 1023 if len(path) == 0 or len(line) == 0: 1024 print('error: invalid source with line "%s"' % 1025 (file_line)) 1026 1027 else: 1028 if path in source_to_lines: 1029 source_to_lines[path].append(int(line)) 1030 else: 1031 source_to_lines[path] = [int(line)] 1032 for source in source_to_lines: 1033 dbg.request_setBreakpoints(source, source_to_lines[source]) 1034 if options.funcBreakpoints: 1035 dbg.request_setFunctionBreakpoints(options.funcBreakpoints) 1036 dbg.request_configurationDone() 1037 dbg.wait_for_stopped() 1038 else: 1039 if 'message' in response: 1040 print(response['message']) 1041 dbg.request_disconnect(terminateDebuggee=True) 1042 1043 1044def main(): 1045 parser = optparse.OptionParser( 1046 description=('A testing framework for the Visual Studio Code Debug ' 1047 'Adaptor protocol')) 1048 1049 parser.add_option( 1050 '--vscode', 1051 type='string', 1052 dest='vscode_path', 1053 help=('The path to the command line program that implements the ' 1054 'Visual Studio Code Debug Adaptor protocol.'), 1055 default=None) 1056 1057 parser.add_option( 1058 '--program', 1059 type='string', 1060 dest='program', 1061 help='The path to the program to debug.', 1062 default=None) 1063 1064 parser.add_option( 1065 '--workingDir', 1066 type='string', 1067 dest='workingDir', 1068 default=None, 1069 help='Set the working directory for the process we launch.') 1070 1071 parser.add_option( 1072 '--sourcePath', 1073 type='string', 1074 dest='sourcePath', 1075 default=None, 1076 help=('Set the relative source root for any debug info that has ' 1077 'relative paths in it.')) 1078 1079 parser.add_option( 1080 '--debuggerRoot', 1081 type='string', 1082 dest='debuggerRoot', 1083 default=None, 1084 help=('Set the working directory for lldb-vscode for any object files ' 1085 'with relative paths in the Mach-o debug map.')) 1086 1087 parser.add_option( 1088 '-r', '--replay', 1089 type='string', 1090 dest='replay', 1091 help=('Specify a file containing a packet log to replay with the ' 1092 'current Visual Studio Code Debug Adaptor executable.'), 1093 default=None) 1094 1095 parser.add_option( 1096 '-g', '--debug', 1097 action='store_true', 1098 dest='debug', 1099 default=False, 1100 help='Pause waiting for a debugger to attach to the debug adaptor') 1101 1102 parser.add_option( 1103 '--port', 1104 type='int', 1105 dest='port', 1106 help="Attach a socket to a port instead of using STDIN for VSCode", 1107 default=None) 1108 1109 parser.add_option( 1110 '--pid', 1111 type='int', 1112 dest='pid', 1113 help="The process ID to attach to", 1114 default=None) 1115 1116 parser.add_option( 1117 '--attach', 1118 action='store_true', 1119 dest='attach', 1120 default=False, 1121 help=('Specify this option to attach to a process by name. The ' 1122 'process name is the basename of the executable specified with ' 1123 'the --program option.')) 1124 1125 parser.add_option( 1126 '-f', '--function-bp', 1127 type='string', 1128 action='append', 1129 dest='funcBreakpoints', 1130 help=('Specify the name of a function to break at. ' 1131 'Can be specified more than once.'), 1132 default=[]) 1133 1134 parser.add_option( 1135 '-s', '--source-bp', 1136 type='string', 1137 action='append', 1138 dest='sourceBreakpoints', 1139 default=[], 1140 help=('Specify source breakpoints to set in the format of ' 1141 '<source>:<line>. ' 1142 'Can be specified more than once.')) 1143 1144 parser.add_option( 1145 '--attachCommand', 1146 type='string', 1147 action='append', 1148 dest='attachCmds', 1149 default=[], 1150 help=('Specify a LLDB command that will attach to a process. ' 1151 'Can be specified more than once.')) 1152 1153 parser.add_option( 1154 '--initCommand', 1155 type='string', 1156 action='append', 1157 dest='initCmds', 1158 default=[], 1159 help=('Specify a LLDB command that will be executed before the target ' 1160 'is created. Can be specified more than once.')) 1161 1162 parser.add_option( 1163 '--preRunCommand', 1164 type='string', 1165 action='append', 1166 dest='preRunCmds', 1167 default=[], 1168 help=('Specify a LLDB command that will be executed after the target ' 1169 'has been created. Can be specified more than once.')) 1170 1171 parser.add_option( 1172 '--stopCommand', 1173 type='string', 1174 action='append', 1175 dest='stopCmds', 1176 default=[], 1177 help=('Specify a LLDB command that will be executed each time the' 1178 'process stops. Can be specified more than once.')) 1179 1180 parser.add_option( 1181 '--exitCommand', 1182 type='string', 1183 action='append', 1184 dest='exitCmds', 1185 default=[], 1186 help=('Specify a LLDB command that will be executed when the process ' 1187 'exits. Can be specified more than once.')) 1188 1189 parser.add_option( 1190 '--terminateCommand', 1191 type='string', 1192 action='append', 1193 dest='terminateCmds', 1194 default=[], 1195 help=('Specify a LLDB command that will be executed when the debugging ' 1196 'session is terminated. Can be specified more than once.')) 1197 1198 parser.add_option( 1199 '--env', 1200 type='string', 1201 action='append', 1202 dest='envs', 1203 default=[], 1204 help=('Specify environment variables to pass to the launched ' 1205 'process.')) 1206 1207 parser.add_option( 1208 '--waitFor', 1209 action='store_true', 1210 dest='waitFor', 1211 default=False, 1212 help=('Wait for the next process to be launched whose name matches ' 1213 'the basename of the program specified with the --program ' 1214 'option')) 1215 1216 (options, args) = parser.parse_args(sys.argv[1:]) 1217 1218 if options.vscode_path is None and options.port is None: 1219 print('error: must either specify a path to a Visual Studio Code ' 1220 'Debug Adaptor vscode executable path using the --vscode ' 1221 'option, or a port to attach to for an existing lldb-vscode ' 1222 'using the --port option') 1223 return 1224 dbg = DebugAdaptor(executable=options.vscode_path, port=options.port) 1225 if options.debug: 1226 raw_input('Waiting for debugger to attach pid "%i"' % ( 1227 dbg.get_pid())) 1228 if options.replay: 1229 dbg.replay_packets(options.replay) 1230 else: 1231 run_vscode(dbg, args, options) 1232 dbg.terminate() 1233 1234 1235if __name__ == '__main__': 1236 main() 1237