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_breakpoints(self, ids):
208        """Delete breakpoints by their ids.
209
210        Raises a KeyError if no breakpoint with this id exists.
211        """
212        vsbp_set = set()
213        for id in ids:
214            vsbp = self._dex_id_to_vs[id]
215
216            # Remove our id from the associated list of dex ids.
217            self._vs_to_dex_ids[vsbp].remove(id)
218            del self._dex_id_to_vs[id]
219
220            # Bail if there are other uses of this vsbp.
221            if len(self._vs_to_dex_ids[vsbp]) > 0:
222                continue
223            # Otherwise find and delete it.
224            vsbp_set.add(vsbp)
225
226        vsbp_to_del_count = len(vsbp_set)
227
228        for bp in self._debugger.Breakpoints:
229            # We're looking at the user-set breakpoints so there should be no
230            # Parent.
231            assert bp.Parent == None
232            this_vsbp = VSBreakpoint(PurePath(bp.File), bp.FileLine,
233                                     bp.FileColumn, bp.Condition)
234            if this_vsbp in vsbp_set:
235                bp.Delete()
236                vsbp_to_del_count -= 1
237                if vsbp_to_del_count == 0:
238                    break
239        if vsbp_to_del_count:
240            raise KeyError('did not find breakpoint to be deleted')
241
242    def _fetch_property(self, props, name):
243        num_props = props.Count
244        result = None
245        for x in range(1, num_props+1):
246            item = props.Item(x)
247            if item.Name == name:
248                return item
249        assert False, "Couldn't find property {}".format(name)
250
251    def launch(self, cmdline):
252        cmdline_str = ' '.join(cmdline)
253
254        # In a slightly baroque manner, lookup the VS project that runs when
255        # you click "run", and set its command line options to the desired
256        # command line options.
257        startup_proj_name = str(self._fetch_property(self._interface.Solution.Properties, 'StartupProject'))
258        project = self._fetch_property(self._interface.Solution, startup_proj_name)
259        ActiveConfiguration = self._fetch_property(project.Properties, 'ActiveConfiguration').Object
260        ActiveConfiguration.DebugSettings.CommandArguments = cmdline_str
261
262        self._fn_go()
263
264    def step(self):
265        self._fn_step()
266
267    def go(self) -> ReturnCode:
268        self._fn_go()
269        return ReturnCode.OK
270
271    def set_current_stack_frame(self, idx: int = 0):
272        thread = self._debugger.CurrentThread
273        stack_frames = thread.StackFrames
274        try:
275            stack_frame = stack_frames[idx]
276            self._debugger.CurrentStackFrame = stack_frame.raw
277        except IndexError:
278            raise Error('attempted to access stack frame {} out of {}'
279                .format(idx, len(stack_frames)))
280
281    def _get_step_info(self, watches, step_index):
282        thread = self._debugger.CurrentThread
283        stackframes = thread.StackFrames
284
285        frames = []
286        state_frames = []
287
288
289        loc = LocIR(**self._location)
290        valid_loc_for_watch = loc.path and os.path.exists(loc.path)
291
292        for idx, sf in enumerate(stackframes):
293            frame = FrameIR(
294                function=self._sanitize_function_name(sf.FunctionName),
295                is_inlined=sf.FunctionName.startswith('[Inline Frame]'),
296                loc=LocIR(path=None, lineno=None, column=None))
297
298            fname = frame.function or ''  # pylint: disable=no-member
299            if any(name in fname for name in self.frames_below_main):
300                break
301
302            state_frame = StackFrame(function=frame.function,
303                                     is_inlined=frame.is_inlined,
304                                     watches={})
305
306            if valid_loc_for_watch and idx == 0:
307                for watch_info in watches:
308                    if watch_is_active(watch_info, loc.path, idx, loc.lineno):
309                        watch_expr = watch_info.expression
310                        state_frame.watches[watch_expr] = self.evaluate_expression(watch_expr, idx)
311
312
313            state_frames.append(state_frame)
314            frames.append(frame)
315
316        if frames:
317            frames[0].loc = loc
318            state_frames[0].location = SourceLocation(**self._location)
319
320        reason = StopReason.BREAKPOINT
321        if loc.path is None:  # pylint: disable=no-member
322            reason = StopReason.STEP
323
324        program_state = ProgramState(frames=state_frames)
325
326        return StepIR(
327            step_index=step_index, frames=frames, stop_reason=reason,
328            program_state=program_state)
329
330    @property
331    def is_running(self):
332        return self._mode == VisualStudio.dbgRunMode
333
334    @property
335    def is_finished(self):
336        return self._mode == VisualStudio.dbgDesignMode
337
338    @property
339    def frames_below_main(self):
340        return [
341            '[Inline Frame] invoke_main', '__scrt_common_main_seh',
342            '__tmainCRTStartup', 'mainCRTStartup'
343        ]
344
345    def evaluate_expression(self, expression, frame_idx=0) -> ValueIR:
346        if frame_idx != 0:
347            self.set_current_stack_frame(frame_idx)
348        result = self._debugger.GetExpression(expression)
349        if frame_idx != 0:
350            self.set_current_stack_frame(0)
351        value = result.Value
352
353        is_optimized_away = any(s in value for s in [
354            'Variable is optimized away and not available',
355            'Value is not available, possibly due to optimization',
356        ])
357
358        is_irretrievable = any(s in value for s in [
359            '???',
360            '<Unable to read memory>',
361        ])
362
363        # an optimized away value is still counted as being able to be
364        # evaluated.
365        could_evaluate = (result.IsValidValue or is_optimized_away
366                          or is_irretrievable)
367
368        return ValueIR(
369            expression=expression,
370            value=value,
371            type_name=result.Type,
372            error_string=None,
373            is_optimized_away=is_optimized_away,
374            could_evaluate=could_evaluate,
375            is_irretrievable=is_irretrievable,
376        )
377