xref: /openbsd-src/gnu/llvm/lldb/packages/Python/lldbsuite/test/tools/lldb-vscode/vscode.py (revision ed30dad480e71d1b8426e69090d8b579f7330c4a)
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 = self.recv_packet(filter_type='response')
304            if response is None:
305                desc = 'no response for "%s"' % (command['command'])
306                raise ValueError(desc)
307            self.validate_response(command, response)
308            return response
309        return None
310
311    def wait_for_event(self, filter=None, timeout=None):
312        while True:
313            return self.recv_packet(filter_type='event', filter_event=filter,
314                                    timeout=timeout)
315        return None
316
317    def wait_for_stopped(self, timeout=None):
318        stopped_events = []
319        stopped_event = self.wait_for_event(filter=['stopped', 'exited'],
320                                            timeout=timeout)
321        exited = False
322        while stopped_event:
323            stopped_events.append(stopped_event)
324            # If we exited, then we are done
325            if stopped_event['event'] == 'exited':
326                self.exit_status = stopped_event['body']['exitCode']
327                exited = True
328                break
329            # Otherwise we stopped and there might be one or more 'stopped'
330            # events for each thread that stopped with a reason, so keep
331            # checking for more 'stopped' events and return all of them
332            stopped_event = self.wait_for_event(filter='stopped', timeout=0.25)
333        if exited:
334            self.threads = []
335        return stopped_events
336
337    def wait_for_exited(self):
338        event_dict = self.wait_for_event('exited')
339        if event_dict is None:
340            raise ValueError("didn't get stopped event")
341        return event_dict
342
343    def get_initialize_value(self, key):
344        '''Get a value for the given key if it there is a key/value pair in
345           the "initialize" request response body.
346        '''
347        if self.initialize_body and key in self.initialize_body:
348            return self.initialize_body[key]
349        return None
350
351    def get_threads(self):
352        if self.threads is None:
353            self.request_threads()
354        return self.threads
355
356    def get_thread_id(self, threadIndex=0):
357        '''Utility function to get the first thread ID in the thread list.
358           If the thread list is empty, then fetch the threads.
359        '''
360        if self.threads is None:
361            self.request_threads()
362        if self.threads and threadIndex < len(self.threads):
363            return self.threads[threadIndex]['id']
364        return None
365
366    def get_stackFrame(self, frameIndex=0, threadId=None):
367        '''Get a single "StackFrame" object from a "stackTrace" request and
368           return the "StackFrame as a python dictionary, or None on failure
369        '''
370        if threadId is None:
371            threadId = self.get_thread_id()
372        if threadId is None:
373            print('invalid threadId')
374            return None
375        response = self.request_stackTrace(threadId, startFrame=frameIndex,
376                                           levels=1)
377        if response:
378            return response['body']['stackFrames'][0]
379        print('invalid response')
380        return None
381
382    def get_completions(self, text):
383        response = self.request_completions(text)
384        return response['body']['targets']
385
386    def get_scope_variables(self, scope_name, frameIndex=0, threadId=None):
387        stackFrame = self.get_stackFrame(frameIndex=frameIndex,
388                                         threadId=threadId)
389        if stackFrame is None:
390            return []
391        frameId = stackFrame['id']
392        if frameId in self.frame_scopes:
393            frame_scopes = self.frame_scopes[frameId]
394        else:
395            scopes_response = self.request_scopes(frameId)
396            frame_scopes = scopes_response['body']['scopes']
397            self.frame_scopes[frameId] = frame_scopes
398        for scope in frame_scopes:
399            if scope['name'] == scope_name:
400                varRef = scope['variablesReference']
401                variables_response = self.request_variables(varRef)
402                if variables_response:
403                    if 'body' in variables_response:
404                        body = variables_response['body']
405                        if 'variables' in body:
406                            vars = body['variables']
407                            return vars
408        return []
409
410    def get_global_variables(self, frameIndex=0, threadId=None):
411        return self.get_scope_variables('Globals', frameIndex=frameIndex,
412                                        threadId=threadId)
413
414    def get_local_variables(self, frameIndex=0, threadId=None):
415        return self.get_scope_variables('Locals', frameIndex=frameIndex,
416                                        threadId=threadId)
417
418    def get_local_variable(self, name, frameIndex=0, threadId=None):
419        locals = self.get_local_variables(frameIndex=frameIndex,
420                                          threadId=threadId)
421        for local in locals:
422            if 'name' in local and local['name'] == name:
423                return local
424        return None
425
426    def get_local_variable_value(self, name, frameIndex=0, threadId=None):
427        variable = self.get_local_variable(name, frameIndex=frameIndex,
428                                           threadId=threadId)
429        if variable and 'value' in variable:
430            return variable['value']
431        return None
432
433    def replay_packets(self, replay_file_path):
434        f = open(replay_file_path, 'r')
435        mode = 'invalid'
436        set_sequence = False
437        command_dict = None
438        while mode != 'eof':
439            if mode == 'invalid':
440                line = f.readline()
441                if line.startswith('to adapter:'):
442                    mode = 'send'
443                elif line.startswith('from adapter:'):
444                    mode = 'recv'
445            elif mode == 'send':
446                command_dict = read_packet(f)
447                # Skip the end of line that follows the JSON
448                f.readline()
449                if command_dict is None:
450                    raise ValueError('decode packet failed from replay file')
451                print('Sending:')
452                pprint.PrettyPrinter(indent=2).pprint(command_dict)
453                # raw_input('Press ENTER to send:')
454                self.send_packet(command_dict, set_sequence)
455                mode = 'invalid'
456            elif mode == 'recv':
457                print('Replay response:')
458                replay_response = read_packet(f)
459                # Skip the end of line that follows the JSON
460                f.readline()
461                pprint.PrettyPrinter(indent=2).pprint(replay_response)
462                actual_response = self.recv_packet()
463                if actual_response:
464                    type = actual_response['type']
465                    print('Actual response:')
466                    if type == 'response':
467                        self.validate_response(command_dict, actual_response)
468                    pprint.PrettyPrinter(indent=2).pprint(actual_response)
469                else:
470                    print("error: didn't get a valid response")
471                mode = 'invalid'
472
473    def request_attach(self, program=None, pid=None, waitFor=None, trace=None,
474                       initCommands=None, preRunCommands=None,
475                       stopCommands=None, exitCommands=None,
476                       attachCommands=None, terminateCommands=None,
477                       coreFile=None):
478        args_dict = {}
479        if pid is not None:
480            args_dict['pid'] = pid
481        if program is not None:
482            args_dict['program'] = program
483        if waitFor is not None:
484            args_dict['waitFor'] = waitFor
485        if trace:
486            args_dict['trace'] = trace
487        args_dict['initCommands'] = self.init_commands
488        if initCommands:
489            args_dict['initCommands'].extend(initCommands)
490        if preRunCommands:
491            args_dict['preRunCommands'] = preRunCommands
492        if stopCommands:
493            args_dict['stopCommands'] = stopCommands
494        if exitCommands:
495            args_dict['exitCommands'] = exitCommands
496        if terminateCommands:
497            args_dict['terminateCommands'] = terminateCommands
498        if attachCommands:
499            args_dict['attachCommands'] = attachCommands
500        if coreFile:
501            args_dict['coreFile'] = coreFile
502        command_dict = {
503            'command': 'attach',
504            'type': 'request',
505            'arguments': args_dict
506        }
507        return self.send_recv(command_dict)
508
509    def request_configurationDone(self):
510        command_dict = {
511            'command': 'configurationDone',
512            'type': 'request',
513            'arguments': {}
514        }
515        response = self.send_recv(command_dict)
516        if response:
517            self.configuration_done_sent = True
518        return response
519
520    def _process_stopped(self):
521        self.threads = None
522        self.frame_scopes = {}
523
524    def request_continue(self, threadId=None):
525        if self.exit_status is not None:
526            raise ValueError('request_continue called after process exited')
527        # If we have launched or attached, then the first continue is done by
528        # sending the 'configurationDone' request
529        if not self.configuration_done_sent:
530            return self.request_configurationDone()
531        args_dict = {}
532        if threadId is None:
533            threadId = self.get_thread_id()
534        args_dict['threadId'] = threadId
535        command_dict = {
536            'command': 'continue',
537            'type': 'request',
538            'arguments': args_dict
539        }
540        response = self.send_recv(command_dict)
541        # Caller must still call wait_for_stopped.
542        return response
543
544    def request_disconnect(self, terminateDebuggee=None):
545        args_dict = {}
546        if terminateDebuggee is not None:
547            if terminateDebuggee:
548                args_dict['terminateDebuggee'] = True
549            else:
550                args_dict['terminateDebuggee'] = False
551        command_dict = {
552            'command': 'disconnect',
553            'type': 'request',
554            'arguments': args_dict
555        }
556        return self.send_recv(command_dict)
557
558    def request_evaluate(self, expression, frameIndex=0, threadId=None):
559        stackFrame = self.get_stackFrame(frameIndex=frameIndex,
560                                         threadId=threadId)
561        if stackFrame is None:
562            return []
563        args_dict = {
564            'expression': expression,
565            'frameId': stackFrame['id'],
566        }
567        command_dict = {
568            'command': 'evaluate',
569            'type': 'request',
570            'arguments': args_dict
571        }
572        return self.send_recv(command_dict)
573
574    def request_initialize(self):
575        command_dict = {
576            'command': 'initialize',
577            'type': 'request',
578            'arguments': {
579                'adapterID': 'lldb-native',
580                'clientID': 'vscode',
581                'columnsStartAt1': True,
582                'linesStartAt1': True,
583                'locale': 'en-us',
584                'pathFormat': 'path',
585                'supportsRunInTerminalRequest': True,
586                'supportsVariablePaging': True,
587                'supportsVariableType': True
588            }
589        }
590        response = self.send_recv(command_dict)
591        if response:
592            if 'body' in response:
593                self.initialize_body = response['body']
594        return response
595
596    def request_launch(self, program, args=None, cwd=None, env=None,
597                       stopOnEntry=False, disableASLR=True,
598                       disableSTDIO=False, shellExpandArguments=False,
599                       trace=False, initCommands=None, preRunCommands=None,
600                       stopCommands=None, exitCommands=None,
601                       terminateCommands=None ,sourcePath=None,
602                       debuggerRoot=None, launchCommands=None, sourceMap=None):
603        args_dict = {
604            'program': program
605        }
606        if args:
607            args_dict['args'] = args
608        if cwd:
609            args_dict['cwd'] = cwd
610        if env:
611            args_dict['env'] = env
612        if stopOnEntry:
613            args_dict['stopOnEntry'] = stopOnEntry
614        if disableASLR:
615            args_dict['disableASLR'] = disableASLR
616        if disableSTDIO:
617            args_dict['disableSTDIO'] = disableSTDIO
618        if shellExpandArguments:
619            args_dict['shellExpandArguments'] = shellExpandArguments
620        if trace:
621            args_dict['trace'] = trace
622        args_dict['initCommands'] = self.init_commands
623        if initCommands:
624            args_dict['initCommands'].extend(initCommands)
625        if preRunCommands:
626            args_dict['preRunCommands'] = preRunCommands
627        if stopCommands:
628            args_dict['stopCommands'] = stopCommands
629        if exitCommands:
630            args_dict['exitCommands'] = exitCommands
631        if terminateCommands:
632            args_dict['terminateCommands'] = terminateCommands
633        if sourcePath:
634            args_dict['sourcePath'] = sourcePath
635        if debuggerRoot:
636            args_dict['debuggerRoot'] = debuggerRoot
637        if launchCommands:
638            args_dict['launchCommands'] = launchCommands
639        if sourceMap:
640            args_dict['sourceMap'] = sourceMap
641        command_dict = {
642            'command': 'launch',
643            'type': 'request',
644            'arguments': args_dict
645        }
646        response = self.send_recv(command_dict)
647
648        # Wait for a 'process' and 'initialized' event in any order
649        self.wait_for_event(filter=['process', 'initialized'])
650        self.wait_for_event(filter=['process', 'initialized'])
651        return response
652
653    def request_next(self, threadId):
654        if self.exit_status is not None:
655            raise ValueError('request_continue called after process exited')
656        args_dict = {'threadId': threadId}
657        command_dict = {
658            'command': 'next',
659            'type': 'request',
660            'arguments': args_dict
661        }
662        return self.send_recv(command_dict)
663
664    def request_stepIn(self, threadId):
665        if self.exit_status is not None:
666            raise ValueError('request_continue called after process exited')
667        args_dict = {'threadId': threadId}
668        command_dict = {
669            'command': 'stepIn',
670            'type': 'request',
671            'arguments': args_dict
672        }
673        return self.send_recv(command_dict)
674
675    def request_stepOut(self, threadId):
676        if self.exit_status is not None:
677            raise ValueError('request_continue called after process exited')
678        args_dict = {'threadId': threadId}
679        command_dict = {
680            'command': 'stepOut',
681            'type': 'request',
682            'arguments': args_dict
683        }
684        return self.send_recv(command_dict)
685
686    def request_pause(self, threadId=None):
687        if self.exit_status is not None:
688            raise ValueError('request_continue called after process exited')
689        if threadId is None:
690            threadId = self.get_thread_id()
691        args_dict = {'threadId': threadId}
692        command_dict = {
693            'command': 'pause',
694            'type': 'request',
695            'arguments': args_dict
696        }
697        return self.send_recv(command_dict)
698
699    def request_scopes(self, frameId):
700        args_dict = {'frameId': frameId}
701        command_dict = {
702            'command': 'scopes',
703            'type': 'request',
704            'arguments': args_dict
705        }
706        return self.send_recv(command_dict)
707
708    def request_setBreakpoints(self, file_path, line_array, condition=None,
709                               hitCondition=None):
710        (dir, base) = os.path.split(file_path)
711        breakpoints = []
712        for line in line_array:
713            bp = {'line': line}
714            if condition is not None:
715                bp['condition'] = condition
716            if hitCondition is not None:
717                bp['hitCondition'] = hitCondition
718            breakpoints.append(bp)
719        source_dict = {
720            'name': base,
721            'path': file_path
722        }
723        args_dict = {
724            'source': source_dict,
725            'breakpoints': breakpoints,
726            'lines': '%s' % (line_array),
727            'sourceModified': False,
728        }
729        command_dict = {
730            'command': 'setBreakpoints',
731            'type': 'request',
732            'arguments': args_dict
733        }
734        return self.send_recv(command_dict)
735
736    def request_setExceptionBreakpoints(self, filters):
737        args_dict = {'filters': filters}
738        command_dict = {
739            'command': 'setExceptionBreakpoints',
740            'type': 'request',
741            'arguments': args_dict
742        }
743        return self.send_recv(command_dict)
744
745    def request_setFunctionBreakpoints(self, names, condition=None,
746                                       hitCondition=None):
747        breakpoints = []
748        for name in names:
749            bp = {'name': name}
750            if condition is not None:
751                bp['condition'] = condition
752            if hitCondition is not None:
753                bp['hitCondition'] = hitCondition
754            breakpoints.append(bp)
755        args_dict = {'breakpoints': breakpoints}
756        command_dict = {
757            'command': 'setFunctionBreakpoints',
758            'type': 'request',
759            'arguments': args_dict
760        }
761        return self.send_recv(command_dict)
762
763    def request_getCompileUnits(self, moduleId):
764        args_dict = {'moduleId': moduleId}
765        command_dict = {
766            'command': 'getCompileUnits',
767            'type': 'request',
768            'arguments': args_dict
769        }
770        response = self.send_recv(command_dict)
771        return response
772
773    def request_completions(self, text):
774        args_dict = {
775            'text': text,
776            'column': len(text)
777        }
778        command_dict = {
779            'command': 'completions',
780            'type': 'request',
781            'arguments': args_dict
782        }
783        return self.send_recv(command_dict)
784
785    def request_stackTrace(self, threadId=None, startFrame=None, levels=None,
786                           dump=False):
787        if threadId is None:
788            threadId = self.get_thread_id()
789        args_dict = {'threadId': threadId}
790        if startFrame is not None:
791            args_dict['startFrame'] = startFrame
792        if levels is not None:
793            args_dict['levels'] = levels
794        command_dict = {
795            'command': 'stackTrace',
796            'type': 'request',
797            'arguments': args_dict
798        }
799        response = self.send_recv(command_dict)
800        if dump:
801            for (idx, frame) in enumerate(response['body']['stackFrames']):
802                name = frame['name']
803                if 'line' in frame and 'source' in frame:
804                    source = frame['source']
805                    if 'sourceReference' not in source:
806                        if 'name' in source:
807                            source_name = source['name']
808                            line = frame['line']
809                            print("[%3u] %s @ %s:%u" % (idx, name, source_name,
810                                                        line))
811                            continue
812                print("[%3u] %s" % (idx, name))
813        return response
814
815    def request_threads(self):
816        '''Request a list of all threads and combine any information from any
817           "stopped" events since those contain more information about why a
818           thread actually stopped. Returns an array of thread dictionaries
819           with information about all threads'''
820        command_dict = {
821            'command': 'threads',
822            'type': 'request',
823            'arguments': {}
824        }
825        response = self.send_recv(command_dict)
826        body = response['body']
827        # Fill in "self.threads" correctly so that clients that call
828        # self.get_threads() or self.get_thread_id(...) can get information
829        # on threads when the process is stopped.
830        if 'threads' in body:
831            self.threads = body['threads']
832            for thread in self.threads:
833                # Copy the thread dictionary so we can add key/value pairs to
834                # it without affecting the original info from the "threads"
835                # command.
836                tid = thread['id']
837                if tid in self.thread_stop_reasons:
838                    thread_stop_info = self.thread_stop_reasons[tid]
839                    copy_keys = ['reason', 'description', 'text']
840                    for key in copy_keys:
841                        if key in thread_stop_info:
842                            thread[key] = thread_stop_info[key]
843        else:
844            self.threads = None
845        return response
846
847    def request_variables(self, variablesReference, start=None, count=None):
848        args_dict = {'variablesReference': variablesReference}
849        if start is not None:
850            args_dict['start'] = start
851        if count is not None:
852            args_dict['count'] = count
853        command_dict = {
854            'command': 'variables',
855            'type': 'request',
856            'arguments': args_dict
857        }
858        return self.send_recv(command_dict)
859
860    def request_setVariable(self, containingVarRef, name, value, id=None):
861        args_dict = {
862            'variablesReference': containingVarRef,
863            'name': name,
864            'value': str(value)
865        }
866        if id is not None:
867            args_dict['id'] = id
868        command_dict = {
869            'command': 'setVariable',
870            'type': 'request',
871            'arguments': args_dict
872        }
873        return self.send_recv(command_dict)
874
875    def request_testGetTargetBreakpoints(self):
876        '''A request packet used in the LLDB test suite to get all currently
877           set breakpoint infos for all breakpoints currently set in the
878           target.
879        '''
880        command_dict = {
881            'command': '_testGetTargetBreakpoints',
882            'type': 'request',
883            'arguments': {}
884        }
885        return self.send_recv(command_dict)
886
887    def terminate(self):
888        self.send.close()
889        # self.recv.close()
890
891
892class DebugAdaptor(DebugCommunication):
893    def __init__(self, executable=None, port=None, init_commands=[], log_file=None):
894        self.process = None
895        if executable is not None:
896            adaptor_env = os.environ.copy()
897            if log_file:
898                adaptor_env['LLDBVSCODE_LOG'] = log_file
899            self.process = subprocess.Popen([executable],
900                                            stdin=subprocess.PIPE,
901                                            stdout=subprocess.PIPE,
902                                            stderr=subprocess.PIPE,
903                                            env=adaptor_env)
904            DebugCommunication.__init__(self, self.process.stdout,
905                                        self.process.stdin, init_commands)
906        elif port is not None:
907            s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
908            s.connect(('127.0.0.1', port))
909            DebugCommunication.__init__(self, s.makefile('r'), s.makefile('w'),
910                init_commands)
911
912    def get_pid(self):
913        if self.process:
914            return self.process.pid
915        return -1
916
917    def terminate(self):
918        super(DebugAdaptor, self).terminate()
919        if self.process is not None:
920            self.process.terminate()
921            self.process.wait()
922            self.process = None
923
924
925def attach_options_specified(options):
926    if options.pid is not None:
927        return True
928    if options.waitFor:
929        return True
930    if options.attach:
931        return True
932    if options.attachCmds:
933        return True
934    return False
935
936
937def run_vscode(dbg, args, options):
938    dbg.request_initialize()
939    if attach_options_specified(options):
940        response = dbg.request_attach(program=options.program,
941                                      pid=options.pid,
942                                      waitFor=options.waitFor,
943                                      attachCommands=options.attachCmds,
944                                      initCommands=options.initCmds,
945                                      preRunCommands=options.preRunCmds,
946                                      stopCommands=options.stopCmds,
947                                      exitCommands=options.exitCmds,
948                                      terminateCommands=options.terminateCmds)
949    else:
950        response = dbg.request_launch(options.program,
951                                      args=args,
952                                      env=options.envs,
953                                      cwd=options.workingDir,
954                                      debuggerRoot=options.debuggerRoot,
955                                      sourcePath=options.sourcePath,
956                                      initCommands=options.initCmds,
957                                      preRunCommands=options.preRunCmds,
958                                      stopCommands=options.stopCmds,
959                                      exitCommands=options.exitCmds,
960                                      terminateCommands=options.terminateCmds)
961
962    if response['success']:
963        if options.sourceBreakpoints:
964            source_to_lines = {}
965            for file_line in options.sourceBreakpoints:
966                (path, line) = file_line.split(':')
967                if len(path) == 0 or len(line) == 0:
968                    print('error: invalid source with line "%s"' %
969                          (file_line))
970
971                else:
972                    if path in source_to_lines:
973                        source_to_lines[path].append(int(line))
974                    else:
975                        source_to_lines[path] = [int(line)]
976            for source in source_to_lines:
977                dbg.request_setBreakpoints(source, source_to_lines[source])
978        if options.funcBreakpoints:
979            dbg.request_setFunctionBreakpoints(options.funcBreakpoints)
980        dbg.request_configurationDone()
981        dbg.wait_for_stopped()
982    else:
983        if 'message' in response:
984            print(response['message'])
985    dbg.request_disconnect(terminateDebuggee=True)
986
987
988def main():
989    parser = optparse.OptionParser(
990        description=('A testing framework for the Visual Studio Code Debug '
991                     'Adaptor protocol'))
992
993    parser.add_option(
994        '--vscode',
995        type='string',
996        dest='vscode_path',
997        help=('The path to the command line program that implements the '
998              'Visual Studio Code Debug Adaptor protocol.'),
999        default=None)
1000
1001    parser.add_option(
1002        '--program',
1003        type='string',
1004        dest='program',
1005        help='The path to the program to debug.',
1006        default=None)
1007
1008    parser.add_option(
1009        '--workingDir',
1010        type='string',
1011        dest='workingDir',
1012        default=None,
1013        help='Set the working directory for the process we launch.')
1014
1015    parser.add_option(
1016        '--sourcePath',
1017        type='string',
1018        dest='sourcePath',
1019        default=None,
1020        help=('Set the relative source root for any debug info that has '
1021              'relative paths in it.'))
1022
1023    parser.add_option(
1024        '--debuggerRoot',
1025        type='string',
1026        dest='debuggerRoot',
1027        default=None,
1028        help=('Set the working directory for lldb-vscode for any object files '
1029              'with relative paths in the Mach-o debug map.'))
1030
1031    parser.add_option(
1032        '-r', '--replay',
1033        type='string',
1034        dest='replay',
1035        help=('Specify a file containing a packet log to replay with the '
1036              'current Visual Studio Code Debug Adaptor executable.'),
1037        default=None)
1038
1039    parser.add_option(
1040        '-g', '--debug',
1041        action='store_true',
1042        dest='debug',
1043        default=False,
1044        help='Pause waiting for a debugger to attach to the debug adaptor')
1045
1046    parser.add_option(
1047        '--port',
1048        type='int',
1049        dest='port',
1050        help="Attach a socket to a port instead of using STDIN for VSCode",
1051        default=None)
1052
1053    parser.add_option(
1054        '--pid',
1055        type='int',
1056        dest='pid',
1057        help="The process ID to attach to",
1058        default=None)
1059
1060    parser.add_option(
1061        '--attach',
1062        action='store_true',
1063        dest='attach',
1064        default=False,
1065        help=('Specify this option to attach to a process by name. The '
1066              'process name is the basename of the executable specified with '
1067              'the --program option.'))
1068
1069    parser.add_option(
1070        '-f', '--function-bp',
1071        type='string',
1072        action='append',
1073        dest='funcBreakpoints',
1074        help=('Specify the name of a function to break at. '
1075              'Can be specified more than once.'),
1076        default=[])
1077
1078    parser.add_option(
1079        '-s', '--source-bp',
1080        type='string',
1081        action='append',
1082        dest='sourceBreakpoints',
1083        default=[],
1084        help=('Specify source breakpoints to set in the format of '
1085              '<source>:<line>. '
1086              'Can be specified more than once.'))
1087
1088    parser.add_option(
1089        '--attachCommand',
1090        type='string',
1091        action='append',
1092        dest='attachCmds',
1093        default=[],
1094        help=('Specify a LLDB command that will attach to a process. '
1095              'Can be specified more than once.'))
1096
1097    parser.add_option(
1098        '--initCommand',
1099        type='string',
1100        action='append',
1101        dest='initCmds',
1102        default=[],
1103        help=('Specify a LLDB command that will be executed before the target '
1104              'is created. Can be specified more than once.'))
1105
1106    parser.add_option(
1107        '--preRunCommand',
1108        type='string',
1109        action='append',
1110        dest='preRunCmds',
1111        default=[],
1112        help=('Specify a LLDB command that will be executed after the target '
1113              'has been created. Can be specified more than once.'))
1114
1115    parser.add_option(
1116        '--stopCommand',
1117        type='string',
1118        action='append',
1119        dest='stopCmds',
1120        default=[],
1121        help=('Specify a LLDB command that will be executed each time the'
1122              'process stops. Can be specified more than once.'))
1123
1124    parser.add_option(
1125        '--exitCommand',
1126        type='string',
1127        action='append',
1128        dest='exitCmds',
1129        default=[],
1130        help=('Specify a LLDB command that will be executed when the process '
1131              'exits. Can be specified more than once.'))
1132
1133    parser.add_option(
1134        '--terminateCommand',
1135        type='string',
1136        action='append',
1137        dest='terminateCmds',
1138        default=[],
1139        help=('Specify a LLDB command that will be executed when the debugging '
1140              'session is terminated. Can be specified more than once.'))
1141
1142    parser.add_option(
1143        '--env',
1144        type='string',
1145        action='append',
1146        dest='envs',
1147        default=[],
1148        help=('Specify environment variables to pass to the launched '
1149              'process.'))
1150
1151    parser.add_option(
1152        '--waitFor',
1153        action='store_true',
1154        dest='waitFor',
1155        default=False,
1156        help=('Wait for the next process to be launched whose name matches '
1157              'the basename of the program specified with the --program '
1158              'option'))
1159
1160    (options, args) = parser.parse_args(sys.argv[1:])
1161
1162    if options.vscode_path is None and options.port is None:
1163        print('error: must either specify a path to a Visual Studio Code '
1164              'Debug Adaptor vscode executable path using the --vscode '
1165              'option, or a port to attach to for an existing lldb-vscode '
1166              'using the --port option')
1167        return
1168    dbg = DebugAdaptor(executable=options.vscode_path, port=options.port)
1169    if options.debug:
1170        raw_input('Waiting for debugger to attach pid "%i"' % (
1171                  dbg.get_pid()))
1172    if options.replay:
1173        dbg.replay_packets(options.replay)
1174    else:
1175        run_vscode(dbg, args, options)
1176    dbg.terminate()
1177
1178
1179if __name__ == '__main__':
1180    main()
1181