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 _fetch_property(self, props, name):
233        num_props = props.Count
234        result = None
235        for x in range(1, num_props+1):
236            item = props.Item(x)
237            if item.Name == name:
238                return item
239        assert False, "Couldn't find property {}".format(name)
240
241    def launch(self, cmdline):
242        cmdline_str = ' '.join(cmdline)
243
244        # In a slightly baroque manner, lookup the VS project that runs when
245        # you click "run", and set its command line options to the desired
246        # command line options.
247        startup_proj_name = str(self._fetch_property(self._interface.Solution.Properties, 'StartupProject'))
248        project = self._fetch_property(self._interface.Solution, startup_proj_name)
249        ActiveConfiguration = self._fetch_property(project.Properties, 'ActiveConfiguration').Object
250        ActiveConfiguration.DebugSettings.CommandArguments = cmdline_str
251
252        self._fn_go()
253
254    def step(self):
255        self._fn_step()
256
257    def go(self) -> ReturnCode:
258        self._fn_go()
259        return ReturnCode.OK
260
261    def set_current_stack_frame(self, idx: int = 0):
262        thread = self._debugger.CurrentThread
263        stack_frames = thread.StackFrames
264        try:
265            stack_frame = stack_frames[idx]
266            self._debugger.CurrentStackFrame = stack_frame.raw
267        except IndexError:
268            raise Error('attempted to access stack frame {} out of {}'
269                .format(idx, len(stack_frames)))
270
271    def _get_step_info(self, watches, step_index):
272        thread = self._debugger.CurrentThread
273        stackframes = thread.StackFrames
274
275        frames = []
276        state_frames = []
277
278
279        loc = LocIR(**self._location)
280        valid_loc_for_watch = loc.path and os.path.exists(loc.path)
281
282        for idx, sf in enumerate(stackframes):
283            frame = FrameIR(
284                function=self._sanitize_function_name(sf.FunctionName),
285                is_inlined=sf.FunctionName.startswith('[Inline Frame]'),
286                loc=LocIR(path=None, lineno=None, column=None))
287
288            fname = frame.function or ''  # pylint: disable=no-member
289            if any(name in fname for name in self.frames_below_main):
290                break
291
292            state_frame = StackFrame(function=frame.function,
293                                     is_inlined=frame.is_inlined,
294                                     watches={})
295
296            if valid_loc_for_watch and idx == 0:
297                for watch_info in watches:
298                    if watch_is_active(watch_info, loc.path, idx, loc.lineno):
299                        watch_expr = watch_info.expression
300                        state_frame.watches[watch_expr] = self.evaluate_expression(watch_expr, idx)
301
302
303            state_frames.append(state_frame)
304            frames.append(frame)
305
306        if frames:
307            frames[0].loc = loc
308            state_frames[0].location = SourceLocation(**self._location)
309
310        reason = StopReason.BREAKPOINT
311        if loc.path is None:  # pylint: disable=no-member
312            reason = StopReason.STEP
313
314        program_state = ProgramState(frames=state_frames)
315
316        return StepIR(
317            step_index=step_index, frames=frames, stop_reason=reason,
318            program_state=program_state)
319
320    @property
321    def is_running(self):
322        return self._mode == VisualStudio.dbgRunMode
323
324    @property
325    def is_finished(self):
326        return self._mode == VisualStudio.dbgDesignMode
327
328    @property
329    def frames_below_main(self):
330        return [
331            '[Inline Frame] invoke_main', '__scrt_common_main_seh',
332            '__tmainCRTStartup', 'mainCRTStartup'
333        ]
334
335    def evaluate_expression(self, expression, frame_idx=0) -> ValueIR:
336        if frame_idx != 0:
337            self.set_current_stack_frame(frame_idx)
338        result = self._debugger.GetExpression(expression)
339        if frame_idx != 0:
340            self.set_current_stack_frame(0)
341        value = result.Value
342
343        is_optimized_away = any(s in value for s in [
344            'Variable is optimized away and not available',
345            'Value is not available, possibly due to optimization',
346        ])
347
348        is_irretrievable = any(s in value for s in [
349            '???',
350            '<Unable to read memory>',
351        ])
352
353        # an optimized away value is still counted as being able to be
354        # evaluated.
355        could_evaluate = (result.IsValidValue or is_optimized_away
356                          or is_irretrievable)
357
358        return ValueIR(
359            expression=expression,
360            value=value,
361            type_name=result.Type,
362            error_string=None,
363            is_optimized_away=is_optimized_away,
364            could_evaluate=could_evaluate,
365            is_irretrievable=is_irretrievable,
366        )
367