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