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