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