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