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