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