1"""
2raven.utils.stacks
3~~~~~~~~~~~~~~~~~~~~~~~~~~
4
5:copyright: (c) 2010-2012 by the Sentry Team, see AUTHORS for more details.
6:license: BSD, see LICENSE for more details.
7"""
8from __future__ import absolute_import, division
9
10import inspect
11import linecache
12import re
13import os
14import sys
15
16from raven.utils.serializer import transform
17from raven.utils.compat import iteritems
18
19
20_coding_re = re.compile(r'coding[:=]\s*([-\w.]+)')
21
22
23def get_lines_from_file(filename, lineno, context_lines,
24                        loader=None, module_name=None):
25    """
26    Returns context_lines before and after lineno from file.
27    Returns (pre_context_lineno, pre_context, context_line, post_context).
28    """
29    source = None
30    if loader is not None and hasattr(loader, "get_source"):
31        try:
32            source = loader.get_source(module_name)
33        except (ImportError, IOError):
34            # Traceback (most recent call last):
35            #   File "/Users/dcramer/Development/django-sentry/sentry/client/handlers.py", line 31, in emit
36            #     get_client().create_from_record(record, request=request)
37            #   File "/Users/dcramer/Development/django-sentry/sentry/client/base.py", line 325, in create_from_record
38            #     data['__sentry__']['frames'] = varmap(shorten, get_stack_info(stack))
39            #   File "/Users/dcramer/Development/django-sentry/sentry/utils/stacks.py", line 112, in get_stack_info
40            #     pre_context_lineno, pre_context, context_line, post_context = get_lines_from_file(filename, lineno, 7, loader, module_name)
41            #   File "/Users/dcramer/Development/django-sentry/sentry/utils/stacks.py", line 24, in get_lines_from_file
42            #     source = loader.get_source(module_name)
43            #   File "/System/Library/Frameworks/Python.framework/Versions/2.7/lib/python2.7/pkgutil.py", line 287, in get_source
44            #     fullname = self._fix_name(fullname)
45            #   File "/System/Library/Frameworks/Python.framework/Versions/2.7/lib/python2.7/pkgutil.py", line 262, in _fix_name
46            #     "module %s" % (self.fullname, fullname))
47            # ImportError: Loader for module cProfile cannot handle module __main__
48            source = None
49        if source is not None:
50            source = source.splitlines()
51
52    if source is None:
53        try:
54            source = linecache.getlines(filename)
55        except (OSError, IOError):
56            return None, None, None
57
58    if not source:
59        return None, None, None
60
61    lower_bound = max(0, lineno - context_lines)
62    upper_bound = min(lineno + 1 + context_lines, len(source))
63
64    try:
65        pre_context = [
66            line.strip('\r\n')
67            for line in source[lower_bound:lineno]
68        ]
69        context_line = source[lineno].strip('\r\n')
70        post_context = [
71            line.strip('\r\n')
72            for line in source[(lineno + 1):upper_bound]
73        ]
74    except IndexError:
75        # the file may have changed since it was loaded into memory
76        return None, None, None
77
78    return (
79        slim_string(pre_context),
80        slim_string(context_line),
81        slim_string(post_context)
82    )
83
84
85def _getitem_from_frame(f_locals, key, default=None):
86    """
87    f_locals is not guaranteed to have .get(), but it will always
88    support __getitem__. Even if it doesn't, we return ``default``.
89    """
90    try:
91        return f_locals[key]
92    except Exception:
93        return default
94
95
96def to_dict(dictish):
97    """
98    Given something that closely resembles a dictionary, we attempt
99    to coerce it into a propery dictionary.
100    """
101    if hasattr(dictish, 'iterkeys'):
102        m = dictish.iterkeys
103    elif hasattr(dictish, 'keys'):
104        m = dictish.keys
105    else:
106        raise ValueError(dictish)
107
108    return dict((k, dictish[k]) for k in m())
109
110
111def iter_traceback_frames(tb):
112    """
113    Given a traceback object, it will iterate over all
114    frames that do not contain the ``__traceback_hide__``
115    local variable.
116    """
117    # Some versions of celery have hacked traceback objects that might
118    # miss tb_frame.
119    while tb and hasattr(tb, 'tb_frame'):
120        # support for __traceback_hide__ which is used by a few libraries
121        # to hide internal frames.
122        f_locals = getattr(tb.tb_frame, 'f_locals', {})
123        if not _getitem_from_frame(f_locals, '__traceback_hide__'):
124            yield tb.tb_frame, getattr(tb, 'tb_lineno', None)
125        tb = tb.tb_next
126
127
128def iter_stack_frames(frames=None):
129    """
130    Given an optional list of frames (defaults to current stack),
131    iterates over all frames that do not contain the ``__traceback_hide__``
132    local variable.
133    """
134    if not frames:
135        frames = inspect.stack()[1:]
136
137    for frame, lineno in ((f[0], f[2]) for f in reversed(frames)):
138        f_locals = getattr(frame, 'f_locals', {})
139        if not _getitem_from_frame(f_locals, '__traceback_hide__'):
140            yield frame, lineno
141
142
143def get_frame_locals(frame, transformer=transform, max_var_size=4096):
144    f_locals = getattr(frame, 'f_locals', None)
145    if not f_locals:
146        return None
147
148    if not isinstance(f_locals, dict):
149        # XXX: Genshi (and maybe others) have broken implementations of
150        # f_locals that are not actually dictionaries
151        try:
152            f_locals = to_dict(f_locals)
153        except Exception:
154            return None
155
156    f_vars = {}
157    f_size = 0
158    for k, v in iteritems(f_locals):
159        v = transformer(v)
160        v_size = len(repr(v))
161        if v_size + f_size < max_var_size:
162            f_vars[k] = v
163            f_size += v_size
164    return f_vars
165
166
167def slim_frame_data(frames, frame_allowance=25):
168    """
169    Removes various excess metadata from middle frames which go beyond
170    ``frame_allowance``.
171
172    Returns ``frames``.
173    """
174    frames_len = 0
175    app_frames = []
176    system_frames = []
177    for frame in frames:
178        frames_len += 1
179        if frame.get('in_app'):
180            app_frames.append(frame)
181        else:
182            system_frames.append(frame)
183
184    if frames_len <= frame_allowance:
185        return frames
186
187    remaining = frames_len - frame_allowance
188    app_count = len(app_frames)
189    system_allowance = max(frame_allowance - app_count, 0)
190    if system_allowance:
191        half_max = int(system_allowance / 2)
192        # prioritize trimming system frames
193        for frame in system_frames[half_max:-half_max]:
194            frame.pop('vars', None)
195            frame.pop('pre_context', None)
196            frame.pop('post_context', None)
197            remaining -= 1
198
199    else:
200        for frame in system_frames:
201            frame.pop('vars', None)
202            frame.pop('pre_context', None)
203            frame.pop('post_context', None)
204            remaining -= 1
205
206    if remaining:
207        app_allowance = app_count - remaining
208        half_max = int(app_allowance / 2)
209
210        for frame in app_frames[half_max:-half_max]:
211            frame.pop('vars', None)
212            frame.pop('pre_context', None)
213            frame.pop('post_context', None)
214
215    return frames
216
217
218def slim_string(value, length=512):
219    if not value:
220        return value
221    if len(value) > length:
222        return value[:length - 3] + '...'
223    return value[:length]
224
225
226def get_stack_info(frames, transformer=transform, capture_locals=True,
227                   frame_allowance=25):
228    """
229    Given a list of frames, returns a list of stack information
230    dictionary objects that are JSON-ready.
231
232    We have to be careful here as certain implementations of the
233    _Frame class do not contain the necessary data to lookup all
234    of the information we want.
235    """
236    __traceback_hide__ = True  # NOQA
237
238    result = []
239    for frame_info in frames:
240        # Old, terrible API
241        if isinstance(frame_info, (list, tuple)):
242            frame, lineno = frame_info
243
244        else:
245            frame = frame_info
246            lineno = frame_info.f_lineno
247
248        # Support hidden frames
249        f_locals = getattr(frame, 'f_locals', {})
250        if _getitem_from_frame(f_locals, '__traceback_hide__'):
251            continue
252
253        f_globals = getattr(frame, 'f_globals', {})
254
255        f_code = getattr(frame, 'f_code', None)
256        if f_code:
257            abs_path = frame.f_code.co_filename
258            function = frame.f_code.co_name
259        else:
260            abs_path = None
261            function = None
262
263        loader = _getitem_from_frame(f_globals, '__loader__')
264        module_name = _getitem_from_frame(f_globals, '__name__')
265
266        if lineno:
267            lineno -= 1
268
269        if lineno is not None and abs_path:
270            pre_context, context_line, post_context = \
271                get_lines_from_file(abs_path, lineno, 5, loader, module_name)
272        else:
273            pre_context, context_line, post_context = None, None, None
274
275        # Try to pull a relative file path
276        # This changes /foo/site-packages/baz/bar.py into baz/bar.py
277        try:
278            base_filename = sys.modules[module_name.split('.', 1)[0]].__file__
279            filename = abs_path.split(
280                base_filename.rsplit(os.sep, 2)[0], 1)[-1].lstrip(os.sep)
281        except Exception:
282            filename = abs_path
283
284        if not filename:
285            filename = abs_path
286
287        frame_result = {
288            'abs_path': abs_path,
289            'filename': filename,
290            'module': module_name or None,
291            'function': function or '<unknown>',
292            'lineno': lineno + 1,
293        }
294        if capture_locals:
295            f_vars = get_frame_locals(frame, transformer=transformer)
296            if f_vars:
297                frame_result['vars'] = f_vars
298
299        if context_line is not None:
300            frame_result.update({
301                'pre_context': pre_context,
302                'context_line': context_line,
303                'post_context': post_context,
304            })
305        result.append(frame_result)
306
307    stackinfo = {
308        'frames': slim_frame_data(result, frame_allowance=frame_allowance),
309    }
310
311    return stackinfo
312