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