1#!/usr/bin/env python3
  2
  3"""Global exception handling code.
  4Be careful importing common files as this module is initialised early.
  5Could have an env variable to drop into python debugger here, could allow
  6abbreviated stack traces.
  7"""
  8
  9import os
 10import sys
 11import types
 12import logging
 13import tempfile
 14
 15from chart.project import settings
 16from chart.common.args import ArgumentParser
 17
 18INDENTATION = '  '
 19
 20# maximum chars to display for contents of any variable
 21MAX_VARIABLE = 10000
 22
 23
 24def display_tb(tb, indent='', depth=1, target=sys.stdout):
 25    """Print information from a traceback (single frame of a stack trace),
 26    and recursively call ourselves to display the next frame.
 27    """
 28
 29    if tb is None:
 30        return
 31
 32    if tb.tb_next is not None:
 33        # recursion - display the next traceback down the stack before this one
 34        display_tb(tb.tb_next, indent=indent, depth=depth + 1, target=target)
 35
 36    target.write('{indent}Frame {depth}:\n'.format(indent=indent, depth=depth))
 37
 38    # show context - a few lines of code either side of the line emitting the exception
 39    display_code(tb.tb_frame.f_code.co_filename,
 40                 tb.tb_lineno,
 41                 indent=indent + INDENTATION,
 42                 target=target)
 43    target.write('\n')
 44
 45    # show local variable values
 46    display_locals(tb.tb_frame.f_locals,
 47                   indent=indent + INDENTATION,
 48                   target=target)
 49    target.write('\n')
 50
 51
 52def display_locals(f_locals, indent='', target=sys.stdout):
 53    """Print a list of local variables from a stack frame.
 54    Exclude:
 55     - Hidden variables starting with '__'
 56     - Django request objects
 57     - Type definitions
 58     - Modules
 59     - Functions
 60     - Class methods
 61     - Class member variables, if called from a member function
 62<<<hidden due to potential security issue>>>
 63     """
 64
 65    target.write(indent + 'Vars:\n')
 66    from django.core.handlers.wsgi import WSGIRequest
 67    for k, v in f_locals.items():
 68        # don't bother displaying vars with names like '__*' or local function definitions
 69        # or django frames.
 70        if (k.startswith('__') or
 71            k == 'environ' or
 72            k == 'self' or
 73<<<hidden due to potential security issue>>>
 74            type(v) in (WSGIRequest,
 75                        type,
 76                        types.ModuleType,
 77                        types.FunctionType,
 78                        types.MethodType)):
 79
 80            continue
 81
 82        # note, str(v) may appear unnecessary but is needed if a stack frame contains a
 83        # listiterator as a local variable since str.format() actually calls __format__
 84        # on the bind variables, not __str__.
 85
 86        # surround actual string values with quotes
 87        if isinstance(v, str):
 88            # v could be a string containing strange chars
 89            try:
 90                v = '"{v}"'.format(v=v.encode('utf-8'))
 91            except UnicodeDecodeError:
 92                # try to repack  binary blob as something vaguely useful on the terminal
 93                import binascii
 94                try:
 95                    v = '"{v}" (len {l})'.format(v=binascii.b2a_hex(v)[:30], l=len(v))
 96
 97                except UnicodeEncodeError:
 98                    v = '<unknown>'
 99
100        try:
101            value = str(v)
102        except:  # (no exception type) pylint:disable=W0702
103            # we have to trap all exceptions here since str() have throw anything
104            # and result in a confused stack trace irrelevant to the original error
105            value = '<error>'
106
107        try:
108            value = str(value)
109
110        except UnicodeDecodeError:
111            value = '<cannot display>'
112
113        if len(value) > MAX_VARIABLE:
114            value = value[:MAX_VARIABLE] + ' (truncated)'
115
116        target.write('{indent_}{name} = {value} ({dtype})\n'.format(
117                indent_=indent + INDENTATION,
118                dtype=str(type(v))[7:-2],
119                name=k,
120                value=value))
121
122
123def display_code(filename, lineno, indent='', target=sys.stdout):
124    """Display a few lines of code in `filename` around `lineno`."""
125
126    if not os.path.isfile(filename):
127        target.write('{indent}File {filename} not found\n\n'.format(
128                indent=indent,
129                filename=filename))
130        return
131
132    target.write('{indent}Filename:\n{indent_}{filename}\n\n{indent}Code:\n'.format(
133            indent=indent,
134            indent_=indent + INDENTATION,
135            filename=filename))
136
137    CONTEXT = 4  # additional lines to show before/after current line
138    for i, line in enumerate(open(filename, 'r'), start=1):
139        if i > (lineno + CONTEXT):
140            break
141
142        if i < (lineno - CONTEXT):
143            continue
144
145        target.write('{indent}{mark}{lineno:5}: {code}'.format(
146                indent=indent,
147                lineno=i,
148                code=line.replace('\t', '    '),
149                mark='->' if i == lineno else '  '))
150
151
152def global_exception(exc_type, exc_value, exc_traceback):
153    """Entry point for unhandled exceptions.
154    This is activated by constructing our ArgumentParser."""
155
156    # Find the best filename and line number to report by walking down the stack.
157    # To be really nice, we could just report the last line that exists inside the application code.
158
159    # Walk to the end of the stack trace to find the line that actually raised the exception
160    acc = exc_traceback
161    filename = None
162    lineno = None
163    while acc is not None:
164        filename = acc.tb_frame.f_code.co_filename
165        lineno = acc.tb_lineno
166        acc = acc.tb_next
167
168    if os.path is None:
169        # This can occasionally happen when an exception if throw during
170        # application shutdown
171        # We avoid using clever Path objects here in case the application
172        # is in trouble and they may not work
173        trace_filename = os.path.join(tempfile.gettempdir(), str(settings.STACK_TRACE_FILENAME))
174
175    else:
176        trace_filename = os.path.abspath(str(settings.STACK_TRACE_FILENAME))
177
178    # Now write a full dump and stack trace to the trace file
179    try:
180        h = open(trace_filename, 'w')
181    except IOError:
182        # regardless of the error, just try to write to /tmp which fixes most things
183        trace_filename = os.path.join(tempfile.gettempdir(), str(settings.STACK_TRACE_FILENAME))
184        try:
185            h = open(trace_filename, 'w')
186        except IOError as e:
187            if e.errno == 13:  # cannot write
188                print('Cannot write trace')
189
190            else:
191                raise
192
193    h.write('Exception:\n{indent}Class: {type}\n{indent}Message: {value}\n\n'.format(
194        type=str(exc_type).replace('<class \'', '').replace('<type \'', '').replace(
195            '\'>', ''),
196        value=exc_value,
197        indent=INDENTATION))
198
199    h.write('Call stack:\n')
200
201    display_tb(exc_traceback, indent='  ', target=h)
202
203    # Print a brief summary of the exception to the current log file
204    logging.critical('Exception {cls} ({filename}:{lineno}) {message}. '
205                     'Trace file written to \'{trace}\'.'.format(
206                         cls=str(exc_type).replace('<class \'', '').replace('\'>', ''),
207                         message=str(exc_value),
208                         filename=filename,
209                         lineno=lineno,
210                         trace=trace_filename))
211
212    # Print an even briefer version to stderr if it is an interactive terminal
213    if sys.stderr.isatty():
214        print('\n{message}\n'.format(message=str(exc_value)))
215
216
217def init_handler():
218    """Install our global exception handler."""
219    sys.excepthook = global_exception
220
221
222def error():
223    """Throw an error."""
224
225    # set a local variable so we can test it appears in the dump file
226    my_var = 'a test variable'  # (unused var) pylint:disable=W0612
227    print(1 / 0)
228
229
230def dump_obj(obj, target=sys.stdout.write):
231    """Utility function to dump all the attributes of `obj` to `target`."""
232    for k in dir(obj):
233        if k.startswith('_'):
234            continue
235
236        target('{k}: {v}'.format(k=k, v=getattr(obj, k)))
237
238
239def main():
240    """Command line entry point."""
241
242    parser = ArgumentParser()
243    parser.add_argument('--error', '-e',
244                        action='store_true',
245                        help='Throw an error')
246
247    args = parser.parse_args()
248
249    if args.error:
250        error()
251        parser.exit()
252
253    parser.error('No actions requested.')
254
255if __name__ == '__main__':
256    main()