Package wtf :: Package app :: Package services :: Module crash
[hide private]
[frames] | no frames]

Source Code for Module wtf.app.services.crash

  1  # -*- coding: ascii -*- 
  2  # 
  3  # Copyright 2006-2012 
  4  # Andr\xe9 Malo or his licensors, as applicable 
  5  # 
  6  # Licensed under the Apache License, Version 2.0 (the "License"); 
  7  # you may not use this file except in compliance with the License. 
  8  # You may obtain a copy of the License at 
  9  # 
 10  #     http://www.apache.org/licenses/LICENSE-2.0 
 11  # 
 12  # Unless required by applicable law or agreed to in writing, software 
 13  # distributed under the License is distributed on an "AS IS" BASIS, 
 14  # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 
 15  # See the License for the specific language governing permissions and 
 16  # limitations under the License. 
 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   
45 -class DumpWarning(WtfWarning):
46 """ A non-fatal error occured while writing a dump """
47 48
49 -class Iterator(object):
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 # pylint: disable = E1103 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
103 - def __iter__(self):
104 """ 105 Return iterator object (iterator protocol) 106 107 :return: The iterator object 108 :rtype: `Iterator` 109 """ 110 return self
111
112 - def next(self):
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 # pylint: disable = E1103 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
138 - def __len__(self):
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
151 -class Middleware(object):
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 # pylint: disable = R0912 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 # can't do more without jeopardizing the 279 # crash page. 280 pass 281 finally: 282 exc_info = None 283 return [] 284 285
286 -class CrashService(object):
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 # check write access 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
345 - def status(self):
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
379 - def shutdown(self):
380 """ :See: `wtf.services.ServiceInterface.shutdown` """ 381 pass
382
383 - def global_service(self):
384 """ :See: `wtf.services.ServiceInterface.global_service` """ 385 return 'wtf.crash', self
386
387 - def middleware(self, func):
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
425 - def _make_dumpfile(self):
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