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