xref: /llvm-project/cross-project-tests/debuginfo-tests/dexter/dex/debugger/visualstudio/VisualStudio.py (revision 323270451d8db24f2c816f455b3d8f70f434286d)
1# DExTer : Debugging Experience Tester
2# ~~~~~~   ~         ~~         ~   ~~
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-exception
7"""Interface for communicating with the Visual Studio debugger via DTE."""
8
9import abc
10import imp
11import os
12import sys
13from pathlib import PurePath, Path
14from collections import defaultdict, namedtuple
15
16from dex.command.CommandBase import StepExpectInfo
17from dex.debugger.DebuggerBase import DebuggerBase, watch_is_active
18from dex.dextIR import FrameIR, LocIR, StepIR, StopReason, ValueIR
19from dex.dextIR import StackFrame, SourceLocation, ProgramState
20from dex.utils.Exceptions import Error, LoadDebuggerException
21from dex.utils.ReturnCode import ReturnCode
22
23
24def _load_com_module():
25    try:
26        module_info = imp.find_module(
27            "ComInterface", [os.path.join(os.path.dirname(__file__), "windows")]
28        )
29        return imp.load_module("ComInterface", *module_info)
30    except ImportError as e:
31        raise LoadDebuggerException(e, sys.exc_info())
32
33
34# VSBreakpoint(path: PurePath, line: int, col: int, cond: str).  This is enough
35# info to identify breakpoint equivalence in visual studio based on the
36# properties we set through dexter currently.
37VSBreakpoint = namedtuple("VSBreakpoint", "path, line, col, cond")
38
39
40class VisualStudio(
41    DebuggerBase, metaclass=abc.ABCMeta
42):  # pylint: disable=abstract-method
43    # Constants for results of Debugger.CurrentMode
44    # (https://msdn.microsoft.com/en-us/library/envdte.debugger.currentmode.aspx)
45    dbgDesignMode = 1
46    dbgBreakMode = 2
47    dbgRunMode = 3
48
49    def __init__(self, *args):
50        self.com_module = None
51        self._debugger = None
52        self._solution = None
53        self._fn_step = None
54        self._fn_go = None
55        # The next available unique breakpoint id. Use self._get_next_id().
56        self._next_bp_id = 0
57        # VisualStudio appears to common identical breakpoints. That is, if you
58        # ask for a breakpoint that already exists the Breakpoints list will
59        # not grow. DebuggerBase requires all breakpoints have a unique id,
60        # even for duplicates, so we'll need to do some bookkeeping.  Map
61        # {VSBreakpoint: list(id)} where id is the unique dexter-side id for
62        # the requested breakpoint.
63        self._vs_to_dex_ids = defaultdict(list)
64        # Map {id: VSBreakpoint} where id is unique and VSBreakpoint identifies
65        # a breakpoint in Visual Studio. There may be many ids mapped to a
66        # single VSBreakpoint. Use self._vs_to_dex_ids to find (dexter)
67        # breakpoints mapped to the same visual studio breakpoint.
68        self._dex_id_to_vs = {}
69
70        super(VisualStudio, self).__init__(*args)
71
72    def _create_solution(self):
73        self._solution.Create(self.context.working_directory.path, "DexterSolution")
74        try:
75            self._solution.AddFromFile(self._project_file)
76        except OSError:
77            raise LoadDebuggerException(
78                "could not debug the specified executable", sys.exc_info()
79            )
80
81    def _load_solution(self):
82        try:
83            self._solution.Open(self.context.options.vs_solution)
84        except:
85            raise LoadDebuggerException(
86                "could not load specified vs solution at {}".format(
87                    self.context.options.vs_solution
88                ),
89                sys.exc_info(),
90            )
91
92    def _custom_init(self):
93        try:
94            self._debugger = self._interface.Debugger
95            self._debugger.HexDisplayMode = False
96
97            self._interface.MainWindow.Visible = self.context.options.show_debugger
98
99            self._solution = self._interface.Solution
100            if self.context.options.vs_solution is None:
101                self._create_solution()
102            else:
103                self._load_solution()
104
105            self._fn_step = self._debugger.StepInto
106            self._fn_go = self._debugger.Go
107
108        except AttributeError as e:
109            raise LoadDebuggerException(str(e), sys.exc_info())
110
111    def _custom_exit(self):
112        if self._interface:
113            self._interface.Quit()
114
115    @property
116    def _project_file(self):
117        return self.context.options.executable
118
119    @abc.abstractproperty
120    def _dte_version(self):
121        pass
122
123    @property
124    def _location(self):
125        # TODO: Find a better way of determining path, line and column info
126        # that doesn't require reading break points. This method requires
127        # all lines to have a break point on them.
128        bp = self._debugger.BreakpointLastHit
129        return {
130            "path": getattr(bp, "File", None),
131            "lineno": getattr(bp, "FileLine", None),
132            "column": getattr(bp, "FileColumn", None),
133        }
134
135    @property
136    def _mode(self):
137        return self._debugger.CurrentMode
138
139    def _load_interface(self):
140        self.com_module = _load_com_module()
141        return self.com_module.DTE(self._dte_version)
142
143    @property
144    def version(self):
145        try:
146            return self._interface.Version
147        except AttributeError:
148            return None
149
150    def clear_breakpoints(self):
151        for bp in self._debugger.Breakpoints:
152            bp.Delete()
153        self._vs_to_dex_ids.clear()
154        self._dex_id_to_vs.clear()
155
156    def _add_breakpoint(self, file_, line):
157        return self._add_conditional_breakpoint(file_, line, "")
158
159    def _get_next_id(self):
160        # "Generate" a new unique id for the breakpoint.
161        id = self._next_bp_id
162        self._next_bp_id += 1
163        return id
164
165    def _add_conditional_breakpoint(self, file_, line, condition):
166        col = 1
167        vsbp = VSBreakpoint(PurePath(file_), line, col, condition)
168        new_id = self._get_next_id()
169
170        # Do we have an exact matching breakpoint already?
171        if vsbp in self._vs_to_dex_ids:
172            self._vs_to_dex_ids[vsbp].append(new_id)
173            self._dex_id_to_vs[new_id] = vsbp
174            return new_id
175
176        # Breakpoint doesn't exist already. Add it now.
177        count_before = self._debugger.Breakpoints.Count
178        self._debugger.Breakpoints.Add("", file_, line, col, condition)
179        # Our internal representation of VS says that the breakpoint doesn't
180        # already exist so we do not expect this operation to fail here.
181        assert count_before < self._debugger.Breakpoints.Count
182        # We've added a new breakpoint, record its id.
183        self._vs_to_dex_ids[vsbp].append(new_id)
184        self._dex_id_to_vs[new_id] = vsbp
185        return new_id
186
187    def get_triggered_breakpoint_ids(self):
188        """Returns a set of opaque ids for just-triggered breakpoints."""
189        bps_hit = self._debugger.AllBreakpointsLastHit
190        bp_id_list = []
191        # Intuitively, AllBreakpointsLastHit breakpoints are the last hit
192        # _bound_ breakpoints. A bound breakpoint's parent holds the info of
193        # the breakpoint the user requested. Our internal state tracks the user
194        # requested breakpoints so we look at the Parent of these triggered
195        # breakpoints to determine which have been hit.
196        for bp in bps_hit:
197            # All bound breakpoints should have the user-defined breakpoint as
198            # a parent.
199            assert bp.Parent
200            vsbp = VSBreakpoint(
201                PurePath(bp.Parent.File),
202                bp.Parent.FileLine,
203                bp.Parent.FileColumn,
204                bp.Parent.Condition,
205            )
206            try:
207                ids = self._vs_to_dex_ids[vsbp]
208            except KeyError:
209                pass
210            else:
211                bp_id_list += ids
212        return set(bp_id_list)
213
214    def delete_breakpoints(self, ids):
215        """Delete breakpoints by their ids.
216
217        Raises a KeyError if no breakpoint with this id exists.
218        """
219        vsbp_set = set()
220        for id in ids:
221            vsbp = self._dex_id_to_vs[id]
222
223            # Remove our id from the associated list of dex ids.
224            self._vs_to_dex_ids[vsbp].remove(id)
225            del self._dex_id_to_vs[id]
226
227            # Bail if there are other uses of this vsbp.
228            if len(self._vs_to_dex_ids[vsbp]) > 0:
229                continue
230            # Otherwise find and delete it.
231            vsbp_set.add(vsbp)
232
233        vsbp_to_del_count = len(vsbp_set)
234
235        for bp in self._debugger.Breakpoints:
236            # We're looking at the user-set breakpoints so there should be no
237            # Parent.
238            assert bp.Parent == None
239            this_vsbp = VSBreakpoint(
240                PurePath(bp.File), bp.FileLine, bp.FileColumn, bp.Condition
241            )
242            if this_vsbp in vsbp_set:
243                bp.Delete()
244                vsbp_to_del_count -= 1
245                if vsbp_to_del_count == 0:
246                    break
247        if vsbp_to_del_count:
248            raise KeyError("did not find breakpoint to be deleted")
249
250    def _fetch_property(self, props, name):
251        num_props = props.Count
252        result = None
253        for x in range(1, num_props + 1):
254            item = props.Item(x)
255            if item.Name == name:
256                return item
257        assert False, "Couldn't find property {}".format(name)
258
259    def launch(self, cmdline):
260        exe_path = Path(self.context.options.executable)
261        self.context.logger.note(f"VS: Using executable: '{exe_path}'")
262        cmdline_str = " ".join(cmdline)
263        if self.context.options.target_run_args:
264            cmdline_str += f" {self.context.options.target_run_args}"
265        if cmdline_str:
266            self.context.logger.note(f"VS: Using executable args: '{cmdline_str}'")
267
268        # In a slightly baroque manner, lookup the VS project that runs when
269        # you click "run", and set its command line options to the desired
270        # command line options.
271        startup_proj_name = str(
272            self._fetch_property(self._interface.Solution.Properties, "StartupProject")
273        )
274        project = self._fetch_property(self._interface.Solution, startup_proj_name)
275        ActiveConfiguration = self._fetch_property(
276            project.Properties, "ActiveConfiguration"
277        ).Object
278        ActiveConfiguration.DebugSettings.CommandArguments = cmdline_str
279
280        self.context.logger.note("Launching VS debugger...")
281        self._fn_go(False)
282
283    def step(self):
284        self._fn_step(False)
285
286    def go(self) -> ReturnCode:
287        self._fn_go(False)
288        return ReturnCode.OK
289
290    def set_current_stack_frame(self, idx: int = 0):
291        thread = self._debugger.CurrentThread
292        stack_frames = thread.StackFrames
293        try:
294            stack_frame = stack_frames[idx]
295            self._debugger.CurrentStackFrame = stack_frame.raw
296        except IndexError:
297            raise Error(
298                "attempted to access stack frame {} out of {}".format(
299                    idx, len(stack_frames)
300                )
301            )
302
303    def _get_step_info(self, watches, step_index):
304        thread = self._debugger.CurrentThread
305        stackframes = thread.StackFrames
306
307        frames = []
308        state_frames = []
309
310        loc = LocIR(**self._location)
311        valid_loc_for_watch = loc.path and os.path.exists(loc.path)
312
313        for idx, sf in enumerate(stackframes):
314            frame = FrameIR(
315                function=self._sanitize_function_name(sf.FunctionName),
316                is_inlined=sf.FunctionName.startswith("[Inline Frame]"),
317                loc=LocIR(path=None, lineno=None, column=None),
318            )
319
320            fname = frame.function or ""  # pylint: disable=no-member
321            if any(name in fname for name in self.frames_below_main):
322                break
323
324            state_frame = StackFrame(
325                function=frame.function, is_inlined=frame.is_inlined, watches={}
326            )
327
328            if valid_loc_for_watch and idx == 0:
329                for watch_info in watches:
330                    if watch_is_active(watch_info, loc.path, idx, loc.lineno):
331                        watch_expr = watch_info.expression
332                        state_frame.watches[watch_expr] = self.evaluate_expression(
333                            watch_expr, idx
334                        )
335
336            state_frames.append(state_frame)
337            frames.append(frame)
338
339        if frames:
340            frames[0].loc = loc
341            state_frames[0].location = SourceLocation(**self._location)
342
343        reason = StopReason.BREAKPOINT
344        if loc.path is None:  # pylint: disable=no-member
345            reason = StopReason.STEP
346
347        program_state = ProgramState(frames=state_frames)
348
349        return StepIR(
350            step_index=step_index,
351            frames=frames,
352            stop_reason=reason,
353            program_state=program_state,
354        )
355
356    @property
357    def is_running(self):
358        return self._mode == VisualStudio.dbgRunMode
359
360    @property
361    def is_finished(self):
362        return self._mode == VisualStudio.dbgDesignMode
363
364    @property
365    def frames_below_main(self):
366        return [
367            "[Inline Frame] invoke_main",
368            "__scrt_common_main_seh",
369            "__tmainCRTStartup",
370            "mainCRTStartup",
371        ]
372
373    def evaluate_expression(self, expression, frame_idx=0) -> ValueIR:
374        if frame_idx != 0:
375            self.set_current_stack_frame(frame_idx)
376        result = self._debugger.GetExpression(expression)
377        if frame_idx != 0:
378            self.set_current_stack_frame(0)
379        value = result.Value
380
381        is_optimized_away = any(
382            s in value
383            for s in [
384                "Variable is optimized away and not available",
385                "Value is not available, possibly due to optimization",
386            ]
387        )
388
389        is_irretrievable = any(
390            s in value
391            for s in [
392                "???",
393                "<Unable to read memory>",
394            ]
395        )
396
397        # an optimized away value is still counted as being able to be
398        # evaluated.
399        could_evaluate = result.IsValidValue or is_optimized_away or is_irretrievable
400
401        return ValueIR(
402            expression=expression,
403            value=value,
404            type_name=result.Type,
405            error_string=None,
406            is_optimized_away=is_optimized_away,
407            could_evaluate=could_evaluate,
408            is_irretrievable=is_irretrievable,
409        )
410