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