brintos

brintos / llvm-project-archived public Read only

0
0
Text · 15.7 KiB · 1a1c8e9 Raw
449 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 the Visual Studio debugger via DTE."""8 9import abc10import os11import sys12from enum import IntEnum13from pathlib import PurePath, Path14from collections import defaultdict, namedtuple15 16from dex.command.CommandBase import StepExpectInfo17from dex.debugger.DebuggerBase import DebuggerBase, watch_is_active18from dex.dextIR import FrameIR, LocIR, StepIR, StopReason, ValueIR19from dex.dextIR import StackFrame, SourceLocation, ProgramState20from dex.utils.Exceptions import Error, LoadDebuggerException21from dex.utils.Imports import load_module22from dex.utils.ReturnCode import ReturnCode23 24def _load_com_module():25    try:26        return load_module(27            "ComInterface",28            os.path.join(os.path.dirname(__file__), "windows"),29            "ComInterface.py",30        )31    except ImportError as e:32        raise LoadDebuggerException(e, sys.exc_info())33 34 35# VSBreakpoint(path: PurePath, line: int, col: int, cond: str).  This is enough36# info to identify breakpoint equivalence in visual studio based on the37# properties we set through dexter currently.38VSBreakpoint = namedtuple("VSBreakpoint", "path, line, col, cond")39 40 41# Visual Studio events.42# https://learn.microsoft.com/en-us/dotnet/api/envdte.dbgeventreason?view=visualstudiosdk-202243class DbgEvent(IntEnum):44    dbgEventReasonNone = 145    dbgEventReasonGo = 246    dbgEventReasonAttachProgram = 347    dbgEventReasonDetachProgram = 448    dbgEventReasonLaunchProgram = 549    dbgEventReasonEndProgram = 650    dbgEventReasonStopDebugging = 751    dbgEventReasonStep = 852    dbgEventReasonBreakpoint = 953    dbgEventReasonExceptionThrown = 1054    dbgEventReasonExceptionNotHandled = 1155    dbgEventReasonUserBreak = 1256    dbgEventReasonContextSwitch = 1357 58    first = dbgEventReasonNone59    last = dbgEventReasonContextSwitch60 61class VisualStudio(62    DebuggerBase, metaclass=abc.ABCMeta63):  # pylint: disable=abstract-method64    # Constants for results of Debugger.CurrentMode65    # (https://msdn.microsoft.com/en-us/library/envdte.debugger.currentmode.aspx)66    dbgDesignMode = 167    dbgBreakMode = 268    dbgRunMode = 369 70    def __init__(self, *args):71        self.com_module = None72        self._debugger = None73        self._solution = None74        self._fn_step = None75        self._fn_go = None76        # The next available unique breakpoint id. Use self._get_next_id().77        self._next_bp_id = 078        # VisualStudio appears to common identical breakpoints. That is, if you79        # ask for a breakpoint that already exists the Breakpoints list will80        # not grow. DebuggerBase requires all breakpoints have a unique id,81        # even for duplicates, so we'll need to do some bookkeeping.  Map82        # {VSBreakpoint: list(id)} where id is the unique dexter-side id for83        # the requested breakpoint.84        self._vs_to_dex_ids = defaultdict(list)85        # Map {id: VSBreakpoint} where id is unique and VSBreakpoint identifies86        # a breakpoint in Visual Studio. There may be many ids mapped to a87        # single VSBreakpoint. Use self._vs_to_dex_ids to find (dexter)88        # breakpoints mapped to the same visual studio breakpoint.89        self._dex_id_to_vs = {}90 91        super(VisualStudio, self).__init__(*args)92 93    def _create_solution(self):94        self._solution.Create(self.context.working_directory.path, "DexterSolution")95        try:96            self._solution.AddFromFile(self._project_file)97        except OSError:98            raise LoadDebuggerException(99                "could not debug the specified executable", sys.exc_info()100            )101 102    def _load_solution(self):103        try:104            self._solution.Open(self.context.options.vs_solution)105        except:106            raise LoadDebuggerException(107                "could not load specified vs solution at {}".format(108                    self.context.options.vs_solution109                ),110                sys.exc_info(),111            )112 113    def _custom_init(self):114        try:115            self._debugger = self._interface.Debugger116            self._debugger.HexDisplayMode = False117 118            self._interface.MainWindow.Visible = self.context.options.show_debugger119 120            self._solution = self._interface.Solution121            if self.context.options.vs_solution is None:122                self._create_solution()123            else:124                self._load_solution()125 126            self._fn_step = self._debugger.StepInto127            self._fn_go = self._debugger.Go128 129        except AttributeError as e:130            raise LoadDebuggerException(str(e), sys.exc_info())131 132    def _custom_exit(self):133        if self._interface:134            self._interface.Quit()135 136    @property137    def _project_file(self):138        return self.context.options.executable139 140    @abc.abstractproperty141    def _dte_version(self):142        pass143 144    @property145    def _location(self):146        # TODO: Find a better way of determining path, line and column info147        # that doesn't require reading break points. This method requires148        # all lines to have a break point on them.149        bp = self._debugger.BreakpointLastHit150        return {151            "path": getattr(bp, "File", None),152            "lineno": getattr(bp, "FileLine", None),153            "column": getattr(bp, "FileColumn", None),154        }155 156    @property157    def _mode(self):158        return self._debugger.CurrentMode159 160    def _load_interface(self):161        self.com_module = _load_com_module()162        return self.com_module.DTE(self._dte_version)163 164    @property165    def version(self):166        try:167            return self._interface.Version168        except AttributeError:169            return None170 171    def clear_breakpoints(self):172        for bp in self._debugger.Breakpoints:173            bp.Delete()174        self._vs_to_dex_ids.clear()175        self._dex_id_to_vs.clear()176 177    def _add_breakpoint(self, file_, line):178        return self._add_conditional_breakpoint(file_, line, "")179 180    def _get_next_id(self):181        # "Generate" a new unique id for the breakpoint.182        id = self._next_bp_id183        self._next_bp_id += 1184        return id185 186    def _add_conditional_breakpoint(self, file_, line, condition):187        col = 1188        vsbp = VSBreakpoint(PurePath(file_), line, col, condition)189        new_id = self._get_next_id()190 191        # Do we have an exact matching breakpoint already?192        if vsbp in self._vs_to_dex_ids:193            self._vs_to_dex_ids[vsbp].append(new_id)194            self._dex_id_to_vs[new_id] = vsbp195            return new_id196 197        # Breakpoint doesn't exist already. Add it now.198        count_before = self._debugger.Breakpoints.Count199        self._debugger.Breakpoints.Add("", file_, line, col, condition)200        # Our internal representation of VS says that the breakpoint doesn't201        # already exist so we do not expect this operation to fail here.202        assert count_before < self._debugger.Breakpoints.Count203        # We've added a new breakpoint, record its id.204        self._vs_to_dex_ids[vsbp].append(new_id)205        self._dex_id_to_vs[new_id] = vsbp206        return new_id207 208    def get_triggered_breakpoint_ids(self):209        """Returns a set of opaque ids for just-triggered breakpoints."""210        bps_hit = self._debugger.AllBreakpointsLastHit211        bp_id_list = []212        # Intuitively, AllBreakpointsLastHit breakpoints are the last hit213        # _bound_ breakpoints. A bound breakpoint's parent holds the info of214        # the breakpoint the user requested. Our internal state tracks the user215        # requested breakpoints so we look at the Parent of these triggered216        # breakpoints to determine which have been hit.217        for bp in bps_hit:218            # All bound breakpoints should have the user-defined breakpoint as219            # a parent.220            assert bp.Parent221            vsbp = VSBreakpoint(222                PurePath(bp.Parent.File),223                bp.Parent.FileLine,224                bp.Parent.FileColumn,225                bp.Parent.Condition,226            )227            try:228                ids = self._vs_to_dex_ids[vsbp]229            except KeyError:230                pass231            else:232                bp_id_list += ids233        return set(bp_id_list)234 235    def delete_breakpoints(self, ids):236        """Delete breakpoints by their ids.237 238        Raises a KeyError if no breakpoint with this id exists.239        """240        vsbp_set = set()241        for id in ids:242            vsbp = self._dex_id_to_vs[id]243 244            # Remove our id from the associated list of dex ids.245            self._vs_to_dex_ids[vsbp].remove(id)246            del self._dex_id_to_vs[id]247 248            # Bail if there are other uses of this vsbp.249            if len(self._vs_to_dex_ids[vsbp]) > 0:250                continue251            # Otherwise find and delete it.252            vsbp_set.add(vsbp)253 254        vsbp_to_del_count = len(vsbp_set)255 256        for bp in self._debugger.Breakpoints:257            # We're looking at the user-set breakpoints so there should be no258            # Parent.259            assert bp.Parent is None260            this_vsbp = VSBreakpoint(261                PurePath(bp.File), bp.FileLine, bp.FileColumn, bp.Condition262            )263            if this_vsbp in vsbp_set:264                bp.Delete()265                vsbp_to_del_count -= 1266                if vsbp_to_del_count == 0:267                    break268        if vsbp_to_del_count:269            raise KeyError("did not find breakpoint to be deleted")270 271    def _fetch_property(self, props, name):272        num_props = props.Count273        result = None274        for x in range(1, num_props + 1):275            item = props.Item(x)276            if item.Name == name:277                return item278        assert False, "Couldn't find property {}".format(name)279 280    def launch(self, cmdline):281        exe_path = Path(self.context.options.executable)282        self.context.logger.note(f"VS: Using executable: '{exe_path}'")283        cmdline_str = " ".join(cmdline)284        if self.context.options.target_run_args:285            cmdline_str += f" {self.context.options.target_run_args}"286        if cmdline_str:287            self.context.logger.note(f"VS: Using executable args: '{cmdline_str}'")288 289        # In a slightly baroque manner, lookup the VS project that runs when290        # you click "run", and set its command line options to the desired291        # command line options.292        startup_proj_name = str(293            self._fetch_property(self._interface.Solution.Properties, "StartupProject")294        )295        project = self._fetch_property(self._interface.Solution, startup_proj_name)296        ActiveConfiguration = self._fetch_property(297            project.Properties, "ActiveConfiguration"298        ).Object299        ActiveConfiguration.DebugSettings.CommandArguments = cmdline_str300        ConfigurationName = ActiveConfiguration.ConfigurationName301        SolConfig = self._fetch_property(302            self._interface.Solution.SolutionBuild.SolutionConfigurations,303            ConfigurationName,304        )305        for Context in SolConfig.SolutionContexts:306            Context.ShouldBuild = False307 308        self.context.logger.note("Launching VS debugger...")309        self._fn_go(False)310 311    def step_in(self):312        self._fn_step(False)313 314    def go(self) -> ReturnCode:315        self._fn_go(False)316        return ReturnCode.OK317 318    def set_current_stack_frame(self, idx: int = 0):319        thread = self._debugger.CurrentThread320        stack_frames = thread.StackFrames321        try:322            stack_frame = stack_frames[idx]323            self._debugger.CurrentStackFrame = stack_frame.raw324        except IndexError:325            raise Error(326                "attempted to access stack frame {} out of {}".format(327                    idx, len(stack_frames)328                )329            )330 331    def _translate_stop_reason(self, reason):332        if reason == DbgEvent.dbgEventReasonNone:333            return None334        if reason == DbgEvent.dbgEventReasonBreakpoint:335            return StopReason.BREAKPOINT336        if reason == DbgEvent.dbgEventReasonStep:337            return StopReason.STEP338        if reason == DbgEvent.dbgEventReasonEndProgram:339            return StopReason.PROGRAM_EXIT340        if reason == DbgEvent.dbgEventReasonExceptionNotHandled:341            return StopReason.ERROR342        assert reason <= DbgEvent.last and reason >= DbgEvent.first343        return StopReason.OTHER344 345    def _get_step_info(self, watches, step_index):346        thread = self._debugger.CurrentThread347        stackframes = thread.StackFrames348 349        frames = []350        state_frames = []351 352        loc = LocIR(**self._location)353        valid_loc_for_watch = loc.path and os.path.exists(loc.path)354 355        for idx, sf in enumerate(stackframes):356            frame = FrameIR(357                function=self._sanitize_function_name(sf.FunctionName),358                is_inlined=sf.FunctionName.startswith("[Inline Frame]"),359                loc=LocIR(path=None, lineno=None, column=None),360            )361 362            fname = frame.function or ""  # pylint: disable=no-member363            if any(name in fname for name in self.frames_below_main):364                break365 366            state_frame = StackFrame(367                function=frame.function, is_inlined=frame.is_inlined, watches={}368            )369 370            if valid_loc_for_watch and idx == 0:371                for watch_info in watches:372                    if watch_is_active(watch_info, loc.path, idx, loc.lineno):373                        watch_expr = watch_info.expression374                        state_frame.watches[watch_expr] = self.evaluate_expression(375                            watch_expr, idx376                        )377 378            state_frames.append(state_frame)379            frames.append(frame)380 381        if frames:382            frames[0].loc = loc383            state_frames[0].location = SourceLocation(**self._location)384 385        stop_reason = self._translate_stop_reason(self._debugger.LastBreakReason)386        program_state = ProgramState(frames=state_frames)387 388        return StepIR(389            step_index=step_index,390            frames=frames,391            stop_reason=stop_reason,392            program_state=program_state,393        )394 395    @property396    def is_running(self):397        return self._mode == VisualStudio.dbgRunMode398 399    @property400    def is_finished(self):401        return self._mode == VisualStudio.dbgDesignMode402 403    @property404    def frames_below_main(self):405        return [406            "[Inline Frame] invoke_main",407            "__scrt_common_main_seh",408            "__tmainCRTStartup",409            "mainCRTStartup",410        ]411 412    def evaluate_expression(self, expression, frame_idx=0) -> ValueIR:413        if frame_idx != 0:414            self.set_current_stack_frame(frame_idx)415        result = self._debugger.GetExpression(expression)416        if frame_idx != 0:417            self.set_current_stack_frame(0)418        value = result.Value419 420        is_optimized_away = any(421            s in value422            for s in [423                "Variable is optimized away and not available",424                "Value is not available, possibly due to optimization",425            ]426        )427 428        is_irretrievable = any(429            s in value430            for s in [431                "???",432                "<Unable to read memory>",433            ]434        )435 436        # an optimized away value is still counted as being able to be437        # evaluated.438        could_evaluate = result.IsValidValue or is_optimized_away or is_irretrievable439 440        return ValueIR(441            expression=expression,442            value=value,443            type_name=result.Type,444            error_string=None,445            is_optimized_away=is_optimized_away,446            could_evaluate=could_evaluate,447            is_irretrievable=is_irretrievable,448        )449