1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17 """
18 Crash handler service
19 =====================
20
21 This service provides a global ``dump_request`` method, which can be called
22 from anywhere, plus it puts a middleware onto the stack, which catches
23 exceptions and dumps them to disk (by calling ``dump_request`` itself).
24
25 The crash handler can be configured to display a crash page when dumping (if
26 the response hasn't been started already). If configured in debug mode, the
27 middleware shows a page containing the traceback.
28 """
29 __author__ = u"Andr\xe9 Malo"
30 __docformat__ = "restructuredtext en"
31
32 import datetime as _datetime
33 import os as _os
34 import pprint as _pprint
35 import sys as _sys
36 import tempfile as _tempfile
37 import traceback as _traceback
38 import warnings as _warnings
39
40 from wtf import WtfWarning
41 from wtf import services as _services
42 from wtf.app.services import _crash_tb
43
44
46 """ A non-fatal error occured while writing a dump """
47
48
50 """
51 Result iterator with crash dumper
52
53 :IVariables:
54 - `close`: The iterable's close method (only if there's one)
55 - `_wrapped`: The wrapped iterable
56 - `_first`: If ``True``, `_wrapped` is actually a tuple, consisting of
57 the first iterable item and the iterable itself
58 - `_crash`: Crash service instance
59 - `_environ`: Request environment
60
61 :Types:
62 - `close`: ``callable``
63 - `_wrapped`: ``iterable``
64 - `_first`: ``bool``
65 - `_crash`: `CrashService`
66 - `_environ`: ``dict``
67 """
68
69 - def __init__(self, wrapped, crashservice, environ):
70 """
71 Initialization
72
73 :Parameters:
74 - `wrapped`: The iterable to wrap
75 - `crashservice`: The crash service instance
76 - `environ`: The request environment
77
78 :Types:
79 - `wrapped`: ``iterable``
80 - `crashservice`: `CrashService`
81 - `environ`: ``dict``
82 """
83 try:
84 close = wrapped.close
85 except AttributeError:
86 pass
87 else:
88 self.close = close
89 self._wrapped = iter(wrapped)
90 try:
91
92 first = self._wrapped.next()
93 while not first:
94 first = self._wrapped.next()
95 self._wrapped = first, self._wrapped
96 except StopIteration:
97 self._first = False
98 self._wrapped = iter(())
99 else:
100 self._first = True
101 self._crash, self._environ = crashservice, environ
102
104 """
105 Return iterator object (iterator protocol)
106
107 :return: The iterator object
108 :rtype: `Iterator`
109 """
110 return self
111
113 """
114 Return next item of the iterable (iterator protocol)
115
116 :return: The next item
117 :rtype: any
118 """
119 if self._first:
120 self._first, (item, self._wrapped) = False, self._wrapped
121 else:
122 try:
123
124 item = self._wrapped.next()
125 except (SystemExit, KeyboardInterrupt, StopIteration):
126 raise
127 except:
128 exc_info = _sys.exc_info()
129 try:
130 self._crash.dump_request(
131 exc_info, self._environ, True, _bail=False
132 )
133 raise exc_info[0], exc_info[1], exc_info[2]
134 finally:
135 exc_info = None
136 return item
137
139 """
140 Determine the length of the iterable
141
142 :return: The length
143 :rtype: ``int``
144
145 :Exceptions:
146 - `TypeError`: The iterable is unsized
147 """
148 return len(self._wrapped) + self._first
149
150
152 """
153 Crash middleware
154
155 :IVariables:
156 - `_crash`: Crash service instance
157 - `_func`: Wrapped WSGI callable
158 - `_debug`: Debugging enabled?
159 - `_display`: Tuple of crash page content and status line
160
161 :Types:
162 - `_crash`: `CrashService`
163 - `_func`: ``callable``
164 - `_debug`: ``bool``
165 - `_display`: ``tuple``
166 """
167
168 - def __init__(self, crashservice, debug, display, func):
169 """
170 Initialization
171
172 :Parameters:
173 - `crashservice`: Crash service instance
174 - `debug`: Debugging enabled?
175 - `display`: Tuple of crash page content and status line
176 - `func`: WSGI callable to wrap
177
178 :Types:
179 - `crashservice`: `CrashService`
180 - `debug`: ``bool``
181 - `display`: ``tuple``
182 - `func`: ``callable``
183 """
184 self._crash, self._func = crashservice, func
185 self._debug, self._display = debug, display
186
187 - def __call__(self, environ, start_response):
188 """
189 Middleware handler
190
191 :Parameters:
192 - `environ`: WSGI environment
193 - `start_response`: Start response callable
194
195 :Types:
196 - `environ`: ``dict``
197 - `start_response`: ``callable``
198
199 :return: WSGI response iterable
200 :rtype: ``iterable``
201 """
202
203
204 started, result = set(), None
205
206 def my_start_response(status, response_headers, exc_info=None):
207 """ Thin start_response wrapper, creating a a decorated writer """
208 write = start_response(status, response_headers, exc_info)
209 def my_write(data):
210 """ Writer, which remembers if it was called """
211 write(data)
212 if data:
213 started.add(1)
214 return my_write
215
216 try:
217 result = self._func(environ, my_start_response)
218 return Iterator(result, self._crash, environ)
219 except (SystemExit, KeyboardInterrupt):
220 raise
221 except:
222 exc_info = _sys.exc_info()
223 try:
224 if self._debug:
225 status, page = (
226 "500 Internal Server Error",
227 _crash_tb.debug_info(environ, exc_info)
228 )
229 else:
230 if self._display is None:
231 raise exc_info[0], exc_info[1], exc_info[2]
232 page, status, fname, mtime = self._display
233 try:
234 fp = open(fname, 'rb')
235 try:
236 xtime = _os.fstat(fp.fileno()).st_mtime
237 if xtime > mtime:
238 page = fp.read()
239 self._display = page, status, fname, xtime
240 finally:
241 fp.close()
242 except (IOError, OSError):
243 pass
244 try:
245 start_response(status, [
246 ("Content-Type", "text/html; charset=utf-8")
247 ], exc_info)
248 except (SystemExit, KeyboardInterrupt):
249 raise
250 except:
251 started = True
252 raise
253 return [page]
254 finally:
255 try:
256 try:
257 self._crash.dump_request(
258 exc_info, environ, bool(started), _bail=False
259 )
260 except (SystemExit, KeyboardInterrupt):
261 raise
262 except:
263 try:
264 print >> _sys.stderr, \
265 "Crashandler: Failed dumping request " \
266 "info:\n%s" % ''.join(
267 _traceback.format_exception(
268 *_sys.exc_info()
269 )
270 )
271 print >> _sys.stderr, \
272 "Original traceback was:\n%s" % ''.join(
273 _traceback.format_exception(*exc_info)
274 )
275 except (SystemExit, KeyboardInterrupt):
276 raise
277 except:
278
279
280 pass
281 finally:
282 exc_info = None
283 return []
284
285
287 """
288 Crash service
289
290 This service provides a middleware which catches exceptions,
291 dumps a traceback and displays an error page.
292
293 :IVariables:
294 - `_debug`: Debug mode?
295 - `_display`: Tuple of display template and status
296 (``('template', 'status')``)
297 - `_dumpdir`: Dump directory
298 - `_perms`: Dump permissions
299
300 :Types:
301 - `_debug`: ``bool``
302 - `_display`: ``tuple``
303 - `_dumpdir`: ``str``
304 - `_perms`: ``int``
305 """
306 __implements__ = [_services.ServiceInterface]
307 _debug, _display = False, None
308 _dumpdir, _dumpdir_unicode, _perms = None, None, None
309
310 - def __init__(self, config, opts, args):
311 """ :See: `wtf.services.ServiceInterface.__init__` """
312 section = config.crash
313 self._debug = bool(section('debug', False))
314 if 'display' in section:
315 fname, status = section.display.template, section.display.status
316 fp = open(fname, 'rb')
317 try:
318 page = fp.read()
319 mtime = _os.fstat(fp.fileno()).st_mtime
320 finally:
321 fp.close()
322 self._display = page, status, fname, mtime
323 if 'dump' in section:
324 self._perms = int(section.dump('perms', '0644'), 8)
325 self._dumpdir = _os.path.join(config.ROOT, unicode(
326 section.dump.directory
327 ).encode(_sys.getfilesystemencoding()))
328 try:
329 self._dumpdir_unicode = self._dumpdir.decode(
330 _sys.getfilesystemencoding()
331 )
332 except UnicodeError:
333 self._dumpdir_unicode = self._dumpdir.decode('latin-1')
334
335
336 fp, name = self._make_dumpfile()
337 try:
338 fp.write("!")
339 finally:
340 try:
341 fp.close()
342 finally:
343 _os.unlink(name)
344
346 """
347 Determine crash dump directory status
348
349 The dump count is ``-1`` if it could not be determined.
350
351 :return: A dict containing status information
352 (``{'status': u'status', 'count': int, 'dir': u'dumpdir'}``)
353 :rtype: ``dict``
354 """
355 if self._dumpdir_unicode:
356 try:
357 count = len(_os.listdir(self._dumpdir))
358 except OSError, e:
359 status = u"WARNING -- Can't open dump directory: %s" % \
360 unicode(repr(str(e)))
361 count = -1
362 else:
363 if count == 0:
364 status = u"OK -- 0 dumps"
365 else:
366 status = u"WARNING -- %s dumps" % count
367
368 return dict(
369 status=status,
370 dir=self._dumpdir_unicode,
371 count=count,
372 )
373 return dict(
374 status=u"WARNING -- No dump directory configured",
375 count=-1,
376 dir=u"n/a",
377 )
378
380 """ :See: `wtf.services.ServiceInterface.shutdown` """
381 pass
382
384 """ :See: `wtf.services.ServiceInterface.global_service` """
385 return 'wtf.crash', self
386
388 """ :See: `wtf.services.ServiceInterface.middleware` """
389 return Middleware(self, self._debug, self._display, func)
390
391 - def dump_request(self, exc_info, environ, started=False, _bail=True):
392 """
393 Dump a request
394
395 :Parameters:
396 - `exc_info`: Exception to dump (as provides by ``sys.exc_info()``)
397 - `environ`: Request environment
398 - `started`: Has the response been started already?
399 - `_bail`: Emit a warning if no dump directory is configured?
400
401 :Types:
402 - `exc_info`: ``tuple``
403 - `environ`: ``dict``
404 - `started`: ``bool``
405 - `_bail`: ``bool``
406 """
407 if self._dumpdir is None:
408 if _bail:
409 _warnings.warn(
410 "Cannot write requested dump: dump.directory not "
411 "configured", category=DumpWarning
412 )
413 else:
414 fp, name = self._make_dumpfile()
415 fp.write(
416 "Response started: %s\nEnvironment:\n%s\n\n%s\n" % (
417 started,
418 _pprint.pformat(environ),
419 exc_info and ''.join(_traceback.format_exception(*exc_info)),
420 ))
421 fp.close()
422 print >> _sys.stderr, \
423 "Crashhandler: Dumped request info to %s" % name
424
426 """
427 Create a file for the request dump
428
429 :return: Open stream and the filename (``(file, 'name')``)
430 :rtype: ``tuple``
431 """
432 prefix = _datetime.datetime.utcnow().strftime("dump.%Y-%m-%dT%H%M%S.")
433 fd, dumpname = _tempfile.mkstemp(prefix=prefix, dir=self._dumpdir)
434 try:
435 try:
436 _os.chmod(dumpname, self._perms)
437 except OSError, e:
438 _warnings.warn("chmod(%s, %04o) failed: %s" % (
439 dumpname, self._perms, str(e)), category=DumpWarning)
440 return _os.fdopen(fd, 'wb'), dumpname
441 except:
442 try:
443 _os.close(fd)
444 finally:
445 _os.unlink(dumpname)
446 raise
447