brintos

brintos / llvm-project-archived public Read only

0
0
Text · 41.3 KiB · 68ca50a Raw
957 lines · python
1# DExTer : Debugging Experience Tester2# ~~~~~~   ~         ~~         ~   ~~3#4# Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.5# See https://llvm.org/LICENSE.txt for license information.6# SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception7"""Interface for communicating with a debugger via the DAP.8"""9 10import abc11from collections import defaultdict12import copy13import json14import os15import shlex16import subprocess17import sys18import threading19import time20from enum import Enum21 22from dex.debugger.DebuggerBase import DebuggerBase, watch_is_active23from dex.dextIR import FrameIR, LocIR, StepIR, StopReason, ValueIR24from dex.dextIR import StackFrame, SourceLocation, ProgramState25from dex.utils.Exceptions import DebuggerException, LoadDebuggerException26from dex.utils.ReturnCode import ReturnCode27from dex.utils.Logging import Logger28from dex.utils.Timeout import Timeout29 30 31# Helper enum used for colorizing DAP Message Log output.32class Color(Enum):33    CYAN = 3634    GREEN = 3235    YELLOW = 3336    RED = 3137    MAGENTA = 3538 39    def apply(self, text: str) -> str:40        return f"\033[{self.value}m{text}\033[0m"41 42 43class DAPMessageLogger:44    def __init__(self, context):45        self.dexter_logger = context.logger46        self.log_file: str = context.options.dap_message_log47        self.colorized: bool = context.options.colorize_dap_log48        self.indent = 2 if context.options.format_dap_log == "pretty" else None49        self.prefix_send: str = "->"50        self.prefix_recv: str = "<-"51        self.out_handle = None52        self.open = False53        self.lock = threading.Lock()54 55    def _custom_enter(self):56        self.open = True57        if self.log_file is None:58            return59        if self.log_file == "-":60            self.out_handle = sys.stdout61            return62        if self.log_file == "-e":63            self.out_handle = sys.stderr64            return65        self.out_handle = open(self.log_file, "w+", encoding="utf-8")66 67    def _custom_exit(self):68        if (69            self.out_handle is not None70            and self.log_file != "-"71            and self.log_file != "-e"72        ):73            self.out_handle.close()74        self.open = False75 76    def _colorize_dap_message(self, message: dict) -> dict:77        if not self.colorized:78            return message79        colorized_message = copy.deepcopy(message)80        if colorized_message["type"] == "event":81            colorized_message["type"] = Color.YELLOW.apply("event")82            colorized_message["event"] = Color.YELLOW.apply(colorized_message["event"])83        elif colorized_message["type"] == "response":84            colorized_message["type"] = Color.GREEN.apply("response")85            colorized_message["command"] = Color.YELLOW.apply(86                colorized_message["command"]87            )88        elif colorized_message["type"] == "request":89            colorized_message["type"] = Color.CYAN.apply("request")90            colorized_message["command"] = Color.YELLOW.apply(91                colorized_message["command"]92            )93        return colorized_message94 95    def write_message(self, message: dict, incoming: bool):96        prefix = self.prefix_recv if incoming else self.prefix_send97        # ANSI escape codes get butchered by json.dumps(), so we fix them up here.98        message_str = json.dumps(99            self._colorize_dap_message(message), indent=self.indent100        ).replace("\\u001b", "\033")101        if self.out_handle is not None and self.open:102            with self.lock:103                self.out_handle.write(f"{prefix} {message_str}\n")104        elif not self.open:105            self.dexter_logger.warning(106                f'Attempted to write message after program closed: "{prefix} {message_str}"'107            )108 109 110# Debuggers communicate optional feature support.111class DAPDebuggerCapabilities:112    def __init__(self):113        self.supportsConfigurationDoneRequest: bool = False114        self.supportsFunctionBreakpoints: bool = False115        self.supportsConditionalBreakpoints: bool = False116        self.supportsHitConditionalBreakpoints: bool = False117        self.supportsEvaluateForHovers: bool = False118        self.supportsSetVariable: bool = False119        self.supportsStepInTargetsRequest: bool = False120        self.supportsModulesRequest: bool = False121        self.supportsValueFormattingOptions: bool = False122        self.supportsLogPoints: bool = False123        self.supportsSetExpression: bool = False124        self.supportsDataBreakpoints: bool = False125        self.supportsReadMemoryRequest: bool = False126        self.supportsWriteMemoryRequest: bool = False127        self.supportsDisassembleRequest: bool = False128        self.supportsCancelRequest: bool = False129        self.supportsSteppingGranularity: bool = False130        self.supportsInstructionBreakpoints: bool = False131 132    def update(self, logger: Logger, feature_dict: dict):133        for k, v in feature_dict.items():134            if hasattr(self, k):135                setattr(self, k, v)136            else:137                logger.warning(f"DAP: Unknown support flag: {k}")138 139 140# As DAP does not give us a trivially query-able process, we are responsible for maintaining our own state information,141# including what breakpoints are currently set, and whether the debugger is running or stopped.142# This class holds all state that is set based on events sent by the debug adapter; most responses are forwarded through143# to the main DAP class, though in a few cases where it is convenient for bookkeeping the DAPDebuggerState may read some144# information from the responses before forwarding them onwards.145class DAPDebuggerState:146    def __init__(self):147        ## Overall debugger state information.148        #149        # Whether we have received the initialize update yet.150        self.initialized: bool = False151        # Whether the debugger has successfully launched yet.152        self.launched: bool = False153        # The thread that we are debugging.154        # TODO: This is primitively handled right now, assuming that we only ever have one thread; if we want155        # support for debugging any multi-threaded program then we will need to track some more complex state.156        self.thread = None157        # True if the debuggee is currently executing.158        self.is_running: bool = False159        # True if the debuggee has finished executing.160        self.is_finished: bool = False161 162        ## Information for the program at a particular stopped point, which will be invalidated when execution resumes.163        #164        # Either None if the debuggee is currently running, or a string specifying the reason why the165        # debuggee is currently stopped otherwise.166        self.stopped_reason = None167        # If we were stopped for the reason 'breakpoint', this will contain a list of the DAP breakpoint IDs168        # responsible for stopping us.169        self.stopped_bps = []170        # For a currently stopped process, stores the mapping of frame indices (top of stack=0) to frameIds returned171        # from the debug adapter.172        self.frame_map = []173 174        # We use responses[idx] to refer to the response for the request sent with seq=idx, where the value175        # is either the response payload, or None if the response hasn't arrived yet.176        # Since requests are indexed from 1, we insert a 'None' at the front to ensure that the first real177        # entry is indexed correctly.178        self.responses = [None]179        # Map of DAP breakpoint IDs to resolved instruction addresses.180        self.bp_addr_map = {}181 182        # DAP features supported by the debugger.183        self.capabilities = DAPDebuggerCapabilities()184 185    def set_response(self, req_id: int, response: dict):186        if len(self.responses) > req_id:187            self.responses[req_id] = response188            return189        while len(self.responses) < req_id:190            self.responses.append(None)191        self.responses.append(response)192 193    # As the receiver thread does not know when a request has been sent, and only the receiver thread should write to the DebuggerState object,194    # the responses list may not have been populated with a None for a pending request at the time that the main thread expects it. Therefore,195    # we use this getter to account for requests that the receiver thread is unaware of.196    def get_response(self, req_id: int):197        if len(self.responses) <= req_id:198            return None199        return self.responses[req_id]200 201 202# DAP Communication model:203# - Communication is message-based, not stateful - we cannot simply query information from the debugger as we can with204#   other debugger implementations, we need to maintain local state.205# - All messages are utf-encoded JSON, which we convert to/from python dicts via methods above; some amount of206#   bookkeeping is performed automatically in the DAP class.207# - Commands and queries are sent via 'request' messages, for which a corresponding 'response' will always be sent back208#   by the adapter indicating success/failure, containing data related to the request.209# - The adapter will also send 'event' messages, indicating state changes in the debugger - for example, when the210#   debugger has stopped at a breakpoint.211# In order to handle this, we run a separate thread that will continuously insert any messages received212# from the adapter into a queue, which the main thread will read; generally, our response to any read message213# is to update our state, which Dexter's DebuggerController will then read.214class DAP(DebuggerBase, metaclass=abc.ABCMeta):215    def __init__(self, context, *args):216        self._debugger_state = DAPDebuggerState()217        self._proc = None218        self._receiver_thread = None219        self._err_thread = None220        self.seq = 0221        self.target_proc_id = -1222        self.max_bp_id = 0223        # Mapping of active breakpoints per-file - intentionally excludes breakpoints that we have deleted.224        # { file -> [dex_breakpoint_id]}225        self.file_to_bp = defaultdict(list)226        # { dex_breakpoint_id -> (file, line, condition) }227        self.bp_info = {}228        # { dex_breakpoint_id -> function_name }229        self.function_bp_info = {}230        # { dex_breakpoint_id -> instruction_reference }231        self.instruction_bp_info = {}232        # We don't rely on IDs returned directly from the debug adapter. Instead, we use dexter breakpoint IDs, and233        # maintain a two-way-mapping of dex_bp_id<->dap_bp_id. This also allows us to defer the setting of breakpoints234        # in the debug adapter itself until necessary.235        # NB: The debug adapter may merge dexter-side breakpoints into a single debugger-side breakpoint; therefore, the236        # DAP->Dex mapping is one-to-many.237        self.dex_id_to_dap_id = {}238        self.dap_id_to_dex_ids = {}239        self.pending_breakpoints: bool = False240        self.pending_function_breakpoints: bool = False241        self.pending_instruction_breakpoints: bool = False242        # List of breakpoints, indexed by BP ID243        # Each entry has the source file (for use in referencing desired_bps), and the DA-assigned244        # ID for that breakpoint if it has one (if it has been removed or not yet created then it will be None).245        # self.bp_source_list: list[(str, int)]246        self.message_logger = None247        super(DAP, self).__init__(context, *args)248 249    @property250    @abc.abstractmethod251    def _debug_adapter_name(self) -> str:252        pass253 254    @property255    @abc.abstractmethod256    def _debug_adapter_executable(self) -> str:257        pass258 259    @property260    def _debug_adapter_launch_args(self) -> list:261        return []262 263    @staticmethod264    def make_request(command: str, arguments=None) -> dict:265        request = {"type": "request", "command": command}266        if arguments is not None:267            request["arguments"] = arguments268        return request269 270    @staticmethod271    def make_initialize_request(adapterID: str) -> dict:272        return DAP.make_request(273            "initialize",274            {275                "clientID": "dexter",276                "adapterID": adapterID,277                "pathFormat": "path",278                "linesStartAt1": True,279                "columnsStartAt1": True,280                "supportsVariableType": True,281                "supportsVariablePaging": True,282                "supportsRunInTerminalRequest": False,283            },284        )285 286    class BreakpointRequest:287        def __init__(self, line: int, condition=None):288            self.line = line289            self.condition = condition290 291        def toDict(self) -> dict:292            result = {"line": self.line}293            if self.condition is not None:294                result["condition"] = self.condition295            return result296 297    @staticmethod298    def make_set_breakpoint_request(source: str, bps) -> dict:299        return DAP.make_request(300            "setBreakpoints",301            {"source": {"path": source}, "breakpoints": [bp.toDict() for bp in bps]},302        )303 304    @staticmethod305    def make_set_function_breakpoint_request(function_names: list) -> dict:306        # Function breakpoints may specify conditions and hit counts, though we307        # don't use those here (though perhaps we should use native hit count,308        # rather than emulating it ConditionalController, now that we have a309        # shared interface (DAP)).310        return DAP.make_request(311            "setFunctionBreakpoints",312            {"breakpoints": [{"name": f} for f in function_names]},313        )314 315    @staticmethod316    def make_set_instruction_breakpoint_request(addrs: list) -> dict:317        # Instruction breakpoints have additional fields we're ignoring for the318        # moment.319        return DAP.make_request(320            "setInstructionBreakpoints",321            {"breakpoints": [{"instructionReference": a} for a in addrs]},322        )323 324    ############################################################################325    ## DAP communication & state-handling functions326 327    # Sends a request to the adapter, returning the seq value of the request.328    def send_message(self, payload: dict) -> int:329        self.seq = self.seq + 1330        payload["seq"] = self.seq331        self.message_logger.write_message(payload, False)332        body = json.dumps(payload)333        message = f"Content-Length: {len(body)}\r\n\r\n{body}".encode("utf-8")334        self._proc.stdin.write(message)335        self._proc.stdin.flush()336        return self.seq337 338    @staticmethod339    def _handle_message(340        message: dict, debugger_state: DAPDebuggerState, logger: Logger341    ):342        # We only support events and responses, we do not implement any reverse-requests.343        # TODO: If we find cases where 'seq' becomes important, we need to read it here and process344        # pending messages in order.345        if message["type"] == "event":346            event_type = message["event"]347            event_details = message.get("body")348            if event_type == "initialized":349                debugger_state.initialized = True350            elif event_type == "process":351                debugger_state.launched = True352                debugger_state.is_running = True353            # The debugger has stopped for some reason.354            elif event_type == "stopped":355                stop_reason = event_details["reason"]356                debugger_state.is_running = False357                debugger_state.stopped_reason = stop_reason358                debugger_state.stopped_bps = event_details.get("hitBreakpointIds", [])359                debugger_state.thread = event_details["threadId"]360            elif event_type == "breakpoint":361                # We handle most BP information in the main DAP thread by reading responses to breakpoint requests;362                # some information is only passed via event, however, which we store here.363                breakpoint_details = event_details["breakpoint"]364                if "instructionReference" in breakpoint_details:365                    debugger_state.bp_addr_map[366                        breakpoint_details["id"]367                    ] = breakpoint_details["instructionReference"]368            elif event_type == "exited" or event_type == "terminated":369                debugger_state.stopped_reason = event_type370                debugger_state.is_running = False371                debugger_state.is_finished = True372            # We may receive this event before or after the response to the corresponding "continue" request.373            elif event_type == "continued":374                debugger_state.is_running = True375                # Reset all state that is invalidated upon program continue.376                debugger_state.stopped_reason = None377                debugger_state.stopped_bps = []378                debugger_state.frame_map = []379            elif event_type == "thread":380                if (381                    event_details["reason"] == "started"382                    and debugger_state.thread is None383                ):384                    debugger_state.thread = event_details["threadId"]385            elif event_type == "capabilities":386                # Unchanged capabilites may not be included.387                debugger_state.capabilities.update(logger, event_details)388            # There are many events we do not care about, just skip processing them.389            else:390                pass391        elif message["type"] == "response":392            # TODO: We also receive a "continued" event, but it seems reasonable to set state based on either the393            # response or the event, since the DAP does not specify an order in which they are sent. May need revisiting394            # if there turns out to be some odd ordering issues, e.g. if we can receive messages in the order395            # ["response: continued", "event: stopped", "event: continued"].396            if (397                message["command"] in ["continue", "stepIn", "next", "stepOut"]398                and message["success"] == True399            ):400                debugger_state.is_running = True401                # Reset all state that is invalidated upon program continue.402                debugger_state.stopped_reason = None403                debugger_state.stopped_bps = []404                debugger_state.frame_map = []405            # It is useful to cache a mapping of frames; since this is invalidated when we continue, and only this406            # message-handling thread should write to debugger_state, we do so while handling the response for407            # convenience.408            if message["command"] == "stackTrace" and message["success"] == True:409                debugger_state.frame_map = [410                    stackframe["id"] for stackframe in message["body"]["stackFrames"]411                ]412            # The debugger communicates which optional DAP features are413            # supported in its initalize response.414            if message["command"] == "initialize" and message["success"] == True:415                body = message.get("body")416                if body:417                    debugger_state.capabilities.update(logger, body)418            # Now we've done whatever we need to do with the response, tell the419            # receiver thread we've got it.420            request_seq = message["request_seq"]421            debugger_state.set_response(request_seq, message)422 423    @staticmethod424    def _colorize_dap_message(message: dict) -> dict:425        colorized_message = copy.deepcopy(message)426        if colorized_message["type"] == "event":427            colorized_message["type"] = "<y>event</>"428            colorized_message["event"] = f"<y>{colorized_message['event']}</>"429        elif colorized_message["type"] == "response":430            colorized_message["type"] = "<g>response</>"431            colorized_message["command"] = f"<y>{colorized_message['command']}</>"432        elif colorized_message["type"] == "request":433            colorized_message["type"] = "<b>request</>"434            colorized_message["command"] = f"<y>{colorized_message['command']}</>"435        return colorized_message436 437    @staticmethod438    def _read_dap_output(439        proc: subprocess.Popen,440        debugger_state: DAPDebuggerState,441        message_logger: DAPMessageLogger,442        logger: Logger,443    ):444        buffer: bytes = b""445        while True:446            chunk: bytes = proc.stdout.read(1)447            if not chunk:448                break449            buffer += chunk450            if b"\r\n\r\n" in buffer:451                header, rest = buffer.split(b"\r\n\r\n", 1)452                content_length = int(header.decode().split(":")[1].strip())453                while len(rest) < content_length:454                    rest += proc.stdout.read(content_length - len(rest))455                message = json.loads(rest[:content_length])456                message_logger.write_message(message, True)457                DAP._handle_message(message, debugger_state, logger)458                buffer = rest[content_length:]459 460    @staticmethod461    def _read_dap_err(proc: subprocess.Popen, logger: Logger):462        while True:463            err: bytes = proc.stderr.readline()464            if len(err) > 0:465                logger.error(f"DAP server: {err.decode().strip()}")466 467    def _custom_init(self):468        self.context.logger.note(469            f"Opening DAP server: {shlex.join([self._debug_adapter_executable] + self._debug_adapter_launch_args)}"470        )471        self.message_logger = DAPMessageLogger(self.context)472        self.message_logger._custom_enter()473        self._proc = subprocess.Popen(474            [self._debug_adapter_executable] + self._debug_adapter_launch_args,475            stdin=subprocess.PIPE,476            stdout=subprocess.PIPE,477            stderr=subprocess.PIPE,478            bufsize=0,479        )480        self._receiver_thread = threading.Thread(481            target=DAP._read_dap_output,482            args=(483                self._proc,484                self._debugger_state,485                self.message_logger,486                self.context.logger,487            ),488            daemon=True,489        )490        self._err_thread = threading.Thread(491            target=DAP._read_dap_err,492            args=(self._proc, self.context.logger),493            daemon=True,494        )495        self._receiver_thread.start()496        self._err_thread.start()497        init_req = self.send_message(498            self.make_initialize_request(self._debug_adapter_name)499        )500        assert self._proc.poll() is None, "Process has closed unexpectedly early?"501        self._await_response(init_req)502 503    def _custom_exit(self):504        if self._proc is not None:505            dc_req = self.send_message(self.make_request("disconnect"))506            dc_req_timeout = 3507            try:508                result = self._await_response(dc_req, dc_req_timeout)509                if not result["success"]:510                    self.context.logger.warning(511                        "The disconnect request sent to the DAP server failed; forcibly shutting down DAP server."512                    )513                else:514                    self.context.logger.note(515                        "Successfully disconnected from DAP server."516                    )517            except:518                # We're going to kill the process regardless, we just want to give the target a chance to shut down519                # gracefully first.520                self.context.logger.warning(521                    f"The disconnect request sent to the DAP server timed out after {dc_req_timeout}s; forcibly shutting down DAP server."522                )523                pass524            self._proc.kill()525            self._proc = None526        self.message_logger._custom_exit()527 528    # Waits for a response to the request with the given seq, optionally raising an error529    # if the response takes too long (blocks forever by default/if timeout=0).530    def _await_response(self, seq: int, timeout: float = 0.0) -> dict:531        timeout_check = Timeout(timeout)532        while self._debugger_state.get_response(seq) is None:533            if timeout_check.timed_out():534                if self._proc.poll() is not None:535                    self.context.logger.error(536                        f"Debug adapter exited while Dexter is awaiting response? Result: {self._proc.poll()}"537                    )538                raise TimeoutError(539                    f"Timed out while waiting for response to DAP request {seq}"540                )541            time.sleep(0.001)542        return self._debugger_state.get_response(seq)543 544    ## End of DAP communication methods545    ############################################################################546 547    def _translate_stop_reason(self, reason):548        if reason is None:549            return None550        if "breakpoint" in reason:551            return StopReason.BREAKPOINT552        if reason == "step":553            return StopReason.STEP554        if reason == "exited" or reason == "terminated":555            return StopReason.PROGRAM_EXIT556        if reason == "exception":557            return StopReason.ERROR558        return StopReason.OTHER559 560    def _load_interface(self):561        if not os.path.isfile(self._debug_adapter_executable):562            raise LoadDebuggerException(563                f'debug adapter "{self._debug_adapter_executable}" does not exist',564                sys.exc_info(),565            )566        # We don't make use of _interface, so return nothing.567 568    @property569    @abc.abstractmethod570    def version(self):571        """The version of this DAP debugger."""572 573    ############################################################################574    ## Breakpoint Methods575 576    def get_next_bp_id(self):577        new_id = self.max_bp_id578        self.max_bp_id += 1579        return new_id580 581    def get_current_bps(self, source):582        if source in self.file_to_bp:583            return self.file_to_bp[source]584        return []585 586    def _update_requested_bp_list(self, bp_list):587        """Can be overridden for any specific implementations that need further processing before sending breakpoints to588        the debug adapter, e.g. in LLDB we cannot store multiple breakpoints at a single location, and therefore must589        combine conditions for breakpoints at the same location."""590        return bp_list591 592    # For a source file, returns the list of BreakpointRequests for the breakpoints in that file, which can be sent to593    # the debug adapter.594    def _get_desired_bps(self, file: str):595        bp_list = [596            DAP.BreakpointRequest(line, cond)597            for (_, line, cond) in map(598                lambda dex_bp_id: self.bp_info[dex_bp_id], self.get_current_bps(file)599            )600        ]601        return self._update_requested_bp_list(bp_list)602 603    def clear_breakpoints(self):604        # We don't actually need to do anything here - even if breakpoints were preserved between runs, we will605        # automatically clear old breakpoints on the first 'setBreakpoints' message.606        pass607 608    def _add_breakpoint(self, file, line):609        return self._add_conditional_breakpoint(file, line, None)610 611    def add_function_breakpoint(self, name: str):612        if not self._debugger_state.capabilities.supportsFunctionBreakpoints:613            raise DebuggerException("Debugger does not support function breakpoints")614        new_id = self.get_next_bp_id()615        self.function_bp_info[new_id] = name616        self.pending_function_breakpoints = True617        return new_id618 619    def add_instruction_breakpoint(self, addr: str):620        if not self._debugger_state.capabilities.supportsInstructionBreakpoints:621            raise DebuggerException("Debugger does not support instruction breakpoints")622        new_id = self.get_next_bp_id()623        self.instruction_bp_info[new_id] = addr624        self.pending_instruction_breakpoints = True625        return new_id626 627    def _add_conditional_breakpoint(self, file, line, condition):628        new_id = self.get_next_bp_id()629        self.file_to_bp[file].append(new_id)630        self.bp_info[new_id] = (file, line, condition)631        self.pending_breakpoints = True632        return new_id633 634    def _update_breakpoint_ids_after_request(self, dex_bp_ids: list, response: dict):635        dap_bp_ids = [bp["id"] for bp in response["body"]["breakpoints"]]636        if len(dex_bp_ids) != len(dap_bp_ids):637            self.context.logger.error(638                f"Sent request to set {len(dex_bp_ids)} breakpoints, but received {len(dap_bp_ids)} in response."639            )640        visited_dap_ids = set()641        for i, dex_bp_id in enumerate(dex_bp_ids):642            dap_bp_id = dap_bp_ids[i]643            self.dex_id_to_dap_id[dex_bp_id] = dap_bp_id644            # We take the mappings in the response as the canonical mapping, meaning that if the debug server has645            # simply *changed* the DAP ID for a breakpoint we overwrite the existing mapping rather than adding to646            # it, but if we receive the same DAP ID for multiple Dex IDs *then* we store a one-to-many mapping.647            if dap_bp_id in visited_dap_ids:648                self.dap_id_to_dex_ids[dap_bp_id].append(dex_bp_id)649            else:650                self.dap_id_to_dex_ids[dap_bp_id] = [dex_bp_id]651                visited_dap_ids.add(dap_bp_id)652 653    def _flush_breakpoints(self):654        # Normal and conditional breakpoints.655        if self.pending_breakpoints:656            self.pending_breakpoints = False657            for file in self.file_to_bp.keys():658                desired_bps = self._get_desired_bps(file)659                request_id = self.send_message(660                    self.make_set_breakpoint_request(file, desired_bps)661                )662                result = self._await_response(request_id, 10)663                if not result["success"]:664                    raise DebuggerException(f"could not set breakpoints for '{file}'")665                # The debug adapter may have chosen to merge our breakpoints. From here we need to identify such cases and666                # handle them so that our internal bookkeeping is correct.667                dex_bp_ids = self.get_current_bps(file)668                self._update_breakpoint_ids_after_request(dex_bp_ids, result)669 670        # Function breakpoints.671        if self.pending_function_breakpoints:672            self.pending_function_breakpoints = False673            desired_bps = list(self.function_bp_info.values())674            request_id = self.send_message(675                self.make_set_function_breakpoint_request(desired_bps)676            )677            result = self._await_response(request_id, 10)678            if not result["success"]:679                raise DebuggerException(680                    f"could not set function breakpoints: '{desired_bps}'"681                )682            # We expect the breakpoint order to match in request and response.683            dex_bp_ids = list(self.function_bp_info.keys())684            self._update_breakpoint_ids_after_request(dex_bp_ids, result)685 686        # Address / instruction breakpoints.687        if self.pending_instruction_breakpoints:688            self.pending_instruction_breakpoints = False689            desired_bps = list(self.instruction_bp_info.values())690            request_id = self.send_message(691                self.make_set_instruction_breakpoint_request(desired_bps)692            )693            result = self._await_response(request_id, 10)694            if not result["success"]:695                raise DebuggerException(696                    f"could not set instruction breakpoints: '{desired_bps}'"697                )698            # We expect the breakpoint order to match in request and response.699            dex_bp_ids = list(self.instruction_bp_info.keys())700            self._update_breakpoint_ids_after_request(dex_bp_ids, result)701 702    def _confirm_triggered_breakpoint_ids(self, dex_bp_ids):703        """Can be overridden for any specific implementations that need further processing from the debug server's704        reported 'hitBreakpointIds', e.g. in LLDB where we the ID for every breakpoint at the current PC, even if some705        are conditional and their condition is not met."""706        return dex_bp_ids707 708    def get_triggered_breakpoint_ids(self):709        # Breakpoints can only have been triggered if we've hit one.710        stop_reason = self._translate_stop_reason(self._debugger_state.stopped_reason)711        if stop_reason != StopReason.BREAKPOINT:712            return set()713        breakpoint_ids = set(714            [715                dex_id716                for dap_id in self._debugger_state.stopped_bps717                if dap_id in self.dap_id_to_dex_ids718                for dex_id in self.dap_id_to_dex_ids[dap_id]719            ]720        )721        return self._confirm_triggered_breakpoint_ids(breakpoint_ids)722 723    def delete_breakpoints(self, ids):724        per_file_deletions = defaultdict(list)725        for dex_bp_id in ids:726            if dex_bp_id in self.bp_info:727                source, _, _ = self.bp_info[dex_bp_id]728                per_file_deletions[source].append(dex_bp_id)729            elif dex_bp_id in self.function_bp_info:730                del self.function_bp_info[dex_bp_id]731                self.pending_function_breakpoints = True732            elif dex_bp_id in self.instruction_bp_info:733                del self.instruction_bp_info[dex_bp_id]734                self.pending_instruction_breakpoints = True735 736        for file, deleted_ids in per_file_deletions.items():737            old_len = len(self.file_to_bp[file])738            self.file_to_bp[file] = [739                bp_id for bp_id in self.file_to_bp[file] if bp_id not in deleted_ids740            ]741            if len(self.file_to_bp[file]) != old_len:742                self.pending_breakpoints = True743 744    ## End of breakpoint methods745    ############################################################################746 747    @classmethod748    @abc.abstractmethod749    def _get_launch_params(self, cmdline):750        """ "Set the debugger-specific params used in a launch request."""751 752    def launch(self, cmdline):753        # FIXME: Should this be a warning or exception, rather than assert?754        assert (755            len(self.file_to_bp)756            + len(self.function_bp_info)757            + len(self.instruction_bp_info)758            > 0759        ), "Expected at least one breakpoint before launching"760 761        if self.context.options.target_run_args:762            cmdline += shlex.split(self.context.options.target_run_args)763 764        launch_request = self._get_launch_params(cmdline)765 766        # Per DAP protocol, the correct sequence is:767        # 1. Send launch request768        # 2. Wait for launch response and "initialized" event769        # 3. Set breakpoints770        # 4. Send configurationDone to start the process771        launch_req_id = self.send_message(self.make_request("launch", launch_request))772        launch_response = self._await_response(launch_req_id)773        if not launch_response["success"]:774            raise DebuggerException(775                f"failure launching debugger: \"{launch_response['body']['error']['format']}\""776            )777 778        # Set breakpoints after receiving launch response but before configurationDone.779        self._flush_breakpoints()780 781        # Send configurationDone to allow the process to start running.782        config_done_req_id = self.send_message(self.make_request("configurationDone"))783        config_done_response = self._await_response(config_done_req_id)784        assert config_done_response["success"]785 786        # Wait for the process to launch and obtain a thread ID.787        while self._debugger_state.thread is None or not self._debugger_state.launched:788            time.sleep(0.001)789 790    # LLDB has unique stepping behaviour w.r.t. breakpoints that needs to be handled after completing a step, so we use791    # an overridable hook to enable debugger-specific behaviour.792    def _post_step_hook(self):793        """Hook to be executed after completing a step request."""794 795    def _step(self, step_request_string):796        self._flush_breakpoints()797        step_req_id = self.send_message(798            self.make_request(799                step_request_string, {"threadId": self._debugger_state.thread}800            )801        )802        response = self._await_response(step_req_id)803        if not response["success"]:804            raise DebuggerException(805                f"failed to perform debugger action: '{step_request_string}'"806            )807        # If we've "stepped" to a breakpoint, then continue to hit the breakpoint properly.808        # NB: This is an issue that only seems relevant to LLDB, but is also harmless outside of LLDB; if it turns out809        #     to cause issues for other debuggers, we can move it to a post-step hook.810        while self._debugger_state.is_running:811            time.sleep(0.001)812        self._post_step_hook()813 814    def step_in(self):815        self._step("stepIn")816 817    def step_next(self):818        self._step("next")819 820    def step_out(self):821        self._step("stepOut")822 823    def go(self) -> ReturnCode:824        self._flush_breakpoints()825        continue_req_id = self.send_message(826            self.make_request("continue", {"threadId": self._debugger_state.thread})827        )828        response = self._await_response(continue_req_id)829        if not response["success"]:830            raise DebuggerException("failed to continue")831        # Assuming the request to continue succeeded, we still need to wait to receive an event back from the debugger832        # indicating that we have successfully resumed.833 834    def _get_step_info(self, watches, step_index):835        assert (836            not self._debugger_state.is_running837        ), "Cannot get step info while debugger is running!"838        trace_req_id = self.send_message(839            self.make_request("stackTrace", {"threadId": self._debugger_state.thread})840        )841        trace_response = self._await_response(trace_req_id)842        if not trace_response["success"]:843            raise DebuggerException("failed to get stack frames")844        stackframes = trace_response["body"]["stackFrames"]845 846        frames = []847        state_frames = []848 849        for idx, stackframe in enumerate(stackframes):850            # FIXME: No source, skip the frame! Currently I've only observed this for frames below main, so we break851            # here; if it happens elsewhere, then this will break more stuff and we'll come up with a better solution.852            if (853                stackframe.get("source") is None854                or stackframe["source"].get("path") is None855            ):856                break857            loc_dict = {858                "path": stackframe["source"]["path"],859                "lineno": stackframe["line"],860                "column": stackframe["column"],861            }862            loc = LocIR(**loc_dict)863            valid_loc_for_watch = loc.path and os.path.exists(loc.path)864            frame = FrameIR(865                function=self._sanitize_function_name(stackframe["name"]),866                is_inlined=stackframe["name"].startswith("[Inline Frame]"),867                loc=loc,868            )869 870            # We skip frames that are below "main", since we do not expect those to be user code.871            fname = frame.function or ""  # pylint: disable=no-member872            if any(name in fname for name in self.frames_below_main):873                break874 875            frames.append(frame)876 877            state_frame = StackFrame(878                function=frame.function,879                is_inlined=frame.is_inlined,880                location=SourceLocation(**loc_dict),881                watches={},882            )883            if valid_loc_for_watch:884                for expr in map(885                    # Filter out watches that are not active in the current frame,886                    # and then evaluate all the active watches.887                    lambda watch_info, idx=idx: self.evaluate_expression(888                        watch_info.expression, idx889                    ),890                    filter(891                        lambda watch_info, idx=idx, line_no=loc.lineno, loc_path=loc.path: watch_is_active(892                            watch_info, loc_path, idx, line_no893                        ),894                        watches,895                    ),896                ):897                    state_frame.watches[expr.expression] = expr898            state_frames.append(state_frame)899 900        if len(frames) == 1 and frames[0].function is None:901            frames = []902            state_frames = []903 904        reason = self._translate_stop_reason(self._debugger_state.stopped_reason)905 906        return StepIR(907            step_index=step_index,908            frames=frames,909            stop_reason=reason,910            program_state=ProgramState(state_frames),911        )912 913    @property914    def is_running(self):915        return self._debugger_state.is_running916 917    @property918    def is_finished(self):919        return self._debugger_state.is_finished920 921    @property922    def frames_below_main(self):923        pass924 925    @staticmethod926    @abc.abstractmethod927    def _evaluate_result_value(expression: str, result_string: str) -> ValueIR:928        """For the result of an "evaluate" message, return a ValueIR. Implementation must be debugger-specific."""929 930    def evaluate_expression(self, expression, frame_idx=0) -> ValueIR:931        # The frame_idx passed in here needs to be translated to the debug adapter's internal frame ID.932        dap_frame_id = self._debugger_state.frame_map[frame_idx]933        eval_req_id = self.send_message(934            self.make_request(935                "evaluate",936                {937                    "expression": expression,938                    "frameId": dap_frame_id,939                    "context": "watch",940                },941            )942        )943        eval_response = self._await_response(eval_req_id)944        result: str = ""945        if not eval_response["success"]:946            if eval_response["body"].get("error", None):947                result = eval_response["body"]["error"]["format"]948            elif eval_response["message"]:949                result = eval_response["message"]950            else:951                result = "<unable to evaluate expression>"952        else:953            result = eval_response["body"]["result"]954        type_str = eval_response["body"].get("type")955 956        return self._evaluate_result_value(expression, result, type_str)957