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, sourceMap=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        if sourceMap:
609            args_dict['sourceMap'] = sourceMap
610        command_dict = {
611            'command': 'launch',
612            'type': 'request',
613            'arguments': args_dict
614        }
615        response = self.send_recv(command_dict)
616
617        # Wait for a 'process' and 'initialized' event in any order
618        self.wait_for_event(filter=['process', 'initialized'])
619        self.wait_for_event(filter=['process', 'initialized'])
620        return response
621
622    def request_next(self, threadId):
623        if self.exit_status is not None:
624            raise ValueError('request_continue called after process exited')
625        args_dict = {'threadId': threadId}
626        command_dict = {
627            'command': 'next',
628            'type': 'request',
629            'arguments': args_dict
630        }
631        return self.send_recv(command_dict)
632
633    def request_stepIn(self, threadId):
634        if self.exit_status is not None:
635            raise ValueError('request_continue called after process exited')
636        args_dict = {'threadId': threadId}
637        command_dict = {
638            'command': 'stepIn',
639            'type': 'request',
640            'arguments': args_dict
641        }
642        return self.send_recv(command_dict)
643
644    def request_stepOut(self, threadId):
645        if self.exit_status is not None:
646            raise ValueError('request_continue called after process exited')
647        args_dict = {'threadId': threadId}
648        command_dict = {
649            'command': 'stepOut',
650            'type': 'request',
651            'arguments': args_dict
652        }
653        return self.send_recv(command_dict)
654
655    def request_pause(self, threadId=None):
656        if self.exit_status is not None:
657            raise ValueError('request_continue called after process exited')
658        if threadId is None:
659            threadId = self.get_thread_id()
660        args_dict = {'threadId': threadId}
661        command_dict = {
662            'command': 'pause',
663            'type': 'request',
664            'arguments': args_dict
665        }
666        return self.send_recv(command_dict)
667
668    def request_scopes(self, frameId):
669        args_dict = {'frameId': frameId}
670        command_dict = {
671            'command': 'scopes',
672            'type': 'request',
673            'arguments': args_dict
674        }
675        return self.send_recv(command_dict)
676
677    def request_setBreakpoints(self, file_path, line_array, condition=None,
678                               hitCondition=None):
679        (dir, base) = os.path.split(file_path)
680        breakpoints = []
681        for line in line_array:
682            bp = {'line': line}
683            if condition is not None:
684                bp['condition'] = condition
685            if hitCondition is not None:
686                bp['hitCondition'] = hitCondition
687            breakpoints.append(bp)
688        source_dict = {
689            'name': base,
690            'path': file_path
691        }
692        args_dict = {
693            'source': source_dict,
694            'breakpoints': breakpoints,
695            'lines': '%s' % (line_array),
696            'sourceModified': False,
697        }
698        command_dict = {
699            'command': 'setBreakpoints',
700            'type': 'request',
701            'arguments': args_dict
702        }
703        return self.send_recv(command_dict)
704
705    def request_setExceptionBreakpoints(self, filters):
706        args_dict = {'filters': filters}
707        command_dict = {
708            'command': 'setExceptionBreakpoints',
709            'type': 'request',
710            'arguments': args_dict
711        }
712        return self.send_recv(command_dict)
713
714    def request_setFunctionBreakpoints(self, names, condition=None,
715                                       hitCondition=None):
716        breakpoints = []
717        for name in names:
718            bp = {'name': name}
719            if condition is not None:
720                bp['condition'] = condition
721            if hitCondition is not None:
722                bp['hitCondition'] = hitCondition
723            breakpoints.append(bp)
724        args_dict = {'breakpoints': breakpoints}
725        command_dict = {
726            'command': 'setFunctionBreakpoints',
727            'type': 'request',
728            'arguments': args_dict
729        }
730        return self.send_recv(command_dict)
731
732    def request_completions(self, text):
733        args_dict = {
734            'text': text,
735            'column': len(text)
736        }
737        command_dict = {
738            'command': 'completions',
739            'type': 'request',
740            'arguments': args_dict
741        }
742        return self.send_recv(command_dict)
743
744    def request_stackTrace(self, threadId=None, startFrame=None, levels=None,
745                           dump=False):
746        if threadId is None:
747            threadId = self.get_thread_id()
748        args_dict = {'threadId': threadId}
749        if startFrame is not None:
750            args_dict['startFrame'] = startFrame
751        if levels is not None:
752            args_dict['levels'] = levels
753        command_dict = {
754            'command': 'stackTrace',
755            'type': 'request',
756            'arguments': args_dict
757        }
758        response = self.send_recv(command_dict)
759        if dump:
760            for (idx, frame) in enumerate(response['body']['stackFrames']):
761                name = frame['name']
762                if 'line' in frame and 'source' in frame:
763                    source = frame['source']
764                    if 'sourceReference' not in source:
765                        if 'name' in source:
766                            source_name = source['name']
767                            line = frame['line']
768                            print("[%3u] %s @ %s:%u" % (idx, name, source_name,
769                                                        line))
770                            continue
771                print("[%3u] %s" % (idx, name))
772        return response
773
774    def request_threads(self):
775        '''Request a list of all threads and combine any information from any
776           "stopped" events since those contain more information about why a
777           thread actually stopped. Returns an array of thread dictionaries
778           with information about all threads'''
779        command_dict = {
780            'command': 'threads',
781            'type': 'request',
782            'arguments': {}
783        }
784        response = self.send_recv(command_dict)
785        body = response['body']
786        # Fill in "self.threads" correctly so that clients that call
787        # self.get_threads() or self.get_thread_id(...) can get information
788        # on threads when the process is stopped.
789        if 'threads' in body:
790            self.threads = body['threads']
791            for thread in self.threads:
792                # Copy the thread dictionary so we can add key/value pairs to
793                # it without affecting the original info from the "threads"
794                # command.
795                tid = thread['id']
796                if tid in self.thread_stop_reasons:
797                    thread_stop_info = self.thread_stop_reasons[tid]
798                    copy_keys = ['reason', 'description', 'text']
799                    for key in copy_keys:
800                        if key in thread_stop_info:
801                            thread[key] = thread_stop_info[key]
802        else:
803            self.threads = None
804        return response
805
806    def request_variables(self, variablesReference, start=None, count=None):
807        args_dict = {'variablesReference': variablesReference}
808        if start is not None:
809            args_dict['start'] = start
810        if count is not None:
811            args_dict['count'] = count
812        command_dict = {
813            'command': 'variables',
814            'type': 'request',
815            'arguments': args_dict
816        }
817        return self.send_recv(command_dict)
818
819    def request_setVariable(self, containingVarRef, name, value, id=None):
820        args_dict = {
821            'variablesReference': containingVarRef,
822            'name': name,
823            'value': str(value)
824        }
825        if id is not None:
826            args_dict['id'] = id
827        command_dict = {
828            'command': 'setVariable',
829            'type': 'request',
830            'arguments': args_dict
831        }
832        return self.send_recv(command_dict)
833
834    def request_testGetTargetBreakpoints(self):
835        '''A request packet used in the LLDB test suite to get all currently
836           set breakpoint infos for all breakpoints currently set in the
837           target.
838        '''
839        command_dict = {
840            'command': '_testGetTargetBreakpoints',
841            'type': 'request',
842            'arguments': {}
843        }
844        return self.send_recv(command_dict)
845
846    def terminate(self):
847        self.send.close()
848        # self.recv.close()
849
850
851class DebugAdaptor(DebugCommunication):
852    def __init__(self, executable=None, port=None, init_commands=[], log_file=None):
853        self.process = None
854        if executable is not None:
855            adaptor_env = os.environ.copy()
856            if log_file:
857                adaptor_env['LLDBVSCODE_LOG'] = log_file
858            self.process = subprocess.Popen([executable],
859                                            stdin=subprocess.PIPE,
860                                            stdout=subprocess.PIPE,
861                                            stderr=subprocess.PIPE,
862                                            env=adaptor_env)
863            DebugCommunication.__init__(self, self.process.stdout,
864                                        self.process.stdin, init_commands)
865        elif port is not None:
866            s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
867            s.connect(('127.0.0.1', port))
868            DebugCommunication.__init__(self, s.makefile('r'), s.makefile('w'),
869                init_commands)
870
871    def get_pid(self):
872        if self.process:
873            return self.process.pid
874        return -1
875
876    def terminate(self):
877        super(DebugAdaptor, self).terminate()
878        if self.process is not None:
879            self.process.terminate()
880            self.process.wait()
881            self.process = None
882
883
884def attach_options_specified(options):
885    if options.pid is not None:
886        return True
887    if options.waitFor:
888        return True
889    if options.attach:
890        return True
891    if options.attachCmds:
892        return True
893    return False
894
895
896def run_vscode(dbg, args, options):
897    dbg.request_initialize()
898    if attach_options_specified(options):
899        response = dbg.request_attach(program=options.program,
900                                      pid=options.pid,
901                                      waitFor=options.waitFor,
902                                      attachCommands=options.attachCmds,
903                                      initCommands=options.initCmds,
904                                      preRunCommands=options.preRunCmds,
905                                      stopCommands=options.stopCmds,
906                                      exitCommands=options.exitCmds)
907    else:
908        response = dbg.request_launch(options.program,
909                                      args=args,
910                                      env=options.envs,
911                                      cwd=options.workingDir,
912                                      debuggerRoot=options.debuggerRoot,
913                                      sourcePath=options.sourcePath,
914                                      initCommands=options.initCmds,
915                                      preRunCommands=options.preRunCmds,
916                                      stopCommands=options.stopCmds,
917                                      exitCommands=options.exitCmds)
918
919    if response['success']:
920        if options.sourceBreakpoints:
921            source_to_lines = {}
922            for file_line in options.sourceBreakpoints:
923                (path, line) = file_line.split(':')
924                if len(path) == 0 or len(line) == 0:
925                    print('error: invalid source with line "%s"' %
926                          (file_line))
927
928                else:
929                    if path in source_to_lines:
930                        source_to_lines[path].append(int(line))
931                    else:
932                        source_to_lines[path] = [int(line)]
933            for source in source_to_lines:
934                dbg.request_setBreakpoints(source, source_to_lines[source])
935        if options.funcBreakpoints:
936            dbg.request_setFunctionBreakpoints(options.funcBreakpoints)
937        dbg.request_configurationDone()
938        dbg.wait_for_stopped()
939    else:
940        if 'message' in response:
941            print(response['message'])
942    dbg.request_disconnect(terminateDebuggee=True)
943
944
945def main():
946    parser = optparse.OptionParser(
947        description=('A testing framework for the Visual Studio Code Debug '
948                     'Adaptor protocol'))
949
950    parser.add_option(
951        '--vscode',
952        type='string',
953        dest='vscode_path',
954        help=('The path to the command line program that implements the '
955              'Visual Studio Code Debug Adaptor protocol.'),
956        default=None)
957
958    parser.add_option(
959        '--program',
960        type='string',
961        dest='program',
962        help='The path to the program to debug.',
963        default=None)
964
965    parser.add_option(
966        '--workingDir',
967        type='string',
968        dest='workingDir',
969        default=None,
970        help='Set the working directory for the process we launch.')
971
972    parser.add_option(
973        '--sourcePath',
974        type='string',
975        dest='sourcePath',
976        default=None,
977        help=('Set the relative source root for any debug info that has '
978              'relative paths in it.'))
979
980    parser.add_option(
981        '--debuggerRoot',
982        type='string',
983        dest='debuggerRoot',
984        default=None,
985        help=('Set the working directory for lldb-vscode for any object files '
986              'with relative paths in the Mach-o debug map.'))
987
988    parser.add_option(
989        '-r', '--replay',
990        type='string',
991        dest='replay',
992        help=('Specify a file containing a packet log to replay with the '
993              'current Visual Studio Code Debug Adaptor executable.'),
994        default=None)
995
996    parser.add_option(
997        '-g', '--debug',
998        action='store_true',
999        dest='debug',
1000        default=False,
1001        help='Pause waiting for a debugger to attach to the debug adaptor')
1002
1003    parser.add_option(
1004        '--port',
1005        type='int',
1006        dest='port',
1007        help="Attach a socket to a port instead of using STDIN for VSCode",
1008        default=None)
1009
1010    parser.add_option(
1011        '--pid',
1012        type='int',
1013        dest='pid',
1014        help="The process ID to attach to",
1015        default=None)
1016
1017    parser.add_option(
1018        '--attach',
1019        action='store_true',
1020        dest='attach',
1021        default=False,
1022        help=('Specify this option to attach to a process by name. The '
1023              'process name is the basename of the executable specified with '
1024              'the --program option.'))
1025
1026    parser.add_option(
1027        '-f', '--function-bp',
1028        type='string',
1029        action='append',
1030        dest='funcBreakpoints',
1031        help=('Specify the name of a function to break at. '
1032              'Can be specified more than once.'),
1033        default=[])
1034
1035    parser.add_option(
1036        '-s', '--source-bp',
1037        type='string',
1038        action='append',
1039        dest='sourceBreakpoints',
1040        default=[],
1041        help=('Specify source breakpoints to set in the format of '
1042              '<source>:<line>. '
1043              'Can be specified more than once.'))
1044
1045    parser.add_option(
1046        '--attachCommand',
1047        type='string',
1048        action='append',
1049        dest='attachCmds',
1050        default=[],
1051        help=('Specify a LLDB command that will attach to a process. '
1052              'Can be specified more than once.'))
1053
1054    parser.add_option(
1055        '--initCommand',
1056        type='string',
1057        action='append',
1058        dest='initCmds',
1059        default=[],
1060        help=('Specify a LLDB command that will be executed before the target '
1061              'is created. Can be specified more than once.'))
1062
1063    parser.add_option(
1064        '--preRunCommand',
1065        type='string',
1066        action='append',
1067        dest='preRunCmds',
1068        default=[],
1069        help=('Specify a LLDB command that will be executed after the target '
1070              'has been created. Can be specified more than once.'))
1071
1072    parser.add_option(
1073        '--stopCommand',
1074        type='string',
1075        action='append',
1076        dest='stopCmds',
1077        default=[],
1078        help=('Specify a LLDB command that will be executed each time the'
1079              'process stops. Can be specified more than once.'))
1080
1081    parser.add_option(
1082        '--exitCommand',
1083        type='string',
1084        action='append',
1085        dest='exitCmds',
1086        default=[],
1087        help=('Specify a LLDB command that will be executed when the process '
1088              'exits. Can be specified more than once.'))
1089
1090    parser.add_option(
1091        '--env',
1092        type='string',
1093        action='append',
1094        dest='envs',
1095        default=[],
1096        help=('Specify environment variables to pass to the launched '
1097              'process.'))
1098
1099    parser.add_option(
1100        '--waitFor',
1101        action='store_true',
1102        dest='waitFor',
1103        default=False,
1104        help=('Wait for the next process to be launched whose name matches '
1105              'the basename of the program specified with the --program '
1106              'option'))
1107
1108    (options, args) = parser.parse_args(sys.argv[1:])
1109
1110    if options.vscode_path is None and options.port is None:
1111        print('error: must either specify a path to a Visual Studio Code '
1112              'Debug Adaptor vscode executable path using the --vscode '
1113              'option, or a port to attach to for an existing lldb-vscode '
1114              'using the --port option')
1115        return
1116    dbg = DebugAdaptor(executable=options.vscode_path, port=options.port)
1117    if options.debug:
1118        raw_input('Waiting for debugger to attach pid "%i"' % (
1119                  dbg.get_pid()))
1120    if options.replay:
1121        dbg.replay_packets(options.replay)
1122    else:
1123        run_vscode(dbg, args, options)
1124    dbg.terminate()
1125
1126
1127if __name__ == '__main__':
1128    main()
1129