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