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