xref: /llvm-project/lldb/packages/Python/lldbsuite/test/tools/lldb-dap/dap_server.py (revision 6257a98b258a3f17b78af31bf43009a559c5dd1d)
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    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    raise Exception("unexpected malformed message from lldb-dap: " + line)
84
85
86def packet_type_is(packet, packet_type):
87    return "type" in packet and packet["type"] == packet_type
88
89
90def dump_dap_log(log_file):
91    print("========= DEBUG ADAPTER PROTOCOL LOGS =========")
92    if log_file is None:
93        print("no log file available")
94    else:
95        with open(log_file, "r") as file:
96            print(file.read())
97    print("========= END =========")
98
99
100def read_packet_thread(vs_comm, log_file):
101    done = False
102    try:
103        while not done:
104            packet = read_packet(vs_comm.recv, trace_file=vs_comm.trace_file)
105            # `packet` will be `None` on EOF. We want to pass it down to
106            # handle_recv_packet anyway so the main thread can handle unexpected
107            # termination of lldb-dap and stop waiting for new packets.
108            done = not vs_comm.handle_recv_packet(packet)
109    finally:
110        dump_dap_log(log_file)
111
112
113class DebugCommunication(object):
114    def __init__(self, recv, send, init_commands, log_file=None):
115        self.trace_file = None
116        self.send = send
117        self.recv = recv
118        self.recv_packets = []
119        self.recv_condition = threading.Condition()
120        self.recv_thread = threading.Thread(
121            target=read_packet_thread, args=(self, log_file)
122        )
123        self.process_event_body = None
124        self.exit_status = None
125        self.initialize_body = None
126        self.thread_stop_reasons = {}
127        self.breakpoint_events = []
128        self.progress_events = []
129        self.reverse_requests = []
130        self.sequence = 1
131        self.threads = None
132        self.recv_thread.start()
133        self.output_condition = threading.Condition()
134        self.output = {}
135        self.configuration_done_sent = False
136        self.frame_scopes = {}
137        self.init_commands = init_commands
138        self.disassembled_instructions = {}
139
140    @classmethod
141    def encode_content(cls, s):
142        return ("Content-Length: %u\r\n\r\n%s" % (len(s), s)).encode("utf-8")
143
144    @classmethod
145    def validate_response(cls, command, response):
146        if command["command"] != response["command"]:
147            raise ValueError("command mismatch in response")
148        if command["seq"] != response["request_seq"]:
149            raise ValueError("seq mismatch in response")
150
151    def get_modules(self):
152        module_list = self.request_modules()["body"]["modules"]
153        modules = {}
154        for module in module_list:
155            modules[module["name"]] = module
156        return modules
157
158    def get_output(self, category, timeout=0.0, clear=True):
159        self.output_condition.acquire()
160        output = None
161        if category in self.output:
162            output = self.output[category]
163            if clear:
164                del self.output[category]
165        elif timeout != 0.0:
166            self.output_condition.wait(timeout)
167            if category in self.output:
168                output = self.output[category]
169                if clear:
170                    del self.output[category]
171        self.output_condition.release()
172        return output
173
174    def collect_output(self, category, timeout_secs, pattern, clear=True):
175        end_time = time.time() + timeout_secs
176        collected_output = ""
177        while end_time > time.time():
178            output = self.get_output(category, timeout=0.25, clear=clear)
179            if output:
180                collected_output += output
181                if pattern is not None and pattern in output:
182                    break
183        return collected_output if collected_output else None
184
185    def enqueue_recv_packet(self, packet):
186        self.recv_condition.acquire()
187        self.recv_packets.append(packet)
188        self.recv_condition.notify()
189        self.recv_condition.release()
190
191    def handle_recv_packet(self, packet):
192        """Called by the read thread that is waiting for all incoming packets
193        to store the incoming packet in "self.recv_packets" in a thread safe
194        way. This function will then signal the "self.recv_condition" to
195        indicate a new packet is available. Returns True if the caller
196        should keep calling this function for more packets.
197        """
198        # If EOF, notify the read thread by enqueuing a None.
199        if not packet:
200            self.enqueue_recv_packet(None)
201            return False
202
203        # Check the packet to see if is an event packet
204        keepGoing = True
205        packet_type = packet["type"]
206        if packet_type == "event":
207            event = packet["event"]
208            body = None
209            if "body" in packet:
210                body = packet["body"]
211            # Handle the event packet and cache information from these packets
212            # as they come in
213            if event == "output":
214                # Store any output we receive so clients can retrieve it later.
215                category = body["category"]
216                output = body["output"]
217                self.output_condition.acquire()
218                if category in self.output:
219                    self.output[category] += output
220                else:
221                    self.output[category] = output
222                self.output_condition.notify()
223                self.output_condition.release()
224                # no need to add 'output' event packets to our packets list
225                return keepGoing
226            elif event == "process":
227                # When a new process is attached or launched, remember the
228                # details that are available in the body of the event
229                self.process_event_body = body
230            elif event == "stopped":
231                # Each thread that stops with a reason will send a
232                # 'stopped' event. We need to remember the thread stop
233                # reasons since the 'threads' command doesn't return
234                # that information.
235                self._process_stopped()
236                tid = body["threadId"]
237                self.thread_stop_reasons[tid] = body
238            elif event == "breakpoint":
239                # Breakpoint events come in when a breakpoint has locations
240                # added or removed. Keep track of them so we can look for them
241                # in tests.
242                self.breakpoint_events.append(packet)
243                # no need to add 'breakpoint' event packets to our packets list
244                return keepGoing
245            elif event.startswith("progress"):
246                # Progress events come in as 'progressStart', 'progressUpdate',
247                # and 'progressEnd' events. Keep these around in case test
248                # cases want to verify them.
249                self.progress_events.append(packet)
250                # No need to add 'progress' event packets to our packets list.
251                return keepGoing
252
253        elif packet_type == "response":
254            if packet["command"] == "disconnect":
255                keepGoing = False
256        self.enqueue_recv_packet(packet)
257        return keepGoing
258
259    def send_packet(self, command_dict, set_sequence=True):
260        """Take the "command_dict" python dictionary and encode it as a JSON
261        string and send the contents as a packet to the VSCode debug
262        adaptor"""
263        # Set the sequence ID for this command automatically
264        if set_sequence:
265            command_dict["seq"] = self.sequence
266            self.sequence += 1
267        # Encode our command dictionary as a JSON string
268        json_str = json.dumps(command_dict, separators=(",", ":"))
269        if self.trace_file:
270            self.trace_file.write("to adaptor:\n%s\n" % (json_str))
271        length = len(json_str)
272        if length > 0:
273            # Send the encoded JSON packet and flush the 'send' file
274            self.send.write(self.encode_content(json_str))
275            self.send.flush()
276
277    def recv_packet(self, filter_type=None, filter_event=None, timeout=None):
278        """Get a JSON packet from the VSCode debug adaptor. This function
279        assumes a thread that reads packets is running and will deliver
280        any received packets by calling handle_recv_packet(...). This
281        function will wait for the packet to arrive and return it when
282        it does."""
283        while True:
284            try:
285                self.recv_condition.acquire()
286                packet = None
287                while True:
288                    for i, curr_packet in enumerate(self.recv_packets):
289                        if not curr_packet:
290                            raise EOFError
291                        packet_type = curr_packet["type"]
292                        if filter_type is None or packet_type in filter_type:
293                            if filter_event is None or (
294                                packet_type == "event"
295                                and curr_packet["event"] in filter_event
296                            ):
297                                packet = self.recv_packets.pop(i)
298                                break
299                    if packet:
300                        break
301                    # Sleep until packet is received
302                    len_before = len(self.recv_packets)
303                    self.recv_condition.wait(timeout)
304                    len_after = len(self.recv_packets)
305                    if len_before == len_after:
306                        return None  # Timed out
307                return packet
308            except EOFError:
309                return None
310            finally:
311                self.recv_condition.release()
312
313        return None
314
315    def send_recv(self, command):
316        """Send a command python dictionary as JSON and receive the JSON
317        response. Validates that the response is the correct sequence and
318        command in the reply. Any events that are received are added to the
319        events list in this object"""
320        self.send_packet(command)
321        done = False
322        while not done:
323            response_or_request = self.recv_packet(filter_type=["response", "request"])
324            if response_or_request is None:
325                desc = 'no response for "%s"' % (command["command"])
326                raise ValueError(desc)
327            if response_or_request["type"] == "response":
328                self.validate_response(command, response_or_request)
329                return response_or_request
330            else:
331                self.reverse_requests.append(response_or_request)
332                if response_or_request["command"] == "runInTerminal":
333                    subprocess.Popen(
334                        response_or_request["arguments"]["args"],
335                        env=response_or_request["arguments"]["env"],
336                    )
337                    self.send_packet(
338                        {
339                            "type": "response",
340                            "seq": -1,
341                            "request_seq": response_or_request["seq"],
342                            "success": True,
343                            "command": "runInTerminal",
344                            "body": {},
345                        },
346                        set_sequence=False,
347                    )
348                elif response_or_request["command"] == "startDebugging":
349                    self.send_packet(
350                        {
351                            "type": "response",
352                            "seq": -1,
353                            "request_seq": response_or_request["seq"],
354                            "success": True,
355                            "command": "startDebugging",
356                            "body": {},
357                        },
358                        set_sequence=False,
359                    )
360                else:
361                    desc = 'unknown reverse request "%s"' % (
362                        response_or_request["command"]
363                    )
364                    raise ValueError(desc)
365
366        return None
367
368    def wait_for_event(self, filter=None, timeout=None):
369        while True:
370            return self.recv_packet(
371                filter_type="event", filter_event=filter, timeout=timeout
372            )
373        return None
374
375    def wait_for_stopped(self, timeout=None):
376        stopped_events = []
377        stopped_event = self.wait_for_event(
378            filter=["stopped", "exited"], timeout=timeout
379        )
380        exited = False
381        while stopped_event:
382            stopped_events.append(stopped_event)
383            # If we exited, then we are done
384            if stopped_event["event"] == "exited":
385                self.exit_status = stopped_event["body"]["exitCode"]
386                exited = True
387                break
388            # Otherwise we stopped and there might be one or more 'stopped'
389            # events for each thread that stopped with a reason, so keep
390            # checking for more 'stopped' events and return all of them
391            stopped_event = self.wait_for_event(filter="stopped", timeout=0.25)
392        if exited:
393            self.threads = []
394        return stopped_events
395
396    def wait_for_exited(self):
397        event_dict = self.wait_for_event("exited")
398        if event_dict is None:
399            raise ValueError("didn't get exited event")
400        return event_dict
401
402    def wait_for_terminated(self):
403        event_dict = self.wait_for_event("terminated")
404        if event_dict is None:
405            raise ValueError("didn't get terminated event")
406        return event_dict
407
408    def get_initialize_value(self, key):
409        """Get a value for the given key if it there is a key/value pair in
410        the "initialize" request response body.
411        """
412        if self.initialize_body and key in self.initialize_body:
413            return self.initialize_body[key]
414        return None
415
416    def get_threads(self):
417        if self.threads is None:
418            self.request_threads()
419        return self.threads
420
421    def get_thread_id(self, threadIndex=0):
422        """Utility function to get the first thread ID in the thread list.
423        If the thread list is empty, then fetch the threads.
424        """
425        if self.threads is None:
426            self.request_threads()
427        if self.threads and threadIndex < len(self.threads):
428            return self.threads[threadIndex]["id"]
429        return None
430
431    def get_stackFrame(self, frameIndex=0, threadId=None):
432        """Get a single "StackFrame" object from a "stackTrace" request and
433        return the "StackFrame" as a python dictionary, or None on failure
434        """
435        if threadId is None:
436            threadId = self.get_thread_id()
437        if threadId is None:
438            print("invalid threadId")
439            return None
440        response = self.request_stackTrace(threadId, startFrame=frameIndex, levels=1)
441        if response:
442            return response["body"]["stackFrames"][0]
443        print("invalid response")
444        return None
445
446    def get_completions(self, text, frameId=None):
447        if frameId is None:
448            stackFrame = self.get_stackFrame()
449            frameId = stackFrame["id"]
450        response = self.request_completions(text, frameId)
451        return response["body"]["targets"]
452
453    def get_scope_variables(self, scope_name, frameIndex=0, threadId=None, is_hex=None):
454        stackFrame = self.get_stackFrame(frameIndex=frameIndex, threadId=threadId)
455        if stackFrame is None:
456            return []
457        frameId = stackFrame["id"]
458        if frameId in self.frame_scopes:
459            frame_scopes = self.frame_scopes[frameId]
460        else:
461            scopes_response = self.request_scopes(frameId)
462            frame_scopes = scopes_response["body"]["scopes"]
463            self.frame_scopes[frameId] = frame_scopes
464        for scope in frame_scopes:
465            if scope["name"] == scope_name:
466                varRef = scope["variablesReference"]
467                variables_response = self.request_variables(varRef, is_hex=is_hex)
468                if variables_response:
469                    if "body" in variables_response:
470                        body = variables_response["body"]
471                        if "variables" in body:
472                            vars = body["variables"]
473                            return vars
474        return []
475
476    def get_global_variables(self, frameIndex=0, threadId=None):
477        return self.get_scope_variables(
478            "Globals", frameIndex=frameIndex, threadId=threadId
479        )
480
481    def get_local_variables(self, frameIndex=0, threadId=None, is_hex=None):
482        return self.get_scope_variables(
483            "Locals", frameIndex=frameIndex, threadId=threadId, is_hex=is_hex
484        )
485
486    def get_registers(self, frameIndex=0, threadId=None):
487        return self.get_scope_variables(
488            "Registers", frameIndex=frameIndex, threadId=threadId
489        )
490
491    def get_local_variable(self, name, frameIndex=0, threadId=None, is_hex=None):
492        locals = self.get_local_variables(
493            frameIndex=frameIndex, threadId=threadId, is_hex=is_hex
494        )
495        for local in locals:
496            if "name" in local and local["name"] == name:
497                return local
498        return None
499
500    def get_local_variable_value(self, name, frameIndex=0, threadId=None, is_hex=None):
501        variable = self.get_local_variable(
502            name, frameIndex=frameIndex, threadId=threadId, is_hex=is_hex
503        )
504        if variable and "value" in variable:
505            return variable["value"]
506        return None
507
508    def get_local_variable_child(
509        self, name, child_name, frameIndex=0, threadId=None, is_hex=None
510    ):
511        local = self.get_local_variable(name, frameIndex, threadId)
512        if local["variablesReference"] == 0:
513            return None
514        children = self.request_variables(local["variablesReference"], is_hex=is_hex)[
515            "body"
516        ]["variables"]
517        for child in children:
518            if child["name"] == child_name:
519                return child
520        return None
521
522    def replay_packets(self, replay_file_path):
523        f = open(replay_file_path, "r")
524        mode = "invalid"
525        set_sequence = False
526        command_dict = None
527        while mode != "eof":
528            if mode == "invalid":
529                line = f.readline()
530                if line.startswith("to adapter:"):
531                    mode = "send"
532                elif line.startswith("from adapter:"):
533                    mode = "recv"
534            elif mode == "send":
535                command_dict = read_packet(f)
536                # Skip the end of line that follows the JSON
537                f.readline()
538                if command_dict is None:
539                    raise ValueError("decode packet failed from replay file")
540                print("Sending:")
541                pprint.PrettyPrinter(indent=2).pprint(command_dict)
542                # raw_input('Press ENTER to send:')
543                self.send_packet(command_dict, set_sequence)
544                mode = "invalid"
545            elif mode == "recv":
546                print("Replay response:")
547                replay_response = read_packet(f)
548                # Skip the end of line that follows the JSON
549                f.readline()
550                pprint.PrettyPrinter(indent=2).pprint(replay_response)
551                actual_response = self.recv_packet()
552                if actual_response:
553                    type = actual_response["type"]
554                    print("Actual response:")
555                    if type == "response":
556                        self.validate_response(command_dict, actual_response)
557                    pprint.PrettyPrinter(indent=2).pprint(actual_response)
558                else:
559                    print("error: didn't get a valid response")
560                mode = "invalid"
561
562    def request_attach(
563        self,
564        program=None,
565        pid=None,
566        waitFor=None,
567        trace=None,
568        initCommands=None,
569        preRunCommands=None,
570        stopCommands=None,
571        exitCommands=None,
572        attachCommands=None,
573        terminateCommands=None,
574        coreFile=None,
575        postRunCommands=None,
576        sourceMap=None,
577        gdbRemotePort=None,
578        gdbRemoteHostname=None,
579    ):
580        args_dict = {}
581        if pid is not None:
582            args_dict["pid"] = pid
583        if program is not None:
584            args_dict["program"] = program
585        if waitFor is not None:
586            args_dict["waitFor"] = waitFor
587        if trace:
588            args_dict["trace"] = trace
589        args_dict["initCommands"] = self.init_commands
590        if initCommands:
591            args_dict["initCommands"].extend(initCommands)
592        if preRunCommands:
593            args_dict["preRunCommands"] = preRunCommands
594        if stopCommands:
595            args_dict["stopCommands"] = stopCommands
596        if exitCommands:
597            args_dict["exitCommands"] = exitCommands
598        if terminateCommands:
599            args_dict["terminateCommands"] = terminateCommands
600        if attachCommands:
601            args_dict["attachCommands"] = attachCommands
602        if coreFile:
603            args_dict["coreFile"] = coreFile
604        if postRunCommands:
605            args_dict["postRunCommands"] = postRunCommands
606        if sourceMap:
607            args_dict["sourceMap"] = sourceMap
608        if gdbRemotePort is not None:
609            args_dict["gdb-remote-port"] = gdbRemotePort
610        if gdbRemoteHostname is not None:
611            args_dict["gdb-remote-hostname"] = gdbRemoteHostname
612        command_dict = {"command": "attach", "type": "request", "arguments": args_dict}
613        return self.send_recv(command_dict)
614
615    def request_configurationDone(self):
616        command_dict = {
617            "command": "configurationDone",
618            "type": "request",
619            "arguments": {},
620        }
621        response = self.send_recv(command_dict)
622        if response:
623            self.configuration_done_sent = True
624        return response
625
626    def _process_stopped(self):
627        self.threads = None
628        self.frame_scopes = {}
629
630    def request_continue(self, threadId=None):
631        if self.exit_status is not None:
632            raise ValueError("request_continue called after process exited")
633        # If we have launched or attached, then the first continue is done by
634        # sending the 'configurationDone' request
635        if not self.configuration_done_sent:
636            return self.request_configurationDone()
637        args_dict = {}
638        if threadId is None:
639            threadId = self.get_thread_id()
640        args_dict["threadId"] = threadId
641        command_dict = {
642            "command": "continue",
643            "type": "request",
644            "arguments": args_dict,
645        }
646        response = self.send_recv(command_dict)
647        # Caller must still call wait_for_stopped.
648        return response
649
650    def request_restart(self, restartArguments=None):
651        command_dict = {
652            "command": "restart",
653            "type": "request",
654        }
655        if restartArguments:
656            command_dict["arguments"] = restartArguments
657
658        response = self.send_recv(command_dict)
659        # Caller must still call wait_for_stopped.
660        return response
661
662    def request_disconnect(self, terminateDebuggee=None):
663        args_dict = {}
664        if terminateDebuggee is not None:
665            if terminateDebuggee:
666                args_dict["terminateDebuggee"] = True
667            else:
668                args_dict["terminateDebuggee"] = False
669        command_dict = {
670            "command": "disconnect",
671            "type": "request",
672            "arguments": args_dict,
673        }
674        return self.send_recv(command_dict)
675
676    def request_disassemble(
677        self, memoryReference, offset=-50, instructionCount=200, resolveSymbols=True
678    ):
679        args_dict = {
680            "memoryReference": memoryReference,
681            "offset": offset,
682            "instructionCount": instructionCount,
683            "resolveSymbols": resolveSymbols,
684        }
685        command_dict = {
686            "command": "disassemble",
687            "type": "request",
688            "arguments": args_dict,
689        }
690        instructions = self.send_recv(command_dict)["body"]["instructions"]
691        for inst in instructions:
692            self.disassembled_instructions[inst["address"]] = inst
693
694    def request_evaluate(self, expression, frameIndex=0, threadId=None, context=None):
695        stackFrame = self.get_stackFrame(frameIndex=frameIndex, threadId=threadId)
696        if stackFrame is None:
697            return []
698        args_dict = {
699            "expression": expression,
700            "context": context,
701            "frameId": stackFrame["id"],
702        }
703        command_dict = {
704            "command": "evaluate",
705            "type": "request",
706            "arguments": args_dict,
707        }
708        return self.send_recv(command_dict)
709
710    def request_initialize(self, sourceInitFile):
711        command_dict = {
712            "command": "initialize",
713            "type": "request",
714            "arguments": {
715                "adapterID": "lldb-native",
716                "clientID": "vscode",
717                "columnsStartAt1": True,
718                "linesStartAt1": True,
719                "locale": "en-us",
720                "pathFormat": "path",
721                "supportsRunInTerminalRequest": True,
722                "supportsVariablePaging": True,
723                "supportsVariableType": True,
724                "supportsStartDebuggingRequest": True,
725                "sourceInitFile": sourceInitFile,
726            },
727        }
728        response = self.send_recv(command_dict)
729        if response:
730            if "body" in response:
731                self.initialize_body = response["body"]
732        return response
733
734    def request_launch(
735        self,
736        program,
737        args=None,
738        cwd=None,
739        env=None,
740        stopOnEntry=False,
741        disableASLR=True,
742        disableSTDIO=False,
743        shellExpandArguments=False,
744        trace=False,
745        initCommands=None,
746        preRunCommands=None,
747        stopCommands=None,
748        exitCommands=None,
749        terminateCommands=None,
750        sourcePath=None,
751        debuggerRoot=None,
752        launchCommands=None,
753        sourceMap=None,
754        runInTerminal=False,
755        postRunCommands=None,
756        enableAutoVariableSummaries=False,
757        enableSyntheticChildDebugging=False,
758        commandEscapePrefix=None,
759        customFrameFormat=None,
760        customThreadFormat=None,
761    ):
762        args_dict = {"program": program}
763        if args:
764            args_dict["args"] = args
765        if cwd:
766            args_dict["cwd"] = cwd
767        if env:
768            args_dict["env"] = env
769        if stopOnEntry:
770            args_dict["stopOnEntry"] = stopOnEntry
771        if disableASLR:
772            args_dict["disableASLR"] = disableASLR
773        if disableSTDIO:
774            args_dict["disableSTDIO"] = disableSTDIO
775        if shellExpandArguments:
776            args_dict["shellExpandArguments"] = shellExpandArguments
777        if trace:
778            args_dict["trace"] = trace
779        args_dict["initCommands"] = self.init_commands
780        if initCommands:
781            args_dict["initCommands"].extend(initCommands)
782        if preRunCommands:
783            args_dict["preRunCommands"] = preRunCommands
784        if stopCommands:
785            args_dict["stopCommands"] = stopCommands
786        if exitCommands:
787            args_dict["exitCommands"] = exitCommands
788        if terminateCommands:
789            args_dict["terminateCommands"] = terminateCommands
790        if sourcePath:
791            args_dict["sourcePath"] = sourcePath
792        if debuggerRoot:
793            args_dict["debuggerRoot"] = debuggerRoot
794        if launchCommands:
795            args_dict["launchCommands"] = launchCommands
796        if sourceMap:
797            args_dict["sourceMap"] = sourceMap
798        if runInTerminal:
799            args_dict["runInTerminal"] = runInTerminal
800        if postRunCommands:
801            args_dict["postRunCommands"] = postRunCommands
802        if customFrameFormat:
803            args_dict["customFrameFormat"] = customFrameFormat
804        if customThreadFormat:
805            args_dict["customThreadFormat"] = customThreadFormat
806
807        args_dict["enableAutoVariableSummaries"] = enableAutoVariableSummaries
808        args_dict["enableSyntheticChildDebugging"] = enableSyntheticChildDebugging
809        args_dict["commandEscapePrefix"] = commandEscapePrefix
810        command_dict = {"command": "launch", "type": "request", "arguments": args_dict}
811        response = self.send_recv(command_dict)
812
813        if response["success"]:
814            # Wait for a 'process' and 'initialized' event in any order
815            self.wait_for_event(filter=["process", "initialized"])
816            self.wait_for_event(filter=["process", "initialized"])
817        return response
818
819    def request_next(self, threadId, granularity="statement"):
820        if self.exit_status is not None:
821            raise ValueError("request_continue called after process exited")
822        args_dict = {"threadId": threadId, "granularity": granularity}
823        command_dict = {"command": "next", "type": "request", "arguments": args_dict}
824        return self.send_recv(command_dict)
825
826    def request_stepIn(self, threadId, targetId, granularity="statement"):
827        if self.exit_status is not None:
828            raise ValueError("request_stepIn called after process exited")
829        args_dict = {
830            "threadId": threadId,
831            "targetId": targetId,
832            "granularity": granularity,
833        }
834        command_dict = {"command": "stepIn", "type": "request", "arguments": args_dict}
835        return self.send_recv(command_dict)
836
837    def request_stepInTargets(self, frameId):
838        if self.exit_status is not None:
839            raise ValueError("request_stepInTargets called after process exited")
840        args_dict = {"frameId": frameId}
841        command_dict = {
842            "command": "stepInTargets",
843            "type": "request",
844            "arguments": args_dict,
845        }
846        return self.send_recv(command_dict)
847
848    def request_stepOut(self, threadId):
849        if self.exit_status is not None:
850            raise ValueError("request_stepOut called after process exited")
851        args_dict = {"threadId": threadId}
852        command_dict = {"command": "stepOut", "type": "request", "arguments": args_dict}
853        return self.send_recv(command_dict)
854
855    def request_pause(self, threadId=None):
856        if self.exit_status is not None:
857            raise ValueError("request_pause called after process exited")
858        if threadId is None:
859            threadId = self.get_thread_id()
860        args_dict = {"threadId": threadId}
861        command_dict = {"command": "pause", "type": "request", "arguments": args_dict}
862        return self.send_recv(command_dict)
863
864    def request_scopes(self, frameId):
865        args_dict = {"frameId": frameId}
866        command_dict = {"command": "scopes", "type": "request", "arguments": args_dict}
867        return self.send_recv(command_dict)
868
869    def request_setBreakpoints(self, file_path, line_array, data=None):
870        """data is array of parameters for breakpoints in line_array.
871        Each parameter object is 1:1 mapping with entries in line_entry.
872        It contains optional location/hitCondition/logMessage parameters.
873        """
874        (dir, base) = os.path.split(file_path)
875        source_dict = {"name": base, "path": file_path}
876        args_dict = {
877            "source": source_dict,
878            "sourceModified": False,
879        }
880        if line_array is not None:
881            args_dict["lines"] = "%s" % line_array
882            breakpoints = []
883            for i, line in enumerate(line_array):
884                breakpoint_data = None
885                if data is not None and i < len(data):
886                    breakpoint_data = data[i]
887                bp = {"line": line}
888                if breakpoint_data is not None:
889                    if "condition" in breakpoint_data and breakpoint_data["condition"]:
890                        bp["condition"] = breakpoint_data["condition"]
891                    if (
892                        "hitCondition" in breakpoint_data
893                        and breakpoint_data["hitCondition"]
894                    ):
895                        bp["hitCondition"] = breakpoint_data["hitCondition"]
896                    if (
897                        "logMessage" in breakpoint_data
898                        and breakpoint_data["logMessage"]
899                    ):
900                        bp["logMessage"] = breakpoint_data["logMessage"]
901                breakpoints.append(bp)
902            args_dict["breakpoints"] = breakpoints
903
904        command_dict = {
905            "command": "setBreakpoints",
906            "type": "request",
907            "arguments": args_dict,
908        }
909        return self.send_recv(command_dict)
910
911    def request_setExceptionBreakpoints(self, filters):
912        args_dict = {"filters": filters}
913        command_dict = {
914            "command": "setExceptionBreakpoints",
915            "type": "request",
916            "arguments": args_dict,
917        }
918        return self.send_recv(command_dict)
919
920    def request_setFunctionBreakpoints(self, names, condition=None, hitCondition=None):
921        breakpoints = []
922        for name in names:
923            bp = {"name": name}
924            if condition is not None:
925                bp["condition"] = condition
926            if hitCondition is not None:
927                bp["hitCondition"] = hitCondition
928            breakpoints.append(bp)
929        args_dict = {"breakpoints": breakpoints}
930        command_dict = {
931            "command": "setFunctionBreakpoints",
932            "type": "request",
933            "arguments": args_dict,
934        }
935        return self.send_recv(command_dict)
936
937    def request_dataBreakpointInfo(
938        self, variablesReference, name, frameIndex=0, threadId=None
939    ):
940        stackFrame = self.get_stackFrame(frameIndex=frameIndex, threadId=threadId)
941        if stackFrame is None:
942            return []
943        args_dict = {
944            "variablesReference": variablesReference,
945            "name": name,
946            "frameId": stackFrame["id"],
947        }
948        command_dict = {
949            "command": "dataBreakpointInfo",
950            "type": "request",
951            "arguments": args_dict,
952        }
953        return self.send_recv(command_dict)
954
955    def request_setDataBreakpoint(self, dataBreakpoints):
956        """dataBreakpoints is a list of dictionary with following fields:
957        {
958            dataId: (address in hex)/(size in bytes)
959            accessType: read/write/readWrite
960            [condition]: string
961            [hitCondition]: string
962        }
963        """
964        args_dict = {"breakpoints": dataBreakpoints}
965        command_dict = {
966            "command": "setDataBreakpoints",
967            "type": "request",
968            "arguments": args_dict,
969        }
970        return self.send_recv(command_dict)
971
972    def request_compileUnits(self, moduleId):
973        args_dict = {"moduleId": moduleId}
974        command_dict = {
975            "command": "compileUnits",
976            "type": "request",
977            "arguments": args_dict,
978        }
979        response = self.send_recv(command_dict)
980        return response
981
982    def request_completions(self, text, frameId=None):
983        args_dict = {"text": text, "column": len(text)}
984        if frameId:
985            args_dict["frameId"] = frameId
986        command_dict = {
987            "command": "completions",
988            "type": "request",
989            "arguments": args_dict,
990        }
991        return self.send_recv(command_dict)
992
993    def request_modules(self):
994        return self.send_recv({"command": "modules", "type": "request"})
995
996    def request_stackTrace(
997        self, threadId=None, startFrame=None, levels=None, dump=False
998    ):
999        if threadId is None:
1000            threadId = self.get_thread_id()
1001        args_dict = {"threadId": threadId}
1002        if startFrame is not None:
1003            args_dict["startFrame"] = startFrame
1004        if levels is not None:
1005            args_dict["levels"] = levels
1006        command_dict = {
1007            "command": "stackTrace",
1008            "type": "request",
1009            "arguments": args_dict,
1010        }
1011        response = self.send_recv(command_dict)
1012        if dump:
1013            for idx, frame in enumerate(response["body"]["stackFrames"]):
1014                name = frame["name"]
1015                if "line" in frame and "source" in frame:
1016                    source = frame["source"]
1017                    if "sourceReference" not in source:
1018                        if "name" in source:
1019                            source_name = source["name"]
1020                            line = frame["line"]
1021                            print("[%3u] %s @ %s:%u" % (idx, name, source_name, line))
1022                            continue
1023                print("[%3u] %s" % (idx, name))
1024        return response
1025
1026    def request_threads(self):
1027        """Request a list of all threads and combine any information from any
1028        "stopped" events since those contain more information about why a
1029        thread actually stopped. Returns an array of thread dictionaries
1030        with information about all threads"""
1031        command_dict = {"command": "threads", "type": "request", "arguments": {}}
1032        response = self.send_recv(command_dict)
1033        body = response["body"]
1034        # Fill in "self.threads" correctly so that clients that call
1035        # self.get_threads() or self.get_thread_id(...) can get information
1036        # on threads when the process is stopped.
1037        if "threads" in body:
1038            self.threads = body["threads"]
1039            for thread in self.threads:
1040                # Copy the thread dictionary so we can add key/value pairs to
1041                # it without affecting the original info from the "threads"
1042                # command.
1043                tid = thread["id"]
1044                if tid in self.thread_stop_reasons:
1045                    thread_stop_info = self.thread_stop_reasons[tid]
1046                    copy_keys = ["reason", "description", "text"]
1047                    for key in copy_keys:
1048                        if key in thread_stop_info:
1049                            thread[key] = thread_stop_info[key]
1050        else:
1051            self.threads = None
1052        return response
1053
1054    def request_variables(
1055        self, variablesReference, start=None, count=None, is_hex=None
1056    ):
1057        args_dict = {"variablesReference": variablesReference}
1058        if start is not None:
1059            args_dict["start"] = start
1060        if count is not None:
1061            args_dict["count"] = count
1062        if is_hex is not None:
1063            args_dict["format"] = {"hex": is_hex}
1064        command_dict = {
1065            "command": "variables",
1066            "type": "request",
1067            "arguments": args_dict,
1068        }
1069        return self.send_recv(command_dict)
1070
1071    def request_setVariable(self, containingVarRef, name, value, id=None):
1072        args_dict = {
1073            "variablesReference": containingVarRef,
1074            "name": name,
1075            "value": str(value),
1076        }
1077        if id is not None:
1078            args_dict["id"] = id
1079        command_dict = {
1080            "command": "setVariable",
1081            "type": "request",
1082            "arguments": args_dict,
1083        }
1084        return self.send_recv(command_dict)
1085
1086    def request_testGetTargetBreakpoints(self):
1087        """A request packet used in the LLDB test suite to get all currently
1088        set breakpoint infos for all breakpoints currently set in the
1089        target.
1090        """
1091        command_dict = {
1092            "command": "_testGetTargetBreakpoints",
1093            "type": "request",
1094            "arguments": {},
1095        }
1096        return self.send_recv(command_dict)
1097
1098    def terminate(self):
1099        self.send.close()
1100        # self.recv.close()
1101
1102
1103class DebugAdaptorServer(DebugCommunication):
1104    def __init__(
1105        self,
1106        executable=None,
1107        port=None,
1108        init_commands=[],
1109        log_file=None,
1110        env=None,
1111    ):
1112        self.process = None
1113        if executable is not None:
1114            adaptor_env = os.environ.copy()
1115            if env is not None:
1116                adaptor_env.update(env)
1117
1118            if log_file:
1119                adaptor_env["LLDBDAP_LOG"] = log_file
1120            self.process = subprocess.Popen(
1121                [executable],
1122                stdin=subprocess.PIPE,
1123                stdout=subprocess.PIPE,
1124                stderr=subprocess.PIPE,
1125                env=adaptor_env,
1126            )
1127            DebugCommunication.__init__(
1128                self, self.process.stdout, self.process.stdin, init_commands, log_file
1129            )
1130        elif port is not None:
1131            s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
1132            s.connect(("127.0.0.1", port))
1133            DebugCommunication.__init__(
1134                self, s.makefile("r"), s.makefile("w"), init_commands
1135            )
1136
1137    def get_pid(self):
1138        if self.process:
1139            return self.process.pid
1140        return -1
1141
1142    def terminate(self):
1143        super(DebugAdaptorServer, self).terminate()
1144        if self.process is not None:
1145            self.process.terminate()
1146            self.process.wait()
1147            self.process = None
1148
1149
1150def attach_options_specified(options):
1151    if options.pid is not None:
1152        return True
1153    if options.waitFor:
1154        return True
1155    if options.attach:
1156        return True
1157    if options.attachCmds:
1158        return True
1159    return False
1160
1161
1162def run_vscode(dbg, args, options):
1163    dbg.request_initialize(options.sourceInitFile)
1164    if attach_options_specified(options):
1165        response = dbg.request_attach(
1166            program=options.program,
1167            pid=options.pid,
1168            waitFor=options.waitFor,
1169            attachCommands=options.attachCmds,
1170            initCommands=options.initCmds,
1171            preRunCommands=options.preRunCmds,
1172            stopCommands=options.stopCmds,
1173            exitCommands=options.exitCmds,
1174            terminateCommands=options.terminateCmds,
1175        )
1176    else:
1177        response = dbg.request_launch(
1178            options.program,
1179            args=args,
1180            env=options.envs,
1181            cwd=options.workingDir,
1182            debuggerRoot=options.debuggerRoot,
1183            sourcePath=options.sourcePath,
1184            initCommands=options.initCmds,
1185            preRunCommands=options.preRunCmds,
1186            stopCommands=options.stopCmds,
1187            exitCommands=options.exitCmds,
1188            terminateCommands=options.terminateCmds,
1189        )
1190
1191    if response["success"]:
1192        if options.sourceBreakpoints:
1193            source_to_lines = {}
1194            for file_line in options.sourceBreakpoints:
1195                (path, line) = file_line.split(":")
1196                if len(path) == 0 or len(line) == 0:
1197                    print('error: invalid source with line "%s"' % (file_line))
1198
1199                else:
1200                    if path in source_to_lines:
1201                        source_to_lines[path].append(int(line))
1202                    else:
1203                        source_to_lines[path] = [int(line)]
1204            for source in source_to_lines:
1205                dbg.request_setBreakpoints(source, source_to_lines[source])
1206        if options.funcBreakpoints:
1207            dbg.request_setFunctionBreakpoints(options.funcBreakpoints)
1208        dbg.request_configurationDone()
1209        dbg.wait_for_stopped()
1210    else:
1211        if "message" in response:
1212            print(response["message"])
1213    dbg.request_disconnect(terminateDebuggee=True)
1214
1215
1216def main():
1217    parser = optparse.OptionParser(
1218        description=(
1219            "A testing framework for the Visual Studio Code Debug " "Adaptor protocol"
1220        )
1221    )
1222
1223    parser.add_option(
1224        "--vscode",
1225        type="string",
1226        dest="vscode_path",
1227        help=(
1228            "The path to the command line program that implements the "
1229            "Visual Studio Code Debug Adaptor protocol."
1230        ),
1231        default=None,
1232    )
1233
1234    parser.add_option(
1235        "--program",
1236        type="string",
1237        dest="program",
1238        help="The path to the program to debug.",
1239        default=None,
1240    )
1241
1242    parser.add_option(
1243        "--workingDir",
1244        type="string",
1245        dest="workingDir",
1246        default=None,
1247        help="Set the working directory for the process we launch.",
1248    )
1249
1250    parser.add_option(
1251        "--sourcePath",
1252        type="string",
1253        dest="sourcePath",
1254        default=None,
1255        help=(
1256            "Set the relative source root for any debug info that has "
1257            "relative paths in it."
1258        ),
1259    )
1260
1261    parser.add_option(
1262        "--debuggerRoot",
1263        type="string",
1264        dest="debuggerRoot",
1265        default=None,
1266        help=(
1267            "Set the working directory for lldb-dap for any object files "
1268            "with relative paths in the Mach-o debug map."
1269        ),
1270    )
1271
1272    parser.add_option(
1273        "-r",
1274        "--replay",
1275        type="string",
1276        dest="replay",
1277        help=(
1278            "Specify a file containing a packet log to replay with the "
1279            "current Visual Studio Code Debug Adaptor executable."
1280        ),
1281        default=None,
1282    )
1283
1284    parser.add_option(
1285        "-g",
1286        "--debug",
1287        action="store_true",
1288        dest="debug",
1289        default=False,
1290        help="Pause waiting for a debugger to attach to the debug adaptor",
1291    )
1292
1293    parser.add_option(
1294        "--sourceInitFile",
1295        action="store_true",
1296        dest="sourceInitFile",
1297        default=False,
1298        help="Whether lldb-dap should source .lldbinit file or not",
1299    )
1300
1301    parser.add_option(
1302        "--port",
1303        type="int",
1304        dest="port",
1305        help="Attach a socket to a port instead of using STDIN for VSCode",
1306        default=None,
1307    )
1308
1309    parser.add_option(
1310        "--pid",
1311        type="int",
1312        dest="pid",
1313        help="The process ID to attach to",
1314        default=None,
1315    )
1316
1317    parser.add_option(
1318        "--attach",
1319        action="store_true",
1320        dest="attach",
1321        default=False,
1322        help=(
1323            "Specify this option to attach to a process by name. The "
1324            "process name is the basename of the executable specified with "
1325            "the --program option."
1326        ),
1327    )
1328
1329    parser.add_option(
1330        "-f",
1331        "--function-bp",
1332        type="string",
1333        action="append",
1334        dest="funcBreakpoints",
1335        help=(
1336            "Specify the name of a function to break at. "
1337            "Can be specified more than once."
1338        ),
1339        default=[],
1340    )
1341
1342    parser.add_option(
1343        "-s",
1344        "--source-bp",
1345        type="string",
1346        action="append",
1347        dest="sourceBreakpoints",
1348        default=[],
1349        help=(
1350            "Specify source breakpoints to set in the format of "
1351            "<source>:<line>. "
1352            "Can be specified more than once."
1353        ),
1354    )
1355
1356    parser.add_option(
1357        "--attachCommand",
1358        type="string",
1359        action="append",
1360        dest="attachCmds",
1361        default=[],
1362        help=(
1363            "Specify a LLDB command that will attach to a process. "
1364            "Can be specified more than once."
1365        ),
1366    )
1367
1368    parser.add_option(
1369        "--initCommand",
1370        type="string",
1371        action="append",
1372        dest="initCmds",
1373        default=[],
1374        help=(
1375            "Specify a LLDB command that will be executed before the target "
1376            "is created. Can be specified more than once."
1377        ),
1378    )
1379
1380    parser.add_option(
1381        "--preRunCommand",
1382        type="string",
1383        action="append",
1384        dest="preRunCmds",
1385        default=[],
1386        help=(
1387            "Specify a LLDB command that will be executed after the target "
1388            "has been created. Can be specified more than once."
1389        ),
1390    )
1391
1392    parser.add_option(
1393        "--stopCommand",
1394        type="string",
1395        action="append",
1396        dest="stopCmds",
1397        default=[],
1398        help=(
1399            "Specify a LLDB command that will be executed each time the"
1400            "process stops. Can be specified more than once."
1401        ),
1402    )
1403
1404    parser.add_option(
1405        "--exitCommand",
1406        type="string",
1407        action="append",
1408        dest="exitCmds",
1409        default=[],
1410        help=(
1411            "Specify a LLDB command that will be executed when the process "
1412            "exits. Can be specified more than once."
1413        ),
1414    )
1415
1416    parser.add_option(
1417        "--terminateCommand",
1418        type="string",
1419        action="append",
1420        dest="terminateCmds",
1421        default=[],
1422        help=(
1423            "Specify a LLDB command that will be executed when the debugging "
1424            "session is terminated. Can be specified more than once."
1425        ),
1426    )
1427
1428    parser.add_option(
1429        "--env",
1430        type="string",
1431        action="append",
1432        dest="envs",
1433        default=[],
1434        help=("Specify environment variables to pass to the launched " "process."),
1435    )
1436
1437    parser.add_option(
1438        "--waitFor",
1439        action="store_true",
1440        dest="waitFor",
1441        default=False,
1442        help=(
1443            "Wait for the next process to be launched whose name matches "
1444            "the basename of the program specified with the --program "
1445            "option"
1446        ),
1447    )
1448
1449    (options, args) = parser.parse_args(sys.argv[1:])
1450
1451    if options.vscode_path is None and options.port is None:
1452        print(
1453            "error: must either specify a path to a Visual Studio Code "
1454            "Debug Adaptor vscode executable path using the --vscode "
1455            "option, or a port to attach to for an existing lldb-dap "
1456            "using the --port option"
1457        )
1458        return
1459    dbg = DebugAdaptorServer(executable=options.vscode_path, port=options.port)
1460    if options.debug:
1461        raw_input('Waiting for debugger to attach pid "%i"' % (dbg.get_pid()))
1462    if options.replay:
1463        dbg.replay_packets(options.replay)
1464    else:
1465        run_vscode(dbg, args, options)
1466    dbg.terminate()
1467
1468
1469if __name__ == "__main__":
1470    main()
1471