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