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