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