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 namedtuple
15from collections import defaultdict
16
17from dex.debugger.DebuggerBase import DebuggerBase
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        for idx, sf in enumerate(stackframes):
248            frame = FrameIR(
249                function=self._sanitize_function_name(sf.FunctionName),
250                is_inlined=sf.FunctionName.startswith('[Inline Frame]'),
251                loc=LocIR(path=None, lineno=None, column=None))
252
253            fname = frame.function or ''  # pylint: disable=no-member
254            if any(name in fname for name in self.frames_below_main):
255                break
256
257
258            state_frame = StackFrame(function=frame.function,
259                                     is_inlined=frame.is_inlined,
260                                     watches={})
261
262            for watch in watches:
263                state_frame.watches[watch] = self.evaluate_expression(
264                    watch, idx)
265
266
267            state_frames.append(state_frame)
268            frames.append(frame)
269
270        loc = LocIR(**self._location)
271        if frames:
272            frames[0].loc = loc
273            state_frames[0].location = SourceLocation(**self._location)
274
275        reason = StopReason.BREAKPOINT
276        if loc.path is None:  # pylint: disable=no-member
277            reason = StopReason.STEP
278
279        program_state = ProgramState(frames=state_frames)
280
281        return StepIR(
282            step_index=step_index, frames=frames, stop_reason=reason,
283            program_state=program_state)
284
285    @property
286    def is_running(self):
287        return self._mode == VisualStudio.dbgRunMode
288
289    @property
290    def is_finished(self):
291        return self._mode == VisualStudio.dbgDesignMode
292
293    @property
294    def frames_below_main(self):
295        return [
296            '[Inline Frame] invoke_main', '__scrt_common_main_seh',
297            '__tmainCRTStartup', 'mainCRTStartup'
298        ]
299
300    def evaluate_expression(self, expression, frame_idx=0) -> ValueIR:
301        self.set_current_stack_frame(frame_idx)
302        result = self._debugger.GetExpression(expression)
303        self.set_current_stack_frame(0)
304        value = result.Value
305
306        is_optimized_away = any(s in value for s in [
307            'Variable is optimized away and not available',
308            'Value is not available, possibly due to optimization',
309        ])
310
311        is_irretrievable = any(s in value for s in [
312            '???',
313            '<Unable to read memory>',
314        ])
315
316        # an optimized away value is still counted as being able to be
317        # evaluated.
318        could_evaluate = (result.IsValidValue or is_optimized_away
319                          or is_irretrievable)
320
321        return ValueIR(
322            expression=expression,
323            value=value,
324            type_name=result.Type,
325            error_string=None,
326            is_optimized_away=is_optimized_away,
327            could_evaluate=could_evaluate,
328            is_irretrievable=is_irretrievable,
329        )
330