xref: /llvm-project/lldb/packages/Python/lldbsuite/test/tools/lldb-dap/dap_server.py (revision 5b4100cc354148a1140546e7f5ac2bf380bc5eff)
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_exceptionInfo(self, threadId=None):
711        if threadId is None:
712            threadId = self.get_thread_id()
713        args_dict = {"threadId": threadId}
714        command_dict = {
715            "command": "exceptionInfo",
716            "type": "request",
717            "arguments": args_dict,
718        }
719        return self.send_recv(command_dict)
720
721    def request_initialize(self, sourceInitFile):
722        command_dict = {
723            "command": "initialize",
724            "type": "request",
725            "arguments": {
726                "adapterID": "lldb-native",
727                "clientID": "vscode",
728                "columnsStartAt1": True,
729                "linesStartAt1": True,
730                "locale": "en-us",
731                "pathFormat": "path",
732                "supportsRunInTerminalRequest": True,
733                "supportsVariablePaging": True,
734                "supportsVariableType": True,
735                "supportsStartDebuggingRequest": True,
736                "sourceInitFile": sourceInitFile,
737            },
738        }
739        response = self.send_recv(command_dict)
740        if response:
741            if "body" in response:
742                self.initialize_body = response["body"]
743        return response
744
745    def request_launch(
746        self,
747        program,
748        args=None,
749        cwd=None,
750        env=None,
751        stopOnEntry=False,
752        disableASLR=True,
753        disableSTDIO=False,
754        shellExpandArguments=False,
755        trace=False,
756        initCommands=None,
757        preRunCommands=None,
758        stopCommands=None,
759        exitCommands=None,
760        terminateCommands=None,
761        sourcePath=None,
762        debuggerRoot=None,
763        launchCommands=None,
764        sourceMap=None,
765        runInTerminal=False,
766        postRunCommands=None,
767        enableAutoVariableSummaries=False,
768        enableDisplayExtendedBacktrace=False,
769        enableSyntheticChildDebugging=False,
770        commandEscapePrefix=None,
771        customFrameFormat=None,
772        customThreadFormat=None,
773    ):
774        args_dict = {"program": program}
775        if args:
776            args_dict["args"] = args
777        if cwd:
778            args_dict["cwd"] = cwd
779        if env:
780            args_dict["env"] = env
781        if stopOnEntry:
782            args_dict["stopOnEntry"] = stopOnEntry
783        if disableASLR:
784            args_dict["disableASLR"] = disableASLR
785        if disableSTDIO:
786            args_dict["disableSTDIO"] = disableSTDIO
787        if shellExpandArguments:
788            args_dict["shellExpandArguments"] = shellExpandArguments
789        if trace:
790            args_dict["trace"] = trace
791        args_dict["initCommands"] = self.init_commands
792        if initCommands:
793            args_dict["initCommands"].extend(initCommands)
794        if preRunCommands:
795            args_dict["preRunCommands"] = preRunCommands
796        if stopCommands:
797            args_dict["stopCommands"] = stopCommands
798        if exitCommands:
799            args_dict["exitCommands"] = exitCommands
800        if terminateCommands:
801            args_dict["terminateCommands"] = terminateCommands
802        if sourcePath:
803            args_dict["sourcePath"] = sourcePath
804        if debuggerRoot:
805            args_dict["debuggerRoot"] = debuggerRoot
806        if launchCommands:
807            args_dict["launchCommands"] = launchCommands
808        if sourceMap:
809            args_dict["sourceMap"] = sourceMap
810        if runInTerminal:
811            args_dict["runInTerminal"] = runInTerminal
812        if postRunCommands:
813            args_dict["postRunCommands"] = postRunCommands
814        if customFrameFormat:
815            args_dict["customFrameFormat"] = customFrameFormat
816        if customThreadFormat:
817            args_dict["customThreadFormat"] = customThreadFormat
818
819        args_dict["enableAutoVariableSummaries"] = enableAutoVariableSummaries
820        args_dict["enableSyntheticChildDebugging"] = enableSyntheticChildDebugging
821        args_dict["enableDisplayExtendedBacktrace"] = enableDisplayExtendedBacktrace
822        args_dict["commandEscapePrefix"] = commandEscapePrefix
823        command_dict = {"command": "launch", "type": "request", "arguments": args_dict}
824        response = self.send_recv(command_dict)
825
826        if response["success"]:
827            # Wait for a 'process' and 'initialized' event in any order
828            self.wait_for_event(filter=["process", "initialized"])
829            self.wait_for_event(filter=["process", "initialized"])
830        return response
831
832    def request_next(self, threadId, granularity="statement"):
833        if self.exit_status is not None:
834            raise ValueError("request_continue called after process exited")
835        args_dict = {"threadId": threadId, "granularity": granularity}
836        command_dict = {"command": "next", "type": "request", "arguments": args_dict}
837        return self.send_recv(command_dict)
838
839    def request_stepIn(self, threadId, targetId, granularity="statement"):
840        if self.exit_status is not None:
841            raise ValueError("request_stepIn called after process exited")
842        args_dict = {
843            "threadId": threadId,
844            "targetId": targetId,
845            "granularity": granularity,
846        }
847        command_dict = {"command": "stepIn", "type": "request", "arguments": args_dict}
848        return self.send_recv(command_dict)
849
850    def request_stepInTargets(self, frameId):
851        if self.exit_status is not None:
852            raise ValueError("request_stepInTargets called after process exited")
853        args_dict = {"frameId": frameId}
854        command_dict = {
855            "command": "stepInTargets",
856            "type": "request",
857            "arguments": args_dict,
858        }
859        return self.send_recv(command_dict)
860
861    def request_stepOut(self, threadId):
862        if self.exit_status is not None:
863            raise ValueError("request_stepOut called after process exited")
864        args_dict = {"threadId": threadId}
865        command_dict = {"command": "stepOut", "type": "request", "arguments": args_dict}
866        return self.send_recv(command_dict)
867
868    def request_pause(self, threadId=None):
869        if self.exit_status is not None:
870            raise ValueError("request_pause called after process exited")
871        if threadId is None:
872            threadId = self.get_thread_id()
873        args_dict = {"threadId": threadId}
874        command_dict = {"command": "pause", "type": "request", "arguments": args_dict}
875        return self.send_recv(command_dict)
876
877    def request_scopes(self, frameId):
878        args_dict = {"frameId": frameId}
879        command_dict = {"command": "scopes", "type": "request", "arguments": args_dict}
880        return self.send_recv(command_dict)
881
882    def request_setBreakpoints(self, file_path, line_array, data=None):
883        """data is array of parameters for breakpoints in line_array.
884        Each parameter object is 1:1 mapping with entries in line_entry.
885        It contains optional location/hitCondition/logMessage parameters.
886        """
887        (dir, base) = os.path.split(file_path)
888        source_dict = {"name": base, "path": file_path}
889        args_dict = {
890            "source": source_dict,
891            "sourceModified": False,
892        }
893        if line_array is not None:
894            args_dict["lines"] = "%s" % line_array
895            breakpoints = []
896            for i, line in enumerate(line_array):
897                breakpoint_data = None
898                if data is not None and i < len(data):
899                    breakpoint_data = data[i]
900                bp = {"line": line}
901                if breakpoint_data is not None:
902                    if "condition" in breakpoint_data and breakpoint_data["condition"]:
903                        bp["condition"] = breakpoint_data["condition"]
904                    if (
905                        "hitCondition" in breakpoint_data
906                        and breakpoint_data["hitCondition"]
907                    ):
908                        bp["hitCondition"] = breakpoint_data["hitCondition"]
909                    if (
910                        "logMessage" in breakpoint_data
911                        and breakpoint_data["logMessage"]
912                    ):
913                        bp["logMessage"] = breakpoint_data["logMessage"]
914                breakpoints.append(bp)
915            args_dict["breakpoints"] = breakpoints
916
917        command_dict = {
918            "command": "setBreakpoints",
919            "type": "request",
920            "arguments": args_dict,
921        }
922        return self.send_recv(command_dict)
923
924    def request_setExceptionBreakpoints(self, filters):
925        args_dict = {"filters": filters}
926        command_dict = {
927            "command": "setExceptionBreakpoints",
928            "type": "request",
929            "arguments": args_dict,
930        }
931        return self.send_recv(command_dict)
932
933    def request_setFunctionBreakpoints(self, names, condition=None, hitCondition=None):
934        breakpoints = []
935        for name in names:
936            bp = {"name": name}
937            if condition is not None:
938                bp["condition"] = condition
939            if hitCondition is not None:
940                bp["hitCondition"] = hitCondition
941            breakpoints.append(bp)
942        args_dict = {"breakpoints": breakpoints}
943        command_dict = {
944            "command": "setFunctionBreakpoints",
945            "type": "request",
946            "arguments": args_dict,
947        }
948        return self.send_recv(command_dict)
949
950    def request_dataBreakpointInfo(
951        self, variablesReference, name, frameIndex=0, threadId=None
952    ):
953        stackFrame = self.get_stackFrame(frameIndex=frameIndex, threadId=threadId)
954        if stackFrame is None:
955            return []
956        args_dict = {
957            "variablesReference": variablesReference,
958            "name": name,
959            "frameId": stackFrame["id"],
960        }
961        command_dict = {
962            "command": "dataBreakpointInfo",
963            "type": "request",
964            "arguments": args_dict,
965        }
966        return self.send_recv(command_dict)
967
968    def request_setDataBreakpoint(self, dataBreakpoints):
969        """dataBreakpoints is a list of dictionary with following fields:
970        {
971            dataId: (address in hex)/(size in bytes)
972            accessType: read/write/readWrite
973            [condition]: string
974            [hitCondition]: string
975        }
976        """
977        args_dict = {"breakpoints": dataBreakpoints}
978        command_dict = {
979            "command": "setDataBreakpoints",
980            "type": "request",
981            "arguments": args_dict,
982        }
983        return self.send_recv(command_dict)
984
985    def request_compileUnits(self, moduleId):
986        args_dict = {"moduleId": moduleId}
987        command_dict = {
988            "command": "compileUnits",
989            "type": "request",
990            "arguments": args_dict,
991        }
992        response = self.send_recv(command_dict)
993        return response
994
995    def request_completions(self, text, frameId=None):
996        args_dict = {"text": text, "column": len(text)}
997        if frameId:
998            args_dict["frameId"] = frameId
999        command_dict = {
1000            "command": "completions",
1001            "type": "request",
1002            "arguments": args_dict,
1003        }
1004        return self.send_recv(command_dict)
1005
1006    def request_modules(self):
1007        return self.send_recv({"command": "modules", "type": "request"})
1008
1009    def request_stackTrace(
1010        self, threadId=None, startFrame=None, levels=None, dump=False
1011    ):
1012        if threadId is None:
1013            threadId = self.get_thread_id()
1014        args_dict = {"threadId": threadId}
1015        if startFrame is not None:
1016            args_dict["startFrame"] = startFrame
1017        if levels is not None:
1018            args_dict["levels"] = levels
1019        command_dict = {
1020            "command": "stackTrace",
1021            "type": "request",
1022            "arguments": args_dict,
1023        }
1024        response = self.send_recv(command_dict)
1025        if dump:
1026            for idx, frame in enumerate(response["body"]["stackFrames"]):
1027                name = frame["name"]
1028                if "line" in frame and "source" in frame:
1029                    source = frame["source"]
1030                    if "sourceReference" not in source:
1031                        if "name" in source:
1032                            source_name = source["name"]
1033                            line = frame["line"]
1034                            print("[%3u] %s @ %s:%u" % (idx, name, source_name, line))
1035                            continue
1036                print("[%3u] %s" % (idx, name))
1037        return response
1038
1039    def request_threads(self):
1040        """Request a list of all threads and combine any information from any
1041        "stopped" events since those contain more information about why a
1042        thread actually stopped. Returns an array of thread dictionaries
1043        with information about all threads"""
1044        command_dict = {"command": "threads", "type": "request", "arguments": {}}
1045        response = self.send_recv(command_dict)
1046        body = response["body"]
1047        # Fill in "self.threads" correctly so that clients that call
1048        # self.get_threads() or self.get_thread_id(...) can get information
1049        # on threads when the process is stopped.
1050        if "threads" in body:
1051            self.threads = body["threads"]
1052            for thread in self.threads:
1053                # Copy the thread dictionary so we can add key/value pairs to
1054                # it without affecting the original info from the "threads"
1055                # command.
1056                tid = thread["id"]
1057                if tid in self.thread_stop_reasons:
1058                    thread_stop_info = self.thread_stop_reasons[tid]
1059                    copy_keys = ["reason", "description", "text"]
1060                    for key in copy_keys:
1061                        if key in thread_stop_info:
1062                            thread[key] = thread_stop_info[key]
1063        else:
1064            self.threads = None
1065        return response
1066
1067    def request_variables(
1068        self, variablesReference, start=None, count=None, is_hex=None
1069    ):
1070        args_dict = {"variablesReference": variablesReference}
1071        if start is not None:
1072            args_dict["start"] = start
1073        if count is not None:
1074            args_dict["count"] = count
1075        if is_hex is not None:
1076            args_dict["format"] = {"hex": is_hex}
1077        command_dict = {
1078            "command": "variables",
1079            "type": "request",
1080            "arguments": args_dict,
1081        }
1082        return self.send_recv(command_dict)
1083
1084    def request_setVariable(self, containingVarRef, name, value, id=None):
1085        args_dict = {
1086            "variablesReference": containingVarRef,
1087            "name": name,
1088            "value": str(value),
1089        }
1090        if id is not None:
1091            args_dict["id"] = id
1092        command_dict = {
1093            "command": "setVariable",
1094            "type": "request",
1095            "arguments": args_dict,
1096        }
1097        return self.send_recv(command_dict)
1098
1099    def request_testGetTargetBreakpoints(self):
1100        """A request packet used in the LLDB test suite to get all currently
1101        set breakpoint infos for all breakpoints currently set in the
1102        target.
1103        """
1104        command_dict = {
1105            "command": "_testGetTargetBreakpoints",
1106            "type": "request",
1107            "arguments": {},
1108        }
1109        return self.send_recv(command_dict)
1110
1111    def terminate(self):
1112        self.send.close()
1113        # self.recv.close()
1114
1115    def request_setInstructionBreakpoints(self, memory_reference=[]):
1116        breakpoints = []
1117        for i in memory_reference:
1118            args_dict = {
1119                "instructionReference": i,
1120            }
1121            breakpoints.append(args_dict)
1122        args_dict = {"breakpoints": breakpoints}
1123        command_dict = {
1124            "command": "setInstructionBreakpoints",
1125            "type": "request",
1126            "arguments": args_dict,
1127        }
1128        return self.send_recv(command_dict)
1129
1130class DebugAdaptorServer(DebugCommunication):
1131    def __init__(
1132        self,
1133        executable=None,
1134        port=None,
1135        init_commands=[],
1136        log_file=None,
1137        env=None,
1138    ):
1139        self.process = None
1140        if executable is not None:
1141            adaptor_env = os.environ.copy()
1142            if env is not None:
1143                adaptor_env.update(env)
1144
1145            if log_file:
1146                adaptor_env["LLDBDAP_LOG"] = log_file
1147            self.process = subprocess.Popen(
1148                [executable],
1149                stdin=subprocess.PIPE,
1150                stdout=subprocess.PIPE,
1151                stderr=subprocess.PIPE,
1152                env=adaptor_env,
1153            )
1154            DebugCommunication.__init__(
1155                self, self.process.stdout, self.process.stdin, init_commands, log_file
1156            )
1157        elif port is not None:
1158            s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
1159            s.connect(("127.0.0.1", port))
1160            DebugCommunication.__init__(
1161                self, s.makefile("r"), s.makefile("w"), init_commands
1162            )
1163
1164    def get_pid(self):
1165        if self.process:
1166            return self.process.pid
1167        return -1
1168
1169    def terminate(self):
1170        super(DebugAdaptorServer, self).terminate()
1171        if self.process is not None:
1172            self.process.terminate()
1173            self.process.wait()
1174            self.process = None
1175
1176
1177def attach_options_specified(options):
1178    if options.pid is not None:
1179        return True
1180    if options.waitFor:
1181        return True
1182    if options.attach:
1183        return True
1184    if options.attachCmds:
1185        return True
1186    return False
1187
1188
1189def run_vscode(dbg, args, options):
1190    dbg.request_initialize(options.sourceInitFile)
1191    if attach_options_specified(options):
1192        response = dbg.request_attach(
1193            program=options.program,
1194            pid=options.pid,
1195            waitFor=options.waitFor,
1196            attachCommands=options.attachCmds,
1197            initCommands=options.initCmds,
1198            preRunCommands=options.preRunCmds,
1199            stopCommands=options.stopCmds,
1200            exitCommands=options.exitCmds,
1201            terminateCommands=options.terminateCmds,
1202        )
1203    else:
1204        response = dbg.request_launch(
1205            options.program,
1206            args=args,
1207            env=options.envs,
1208            cwd=options.workingDir,
1209            debuggerRoot=options.debuggerRoot,
1210            sourcePath=options.sourcePath,
1211            initCommands=options.initCmds,
1212            preRunCommands=options.preRunCmds,
1213            stopCommands=options.stopCmds,
1214            exitCommands=options.exitCmds,
1215            terminateCommands=options.terminateCmds,
1216        )
1217
1218    if response["success"]:
1219        if options.sourceBreakpoints:
1220            source_to_lines = {}
1221            for file_line in options.sourceBreakpoints:
1222                (path, line) = file_line.split(":")
1223                if len(path) == 0 or len(line) == 0:
1224                    print('error: invalid source with line "%s"' % (file_line))
1225
1226                else:
1227                    if path in source_to_lines:
1228                        source_to_lines[path].append(int(line))
1229                    else:
1230                        source_to_lines[path] = [int(line)]
1231            for source in source_to_lines:
1232                dbg.request_setBreakpoints(source, source_to_lines[source])
1233        if options.funcBreakpoints:
1234            dbg.request_setFunctionBreakpoints(options.funcBreakpoints)
1235        dbg.request_configurationDone()
1236        dbg.wait_for_stopped()
1237    else:
1238        if "message" in response:
1239            print(response["message"])
1240    dbg.request_disconnect(terminateDebuggee=True)
1241
1242
1243def main():
1244    parser = optparse.OptionParser(
1245        description=(
1246            "A testing framework for the Visual Studio Code Debug " "Adaptor protocol"
1247        )
1248    )
1249
1250    parser.add_option(
1251        "--vscode",
1252        type="string",
1253        dest="vscode_path",
1254        help=(
1255            "The path to the command line program that implements the "
1256            "Visual Studio Code Debug Adaptor protocol."
1257        ),
1258        default=None,
1259    )
1260
1261    parser.add_option(
1262        "--program",
1263        type="string",
1264        dest="program",
1265        help="The path to the program to debug.",
1266        default=None,
1267    )
1268
1269    parser.add_option(
1270        "--workingDir",
1271        type="string",
1272        dest="workingDir",
1273        default=None,
1274        help="Set the working directory for the process we launch.",
1275    )
1276
1277    parser.add_option(
1278        "--sourcePath",
1279        type="string",
1280        dest="sourcePath",
1281        default=None,
1282        help=(
1283            "Set the relative source root for any debug info that has "
1284            "relative paths in it."
1285        ),
1286    )
1287
1288    parser.add_option(
1289        "--debuggerRoot",
1290        type="string",
1291        dest="debuggerRoot",
1292        default=None,
1293        help=(
1294            "Set the working directory for lldb-dap for any object files "
1295            "with relative paths in the Mach-o debug map."
1296        ),
1297    )
1298
1299    parser.add_option(
1300        "-r",
1301        "--replay",
1302        type="string",
1303        dest="replay",
1304        help=(
1305            "Specify a file containing a packet log to replay with the "
1306            "current Visual Studio Code Debug Adaptor executable."
1307        ),
1308        default=None,
1309    )
1310
1311    parser.add_option(
1312        "-g",
1313        "--debug",
1314        action="store_true",
1315        dest="debug",
1316        default=False,
1317        help="Pause waiting for a debugger to attach to the debug adaptor",
1318    )
1319
1320    parser.add_option(
1321        "--sourceInitFile",
1322        action="store_true",
1323        dest="sourceInitFile",
1324        default=False,
1325        help="Whether lldb-dap should source .lldbinit file or not",
1326    )
1327
1328    parser.add_option(
1329        "--port",
1330        type="int",
1331        dest="port",
1332        help="Attach a socket to a port instead of using STDIN for VSCode",
1333        default=None,
1334    )
1335
1336    parser.add_option(
1337        "--pid",
1338        type="int",
1339        dest="pid",
1340        help="The process ID to attach to",
1341        default=None,
1342    )
1343
1344    parser.add_option(
1345        "--attach",
1346        action="store_true",
1347        dest="attach",
1348        default=False,
1349        help=(
1350            "Specify this option to attach to a process by name. The "
1351            "process name is the basename of the executable specified with "
1352            "the --program option."
1353        ),
1354    )
1355
1356    parser.add_option(
1357        "-f",
1358        "--function-bp",
1359        type="string",
1360        action="append",
1361        dest="funcBreakpoints",
1362        help=(
1363            "Specify the name of a function to break at. "
1364            "Can be specified more than once."
1365        ),
1366        default=[],
1367    )
1368
1369    parser.add_option(
1370        "-s",
1371        "--source-bp",
1372        type="string",
1373        action="append",
1374        dest="sourceBreakpoints",
1375        default=[],
1376        help=(
1377            "Specify source breakpoints to set in the format of "
1378            "<source>:<line>. "
1379            "Can be specified more than once."
1380        ),
1381    )
1382
1383    parser.add_option(
1384        "--attachCommand",
1385        type="string",
1386        action="append",
1387        dest="attachCmds",
1388        default=[],
1389        help=(
1390            "Specify a LLDB command that will attach to a process. "
1391            "Can be specified more than once."
1392        ),
1393    )
1394
1395    parser.add_option(
1396        "--initCommand",
1397        type="string",
1398        action="append",
1399        dest="initCmds",
1400        default=[],
1401        help=(
1402            "Specify a LLDB command that will be executed before the target "
1403            "is created. Can be specified more than once."
1404        ),
1405    )
1406
1407    parser.add_option(
1408        "--preRunCommand",
1409        type="string",
1410        action="append",
1411        dest="preRunCmds",
1412        default=[],
1413        help=(
1414            "Specify a LLDB command that will be executed after the target "
1415            "has been created. Can be specified more than once."
1416        ),
1417    )
1418
1419    parser.add_option(
1420        "--stopCommand",
1421        type="string",
1422        action="append",
1423        dest="stopCmds",
1424        default=[],
1425        help=(
1426            "Specify a LLDB command that will be executed each time the"
1427            "process stops. Can be specified more than once."
1428        ),
1429    )
1430
1431    parser.add_option(
1432        "--exitCommand",
1433        type="string",
1434        action="append",
1435        dest="exitCmds",
1436        default=[],
1437        help=(
1438            "Specify a LLDB command that will be executed when the process "
1439            "exits. Can be specified more than once."
1440        ),
1441    )
1442
1443    parser.add_option(
1444        "--terminateCommand",
1445        type="string",
1446        action="append",
1447        dest="terminateCmds",
1448        default=[],
1449        help=(
1450            "Specify a LLDB command that will be executed when the debugging "
1451            "session is terminated. Can be specified more than once."
1452        ),
1453    )
1454
1455    parser.add_option(
1456        "--env",
1457        type="string",
1458        action="append",
1459        dest="envs",
1460        default=[],
1461        help=("Specify environment variables to pass to the launched " "process."),
1462    )
1463
1464    parser.add_option(
1465        "--waitFor",
1466        action="store_true",
1467        dest="waitFor",
1468        default=False,
1469        help=(
1470            "Wait for the next process to be launched whose name matches "
1471            "the basename of the program specified with the --program "
1472            "option"
1473        ),
1474    )
1475
1476    (options, args) = parser.parse_args(sys.argv[1:])
1477
1478    if options.vscode_path is None and options.port is None:
1479        print(
1480            "error: must either specify a path to a Visual Studio Code "
1481            "Debug Adaptor vscode executable path using the --vscode "
1482            "option, or a port to attach to for an existing lldb-dap "
1483            "using the --port option"
1484        )
1485        return
1486    dbg = DebugAdaptorServer(executable=options.vscode_path, port=options.port)
1487    if options.debug:
1488        raw_input('Waiting for debugger to attach pid "%i"' % (dbg.get_pid()))
1489    if options.replay:
1490        dbg.replay_packets(options.replay)
1491    else:
1492        run_vscode(dbg, args, options)
1493    dbg.terminate()
1494
1495
1496if __name__ == "__main__":
1497    main()
1498