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
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, postRunCommands=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        if postRunCommands:
523            args_dict['postRunCommands'] = postRunCommands
524        command_dict = {
525            'command': 'attach',
526            'type': 'request',
527            'arguments': args_dict
528        }
529        return self.send_recv(command_dict)
530
531    def request_configurationDone(self):
532        command_dict = {
533            'command': 'configurationDone',
534            'type': 'request',
535            'arguments': {}
536        }
537        response = self.send_recv(command_dict)
538        if response:
539            self.configuration_done_sent = True
540        return response
541
542    def _process_stopped(self):
543        self.threads = None
544        self.frame_scopes = {}
545
546    def request_continue(self, threadId=None):
547        if self.exit_status is not None:
548            raise ValueError('request_continue called after process exited')
549        # If we have launched or attached, then the first continue is done by
550        # sending the 'configurationDone' request
551        if not self.configuration_done_sent:
552            return self.request_configurationDone()
553        args_dict = {}
554        if threadId is None:
555            threadId = self.get_thread_id()
556        args_dict['threadId'] = threadId
557        command_dict = {
558            'command': 'continue',
559            'type': 'request',
560            'arguments': args_dict
561        }
562        response = self.send_recv(command_dict)
563        # Caller must still call wait_for_stopped.
564        return response
565
566    def request_disconnect(self, terminateDebuggee=None):
567        args_dict = {}
568        if terminateDebuggee is not None:
569            if terminateDebuggee:
570                args_dict['terminateDebuggee'] = True
571            else:
572                args_dict['terminateDebuggee'] = False
573        command_dict = {
574            'command': 'disconnect',
575            'type': 'request',
576            'arguments': args_dict
577        }
578        return self.send_recv(command_dict)
579
580    def request_evaluate(self, expression, frameIndex=0, threadId=None, context=None):
581        stackFrame = self.get_stackFrame(frameIndex=frameIndex,
582                                         threadId=threadId)
583        if stackFrame is None:
584            return []
585        args_dict = {
586            'expression': expression,
587            'context': context,
588            'frameId': stackFrame['id'],
589        }
590        command_dict = {
591            'command': 'evaluate',
592            'type': 'request',
593            'arguments': args_dict
594        }
595        return self.send_recv(command_dict)
596
597    def request_initialize(self):
598        command_dict = {
599            'command': 'initialize',
600            'type': 'request',
601            'arguments': {
602                'adapterID': 'lldb-native',
603                'clientID': 'vscode',
604                'columnsStartAt1': True,
605                'linesStartAt1': True,
606                'locale': 'en-us',
607                'pathFormat': 'path',
608                'supportsRunInTerminalRequest': True,
609                'supportsVariablePaging': True,
610                'supportsVariableType': True
611            }
612        }
613        response = self.send_recv(command_dict)
614        if response:
615            if 'body' in response:
616                self.initialize_body = response['body']
617        return response
618
619    def request_launch(self, program, args=None, cwd=None, env=None,
620                       stopOnEntry=False, disableASLR=True,
621                       disableSTDIO=False, shellExpandArguments=False,
622                       trace=False, initCommands=None, preRunCommands=None,
623                       stopCommands=None, exitCommands=None,
624                       terminateCommands=None ,sourcePath=None,
625                       debuggerRoot=None, launchCommands=None, sourceMap=None,
626                       runInTerminal=False, expectFailure=False,
627                       postRunCommands=None):
628        args_dict = {
629            'program': program
630        }
631        if args:
632            args_dict['args'] = args
633        if cwd:
634            args_dict['cwd'] = cwd
635        if env:
636            args_dict['env'] = env
637        if stopOnEntry:
638            args_dict['stopOnEntry'] = stopOnEntry
639        if disableASLR:
640            args_dict['disableASLR'] = disableASLR
641        if disableSTDIO:
642            args_dict['disableSTDIO'] = disableSTDIO
643        if shellExpandArguments:
644            args_dict['shellExpandArguments'] = shellExpandArguments
645        if trace:
646            args_dict['trace'] = trace
647        args_dict['initCommands'] = self.init_commands
648        if initCommands:
649            args_dict['initCommands'].extend(initCommands)
650        if preRunCommands:
651            args_dict['preRunCommands'] = preRunCommands
652        if stopCommands:
653            args_dict['stopCommands'] = stopCommands
654        if exitCommands:
655            args_dict['exitCommands'] = exitCommands
656        if terminateCommands:
657            args_dict['terminateCommands'] = terminateCommands
658        if sourcePath:
659            args_dict['sourcePath'] = sourcePath
660        if debuggerRoot:
661            args_dict['debuggerRoot'] = debuggerRoot
662        if launchCommands:
663            args_dict['launchCommands'] = launchCommands
664        if sourceMap:
665            args_dict['sourceMap'] = sourceMap
666        if runInTerminal:
667            args_dict['runInTerminal'] = runInTerminal
668        if postRunCommands:
669            args_dict['postRunCommands'] = postRunCommands
670        command_dict = {
671            'command': 'launch',
672            'type': 'request',
673            'arguments': args_dict
674        }
675        response = self.send_recv(command_dict)
676
677        if not expectFailure:
678            # Wait for a 'process' and 'initialized' event in any order
679            self.wait_for_event(filter=['process', 'initialized'])
680            self.wait_for_event(filter=['process', 'initialized'])
681        return response
682
683    def request_next(self, threadId):
684        if self.exit_status is not None:
685            raise ValueError('request_continue called after process exited')
686        args_dict = {'threadId': threadId}
687        command_dict = {
688            'command': 'next',
689            'type': 'request',
690            'arguments': args_dict
691        }
692        return self.send_recv(command_dict)
693
694    def request_stepIn(self, threadId):
695        if self.exit_status is not None:
696            raise ValueError('request_continue called after process exited')
697        args_dict = {'threadId': threadId}
698        command_dict = {
699            'command': 'stepIn',
700            'type': 'request',
701            'arguments': args_dict
702        }
703        return self.send_recv(command_dict)
704
705    def request_stepOut(self, threadId):
706        if self.exit_status is not None:
707            raise ValueError('request_continue called after process exited')
708        args_dict = {'threadId': threadId}
709        command_dict = {
710            'command': 'stepOut',
711            'type': 'request',
712            'arguments': args_dict
713        }
714        return self.send_recv(command_dict)
715
716    def request_pause(self, threadId=None):
717        if self.exit_status is not None:
718            raise ValueError('request_continue called after process exited')
719        if threadId is None:
720            threadId = self.get_thread_id()
721        args_dict = {'threadId': threadId}
722        command_dict = {
723            'command': 'pause',
724            'type': 'request',
725            'arguments': args_dict
726        }
727        return self.send_recv(command_dict)
728
729    def request_scopes(self, frameId):
730        args_dict = {'frameId': frameId}
731        command_dict = {
732            'command': 'scopes',
733            'type': 'request',
734            'arguments': args_dict
735        }
736        return self.send_recv(command_dict)
737
738    def request_setBreakpoints(self, file_path, line_array, condition=None,
739                               hitCondition=None):
740        (dir, base) = os.path.split(file_path)
741        source_dict = {
742            'name': base,
743            'path': file_path
744        }
745        args_dict = {
746            'source': source_dict,
747            'sourceModified': False,
748        }
749        if line_array is not None:
750            args_dict['lines'] = '%s' % line_array
751            breakpoints = []
752            for line in line_array:
753                bp = {'line': line}
754                if condition is not None:
755                    bp['condition'] = condition
756                if hitCondition is not None:
757                    bp['hitCondition'] = hitCondition
758                breakpoints.append(bp)
759            args_dict['breakpoints'] = breakpoints
760
761        command_dict = {
762            'command': 'setBreakpoints',
763            'type': 'request',
764            'arguments': args_dict
765        }
766        return self.send_recv(command_dict)
767
768    def request_setExceptionBreakpoints(self, filters):
769        args_dict = {'filters': filters}
770        command_dict = {
771            'command': 'setExceptionBreakpoints',
772            'type': 'request',
773            'arguments': args_dict
774        }
775        return self.send_recv(command_dict)
776
777    def request_setFunctionBreakpoints(self, names, condition=None,
778                                       hitCondition=None):
779        breakpoints = []
780        for name in names:
781            bp = {'name': name}
782            if condition is not None:
783                bp['condition'] = condition
784            if hitCondition is not None:
785                bp['hitCondition'] = hitCondition
786            breakpoints.append(bp)
787        args_dict = {'breakpoints': breakpoints}
788        command_dict = {
789            'command': 'setFunctionBreakpoints',
790            'type': 'request',
791            'arguments': args_dict
792        }
793        return self.send_recv(command_dict)
794
795    def request_compileUnits(self, moduleId):
796        args_dict = {'moduleId': moduleId}
797        command_dict = {
798            'command': 'compileUnits',
799            'type': 'request',
800            'arguments': args_dict
801        }
802        response = self.send_recv(command_dict)
803        return response
804
805    def request_completions(self, text):
806        args_dict = {
807            'text': text,
808            'column': len(text)
809        }
810        command_dict = {
811            'command': 'completions',
812            'type': 'request',
813            'arguments': args_dict
814        }
815        return self.send_recv(command_dict)
816
817    def request_modules(self):
818        return self.send_recv({
819            'command': 'modules',
820            'type': 'request'
821        })
822
823    def request_stackTrace(self, threadId=None, startFrame=None, levels=None,
824                           dump=False):
825        if threadId is None:
826            threadId = self.get_thread_id()
827        args_dict = {'threadId': threadId}
828        if startFrame is not None:
829            args_dict['startFrame'] = startFrame
830        if levels is not None:
831            args_dict['levels'] = levels
832        command_dict = {
833            'command': 'stackTrace',
834            'type': 'request',
835            'arguments': args_dict
836        }
837        response = self.send_recv(command_dict)
838        if dump:
839            for (idx, frame) in enumerate(response['body']['stackFrames']):
840                name = frame['name']
841                if 'line' in frame and 'source' in frame:
842                    source = frame['source']
843                    if 'sourceReference' not in source:
844                        if 'name' in source:
845                            source_name = source['name']
846                            line = frame['line']
847                            print("[%3u] %s @ %s:%u" % (idx, name, source_name,
848                                                        line))
849                            continue
850                print("[%3u] %s" % (idx, name))
851        return response
852
853    def request_threads(self):
854        '''Request a list of all threads and combine any information from any
855           "stopped" events since those contain more information about why a
856           thread actually stopped. Returns an array of thread dictionaries
857           with information about all threads'''
858        command_dict = {
859            'command': 'threads',
860            'type': 'request',
861            'arguments': {}
862        }
863        response = self.send_recv(command_dict)
864        body = response['body']
865        # Fill in "self.threads" correctly so that clients that call
866        # self.get_threads() or self.get_thread_id(...) can get information
867        # on threads when the process is stopped.
868        if 'threads' in body:
869            self.threads = body['threads']
870            for thread in self.threads:
871                # Copy the thread dictionary so we can add key/value pairs to
872                # it without affecting the original info from the "threads"
873                # command.
874                tid = thread['id']
875                if tid in self.thread_stop_reasons:
876                    thread_stop_info = self.thread_stop_reasons[tid]
877                    copy_keys = ['reason', 'description', 'text']
878                    for key in copy_keys:
879                        if key in thread_stop_info:
880                            thread[key] = thread_stop_info[key]
881        else:
882            self.threads = None
883        return response
884
885    def request_variables(self, variablesReference, start=None, count=None):
886        args_dict = {'variablesReference': variablesReference}
887        if start is not None:
888            args_dict['start'] = start
889        if count is not None:
890            args_dict['count'] = count
891        command_dict = {
892            'command': 'variables',
893            'type': 'request',
894            'arguments': args_dict
895        }
896        return self.send_recv(command_dict)
897
898    def request_setVariable(self, containingVarRef, name, value, id=None):
899        args_dict = {
900            'variablesReference': containingVarRef,
901            'name': name,
902            'value': str(value)
903        }
904        if id is not None:
905            args_dict['id'] = id
906        command_dict = {
907            'command': 'setVariable',
908            'type': 'request',
909            'arguments': args_dict
910        }
911        return self.send_recv(command_dict)
912
913    def request_testGetTargetBreakpoints(self):
914        '''A request packet used in the LLDB test suite to get all currently
915           set breakpoint infos for all breakpoints currently set in the
916           target.
917        '''
918        command_dict = {
919            'command': '_testGetTargetBreakpoints',
920            'type': 'request',
921            'arguments': {}
922        }
923        return self.send_recv(command_dict)
924
925    def terminate(self):
926        self.send.close()
927        # self.recv.close()
928
929
930class DebugAdaptor(DebugCommunication):
931    def __init__(self, executable=None, port=None, init_commands=[], log_file=None, env=None):
932        self.process = None
933        if executable is not None:
934            adaptor_env = os.environ.copy()
935            if env is not None:
936                adaptor_env.update(env)
937
938            if log_file:
939                adaptor_env['LLDBVSCODE_LOG'] = log_file
940            self.process = subprocess.Popen([executable],
941                                            stdin=subprocess.PIPE,
942                                            stdout=subprocess.PIPE,
943                                            stderr=subprocess.PIPE,
944                                            env=adaptor_env)
945            DebugCommunication.__init__(self, self.process.stdout,
946                                        self.process.stdin, init_commands)
947        elif port is not None:
948            s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
949            s.connect(('127.0.0.1', port))
950            DebugCommunication.__init__(self, s.makefile('r'), s.makefile('w'),
951                init_commands)
952
953    def get_pid(self):
954        if self.process:
955            return self.process.pid
956        return -1
957
958    def terminate(self):
959        super(DebugAdaptor, self).terminate()
960        if self.process is not None:
961            self.process.terminate()
962            self.process.wait()
963            self.process = None
964
965
966def attach_options_specified(options):
967    if options.pid is not None:
968        return True
969    if options.waitFor:
970        return True
971    if options.attach:
972        return True
973    if options.attachCmds:
974        return True
975    return False
976
977
978def run_vscode(dbg, args, options):
979    dbg.request_initialize()
980    if attach_options_specified(options):
981        response = dbg.request_attach(program=options.program,
982                                      pid=options.pid,
983                                      waitFor=options.waitFor,
984                                      attachCommands=options.attachCmds,
985                                      initCommands=options.initCmds,
986                                      preRunCommands=options.preRunCmds,
987                                      stopCommands=options.stopCmds,
988                                      exitCommands=options.exitCmds,
989                                      terminateCommands=options.terminateCmds)
990    else:
991        response = dbg.request_launch(options.program,
992                                      args=args,
993                                      env=options.envs,
994                                      cwd=options.workingDir,
995                                      debuggerRoot=options.debuggerRoot,
996                                      sourcePath=options.sourcePath,
997                                      initCommands=options.initCmds,
998                                      preRunCommands=options.preRunCmds,
999                                      stopCommands=options.stopCmds,
1000                                      exitCommands=options.exitCmds,
1001                                      terminateCommands=options.terminateCmds)
1002
1003    if response['success']:
1004        if options.sourceBreakpoints:
1005            source_to_lines = {}
1006            for file_line in options.sourceBreakpoints:
1007                (path, line) = file_line.split(':')
1008                if len(path) == 0 or len(line) == 0:
1009                    print('error: invalid source with line "%s"' %
1010                          (file_line))
1011
1012                else:
1013                    if path in source_to_lines:
1014                        source_to_lines[path].append(int(line))
1015                    else:
1016                        source_to_lines[path] = [int(line)]
1017            for source in source_to_lines:
1018                dbg.request_setBreakpoints(source, source_to_lines[source])
1019        if options.funcBreakpoints:
1020            dbg.request_setFunctionBreakpoints(options.funcBreakpoints)
1021        dbg.request_configurationDone()
1022        dbg.wait_for_stopped()
1023    else:
1024        if 'message' in response:
1025            print(response['message'])
1026    dbg.request_disconnect(terminateDebuggee=True)
1027
1028
1029def main():
1030    parser = optparse.OptionParser(
1031        description=('A testing framework for the Visual Studio Code Debug '
1032                     'Adaptor protocol'))
1033
1034    parser.add_option(
1035        '--vscode',
1036        type='string',
1037        dest='vscode_path',
1038        help=('The path to the command line program that implements the '
1039              'Visual Studio Code Debug Adaptor protocol.'),
1040        default=None)
1041
1042    parser.add_option(
1043        '--program',
1044        type='string',
1045        dest='program',
1046        help='The path to the program to debug.',
1047        default=None)
1048
1049    parser.add_option(
1050        '--workingDir',
1051        type='string',
1052        dest='workingDir',
1053        default=None,
1054        help='Set the working directory for the process we launch.')
1055
1056    parser.add_option(
1057        '--sourcePath',
1058        type='string',
1059        dest='sourcePath',
1060        default=None,
1061        help=('Set the relative source root for any debug info that has '
1062              'relative paths in it.'))
1063
1064    parser.add_option(
1065        '--debuggerRoot',
1066        type='string',
1067        dest='debuggerRoot',
1068        default=None,
1069        help=('Set the working directory for lldb-vscode for any object files '
1070              'with relative paths in the Mach-o debug map.'))
1071
1072    parser.add_option(
1073        '-r', '--replay',
1074        type='string',
1075        dest='replay',
1076        help=('Specify a file containing a packet log to replay with the '
1077              'current Visual Studio Code Debug Adaptor executable.'),
1078        default=None)
1079
1080    parser.add_option(
1081        '-g', '--debug',
1082        action='store_true',
1083        dest='debug',
1084        default=False,
1085        help='Pause waiting for a debugger to attach to the debug adaptor')
1086
1087    parser.add_option(
1088        '--port',
1089        type='int',
1090        dest='port',
1091        help="Attach a socket to a port instead of using STDIN for VSCode",
1092        default=None)
1093
1094    parser.add_option(
1095        '--pid',
1096        type='int',
1097        dest='pid',
1098        help="The process ID to attach to",
1099        default=None)
1100
1101    parser.add_option(
1102        '--attach',
1103        action='store_true',
1104        dest='attach',
1105        default=False,
1106        help=('Specify this option to attach to a process by name. The '
1107              'process name is the basename of the executable specified with '
1108              'the --program option.'))
1109
1110    parser.add_option(
1111        '-f', '--function-bp',
1112        type='string',
1113        action='append',
1114        dest='funcBreakpoints',
1115        help=('Specify the name of a function to break at. '
1116              'Can be specified more than once.'),
1117        default=[])
1118
1119    parser.add_option(
1120        '-s', '--source-bp',
1121        type='string',
1122        action='append',
1123        dest='sourceBreakpoints',
1124        default=[],
1125        help=('Specify source breakpoints to set in the format of '
1126              '<source>:<line>. '
1127              'Can be specified more than once.'))
1128
1129    parser.add_option(
1130        '--attachCommand',
1131        type='string',
1132        action='append',
1133        dest='attachCmds',
1134        default=[],
1135        help=('Specify a LLDB command that will attach to a process. '
1136              'Can be specified more than once.'))
1137
1138    parser.add_option(
1139        '--initCommand',
1140        type='string',
1141        action='append',
1142        dest='initCmds',
1143        default=[],
1144        help=('Specify a LLDB command that will be executed before the target '
1145              'is created. Can be specified more than once.'))
1146
1147    parser.add_option(
1148        '--preRunCommand',
1149        type='string',
1150        action='append',
1151        dest='preRunCmds',
1152        default=[],
1153        help=('Specify a LLDB command that will be executed after the target '
1154              'has been created. Can be specified more than once.'))
1155
1156    parser.add_option(
1157        '--stopCommand',
1158        type='string',
1159        action='append',
1160        dest='stopCmds',
1161        default=[],
1162        help=('Specify a LLDB command that will be executed each time the'
1163              'process stops. Can be specified more than once.'))
1164
1165    parser.add_option(
1166        '--exitCommand',
1167        type='string',
1168        action='append',
1169        dest='exitCmds',
1170        default=[],
1171        help=('Specify a LLDB command that will be executed when the process '
1172              'exits. Can be specified more than once.'))
1173
1174    parser.add_option(
1175        '--terminateCommand',
1176        type='string',
1177        action='append',
1178        dest='terminateCmds',
1179        default=[],
1180        help=('Specify a LLDB command that will be executed when the debugging '
1181              'session is terminated. Can be specified more than once.'))
1182
1183    parser.add_option(
1184        '--env',
1185        type='string',
1186        action='append',
1187        dest='envs',
1188        default=[],
1189        help=('Specify environment variables to pass to the launched '
1190              'process.'))
1191
1192    parser.add_option(
1193        '--waitFor',
1194        action='store_true',
1195        dest='waitFor',
1196        default=False,
1197        help=('Wait for the next process to be launched whose name matches '
1198              'the basename of the program specified with the --program '
1199              'option'))
1200
1201    (options, args) = parser.parse_args(sys.argv[1:])
1202
1203    if options.vscode_path is None and options.port is None:
1204        print('error: must either specify a path to a Visual Studio Code '
1205              'Debug Adaptor vscode executable path using the --vscode '
1206              'option, or a port to attach to for an existing lldb-vscode '
1207              'using the --port option')
1208        return
1209    dbg = DebugAdaptor(executable=options.vscode_path, port=options.port)
1210    if options.debug:
1211        raw_input('Waiting for debugger to attach pid "%i"' % (
1212                  dbg.get_pid()))
1213    if options.replay:
1214        dbg.replay_packets(options.replay)
1215    else:
1216        run_vscode(dbg, args, options)
1217    dbg.terminate()
1218
1219
1220if __name__ == '__main__':
1221    main()
1222