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()