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