root/galaxy-central/eggs/WebError-0.8a-py2.6.egg/weberror/evalexception/middleware.py @ 3

リビジョン 3, 24.7 KB (コミッタ: kohda, 14 年 前)

Install Unix tools  http://hannonlab.cshl.edu/galaxy_unix_tools/galaxy.html

行番号 
1"""Exception-catching middleware that allows interactive debugging.
2
3This middleware catches all unexpected exceptions.  A normal
4traceback, like produced by
5``weberror.exceptions.errormiddleware.ErrorMiddleware`` is given, plus
6controls to see local variables and evaluate expressions in a local
7context.
8
9This can only be used in single-process environments, because
10subsequent requests must go back to the same process that the
11exception originally occurred in.  Threaded or non-concurrent
12environments both work.
13
14This shouldn't be used in production in any way.  That would just be
15silly.
16
17If calling from an XMLHttpRequest call, if the GET variable ``_`` is
18given then it will make the response more compact (and less
19Javascripty), since if you use innerHTML it'll kill your browser.  You
20can look for the header X-Debug-URL in your 500 responses if you want
21to see the full debuggable traceback.  Also, this URL is printed to
22``wsgi.errors``, so you can open it up in another browser window.
23
24"""
25import sys
26import os
27import cgi
28import traceback
29from cStringIO import StringIO
30import pprint
31import itertools
32import time
33import re
34
35from pkg_resources import resource_filename
36
37from paste import urlparser
38from paste import registry
39from paste import request
40
41import evalcontext
42from weberror.exceptions import errormiddleware, formatter, collector
43from tempita import HTMLTemplate
44from webob import Request, Response
45from webob import exc
46from weberror.util import PySourceColor
47
48limit = 200
49
50def html_quote(v):
51    """
52    Escape HTML characters, plus translate None to ''
53    """
54    if v is None:
55        return ''
56    return cgi.escape(str(v), 1)
57
58
59def preserve_whitespace(v, quote=True):
60    """
61    Quote a value for HTML, preserving whitespace (translating
62    newlines to ``<br>`` and multiple spaces to use ``&nbsp;``).
63
64    If ``quote`` is true, then the value will be HTML quoted first.
65    """
66    if quote:
67        v = html_quote(v)
68    v = v.replace('\n', '<br>\n')
69    v = re.sub(r'()(  +)', _repl_nbsp, v)
70    v = re.sub(r'(\n)( +)', _repl_nbsp, v)
71    v = re.sub(r'^()( +)', _repl_nbsp, v)
72    return '<code>%s</code>' % v
73
74
75def _repl_nbsp(match):
76    if len(match.group(2)) == 1:
77        return '&nbsp;'
78    return match.group(1) + '&nbsp;' * (len(match.group(2))-1) + ' '
79
80
81def simplecatcher(application):
82    """
83    A simple middleware that catches errors and turns them into simple
84    tracebacks.
85    """
86    def simplecatcher_app(environ, start_response):
87        try:
88            return application(environ, start_response)
89        except:
90            out = StringIO()
91            traceback.print_exc(file=out)
92            start_response('500 Server Error',
93                           [('content-type', 'text/html')],
94                           sys.exc_info())
95            res = out.getvalue()
96            return ['<h3>Error</h3><pre>%s</pre>'
97                    % html_quote(res)]
98    return simplecatcher_app
99
100
101def wsgiapp():
102    """
103    Turns a function or method into a WSGI application.
104    """
105    def decorator(func):
106        def wsgiapp_wrapper(*args):
107            # we get 3 args when this is a method, two when it is
108            # a function :(
109            if len(args) == 3:
110                environ = args[1]
111                start_response = args[2]
112                args = [args[0]]
113            else:
114                environ, start_response = args
115                args = []
116            def application(environ, start_response):
117                form = request.parse_formvars(environ,
118                                              include_get_vars=True)
119                status = '200 OK'
120                form['environ'] = environ
121                try:
122                    res = func(*args, **form.mixed())
123                except ValueError, ve:
124                    status = '500 Server Error'
125                    res = '<html>There was an error: %s</html>' % \
126                        html_quote(ve)
127                start_response(status, [('content-type', 'text/html')])
128                return [res]
129            app = simplecatcher(application)
130            return app(environ, start_response)
131        wsgiapp_wrapper.exposed = True
132        return wsgiapp_wrapper
133    return decorator
134
135
136def get_debug_info(func):
137    """
138    A decorator (meant to be used under ``wsgiapp()``) that resolves
139    the ``debugcount`` variable to a ``DebugInfo`` object (or gives an
140    error if it can't be found).
141    """
142    def debug_info_replacement(self, req):
143        if 'debugcount' not in req.params:
144            return exc.HTTPBadRequest(
145                "You must provide a debugcount parameter")
146        debugcount = req.params['debugcount']
147        try:
148            debugcount = int(debugcount)
149        except ValueError, e:
150            return exc.HTTPBadRequest(
151                "Invalid value for debugcount (%r): %s"
152                % (debugcount, e))
153        if debugcount not in self.debug_infos:
154            return exc.HTTPServerError(
155                "Debug %s not found (maybe it has expired, or the server was restarted)"
156                % debugcount)
157        req.debug_info = self.debug_infos[debugcount]
158        return func(self, req)
159    debug_info_replacement.exposed = True
160    return debug_info_replacement
161
162debug_counter = itertools.count(int(time.time()))
163
164def get_debug_count(req):
165    """
166    Return the unique debug count for the current request
167    """
168    if hasattr(req, 'environ'):
169        environ = req.environ
170    else:
171        environ = req
172    # XXX: Legacy support for Paste restorer
173    if 'paste.evalexception.debug_count' in environ:
174        return environ['paste.evalexception.debug_count']
175    elif 'weberror.evalexception.debug_count' in environ:
176        return environ['weberror.evalexception.debug_count']
177    else:
178        next = debug_counter.next()
179        environ['weberror.evalexception.debug_count'] = next
180        environ['paste.evalexception.debug_count'] = next
181        return next
182
183
184class InvalidTemplate(Exception):
185    pass
186
187
188class EvalException(object):
189    """Handles capturing an exception and turning it into an interactive
190    exception explorer"""
191    def __init__(self, application, global_conf=None,
192                 error_template_filename=None,
193                 xmlhttp_key=None, media_paths=None,
194                 templating_formatters=None, head_html='', footer_html='',
195                 **params):
196        self.application = application
197        self.debug_infos = {}
198        self.templating_formatters = templating_formatters or []
199        self.head_html = HTMLTemplate(head_html)
200        self.footer_html = HTMLTemplate(footer_html)
201        if error_template_filename is None:
202            error_template_filename = resource_filename( "weberror.evalexception",
203                                                         "error_template.html.tmpl" )
204        if xmlhttp_key is None:
205            if global_conf is None:
206                xmlhttp_key = '_'
207            else:
208                xmlhttp_key = global_conf.get('xmlhttp_key', '_')
209        self.xmlhttp_key = xmlhttp_key
210        self.media_paths = media_paths or {}
211        self.error_template = HTMLTemplate.from_filename(error_template_filename)
212   
213    def __call__(self, environ, start_response):
214        ## FIXME: print better error message (maybe fall back on
215        ## normal middleware, plus an error message)
216        assert not environ['wsgi.multiprocess'], (
217            "The EvalException middleware is not usable in a "
218            "multi-process environment")
219        # XXX: Legacy support for Paste restorer
220        environ['weberror.evalexception'] = environ['paste.evalexception'] = \
221            self
222        req = Request(environ)
223        if req.path_info_peek() == '_debug':
224            return self.debug(req)(environ, start_response)
225        else:
226            return self.respond(environ, start_response)
227
228    def debug(self, req):
229        assert req.path_info_pop() == '_debug'
230        next_part = req.path_info_pop()
231        method = getattr(self, next_part, None)
232        if method is None:
233            return exc.HTTPNotFound('Nothing could be found to match %r' % next_part)
234        if not getattr(method, 'exposed', False):
235            return exc.HTTPForbidden('Access to %r is forbidden' % next_part)
236        return method(req)
237
238    def media(self, req):
239        """Static path where images and other files live"""
240        first_part = req.path_info_peek()
241        if first_part in self.media_paths:
242            req.path_info_pop()
243            path = self.media_paths[first_part]
244        else:
245            path = resource_filename("weberror.evalexception", "media")
246        app = urlparser.StaticURLParser(path)
247        return app
248    media.exposed = True
249
250    def summary(self, req):
251        """
252        Returns a JSON-format summary of all the cached
253        exception reports
254        """
255        res = Response(content_type='text/x-json')
256        data = [];
257        items = self.debug_infos.values()
258        items.sort(lambda a, b: cmp(a.created, b.created))
259        data = [item.json() for item in items]
260        res.body = repr(data)
261        return res
262    summary.exposed = True
263
264    def view(self, req):
265        """
266        View old exception reports
267        """
268        id = int(req.path_info_pop())
269        if id not in self.debug_infos:
270            return exc.HTTPServerError(
271                "Traceback by id %s does not exist (maybe "
272                "the server has been restarted?)" % id)
273        debug_info = self.debug_infos[id]
274        return debug_info.wsgi_application
275    view.exposed = True
276
277    def make_view_url(self, environ, base_path, count):
278        return base_path + '/view/%s' % count
279
280    #@get_debug_info
281    def show_frame(self, req):
282        tbid = int(req.params['tbid'])
283        frame = req.debug_info.frame(tbid)
284        vars = frame.tb_frame.f_locals
285        if vars:
286            registry.restorer.restoration_begin(req.debug_info.counter)
287            try:
288                local_vars = make_table(vars)
289            finally:
290                registry.restorer.restoration_end()
291        else:
292            local_vars = 'No local vars'
293        res = Response(content_type='text/html')
294        res.body = input_form.substitute(tbid=tbid, debug_info=req.debug_info) + local_vars
295        return res
296
297    show_frame = get_debug_info(show_frame)
298
299    #@get_debug_info
300    def exec_input(self, req):
301        input = req.params.get('input')
302        if not input.strip():
303            return ''
304        input = input.rstrip() + '\n'
305        frame = req.debug_info.frame(int(req.params['tbid']))
306        vars = frame.tb_frame.f_locals
307        glob_vars = frame.tb_frame.f_globals
308        context = evalcontext.EvalContext(vars, glob_vars)
309        registry.restorer.restoration_begin(req.debug_info.counter)
310        try:
311            output = context.exec_expr(input)
312        finally:
313            registry.restorer.restoration_end()
314        input_html = formatter.str2html(input)
315        res = Response(content_type='text/html')
316        res.body = (
317            '<code style="color: #060">&gt;&gt;&gt;</code> '
318            '<code>%s</code><br>\n%s'
319            % (preserve_whitespace(input_html, quote=False),
320               preserve_whitespace(output)))
321        return res
322
323    exec_input = get_debug_info(exec_input)
324
325    def source_code(self, req):
326        location = req.params['location']
327        module_name, lineno = location.split(':', 1)
328        module = sys.modules[module_name]
329        filename = module.__file__
330        if filename.endswith('.pyc'):
331            filename = filename[:-1]
332        f = open(filename, 'rb')
333        source = f.read()
334        f.close()
335        html = (
336            ('<div>Module: <b>%s</b> file: %s</div>'
337             % (module_name, filename))
338            + PySourceColor.str2html(source, form='snip', linenumbers=-1))
339        source_lines = len(source.splitlines())
340        if source_lines < 60:
341            html += '\n<br>'*(60-source_lines)
342        res = Response(content_type='text/html', charset='utf8')
343        res.body = html
344        return res
345
346    source_code.exposed = True
347
348    def respond(self, environ, start_response):
349        req = Request(environ)
350        if req.environ.get('paste.throw_errors'):
351            return self.application(environ, start_response)
352        base_path = req.application_url + '/_debug'
353        req.environ['paste.throw_errors'] = True
354        started = []
355        def detect_start_response(status, headers, exc_info=None):
356            try:
357                return start_response(status, headers, exc_info)
358            except:
359                raise
360            else:
361                started.append(True)
362        try:
363            __traceback_supplement__ = errormiddleware.Supplement, self, environ
364            app_iter = self.application(environ, detect_start_response)
365            try:
366                return_iter = list(app_iter)
367                return return_iter
368            finally:
369                if hasattr(app_iter, 'close'):
370                    app_iter.close()
371        except:
372            exc_info = sys.exc_info()
373
374            # Tell the Registry to save its StackedObjectProxies current state
375            # for later restoration
376            ## FIXME: needs to be more abstract (something in the environ)
377            ## to remove the Paste dependency
378            registry.restorer.save_registry_state(environ)
379
380            count = get_debug_count(environ)
381            view_uri = self.make_view_url(environ, base_path, count)
382            if not started:
383                headers = [('content-type', 'text/html')]
384                headers.append(('X-Debug-URL', view_uri))
385                start_response('500 Internal Server Error',
386                               headers,
387                               exc_info)
388            environ['wsgi.errors'].write('Debug at: %s\n' % view_uri)
389
390            exc_data = collector.collect_exception(*exc_info)
391            debug_info = DebugInfo(count, exc_info, exc_data, base_path,
392                                   environ, view_uri, self.error_template,
393                                   self.templating_formatters, self.head_html,
394                                   self.footer_html)
395            assert count not in self.debug_infos
396            self.debug_infos[count] = debug_info
397
398            if self.xmlhttp_key:
399                if self.xmlhttp_key in req.params:
400                    exc_data = collector.collect_exception(*exc_info)
401                    html, extra_html = formatter.format_html(
402                        exc_data, include_hidden_frames=False,
403                        include_reusable=False, show_extra_data=False)
404                    return [html, extra_html]
405
406            # @@: it would be nice to deal with bad content types here
407            return debug_info.content()
408
409
410class DebugInfo(object):
411
412    def __init__(self, counter, exc_info, exc_data, base_path,
413                 environ, view_uri, error_template, templating_formatters,
414                 head_html, footer_html):
415        self.counter = counter
416        self.exc_data = exc_data
417        self.base_path = base_path
418        self.environ = environ
419        self.view_uri = view_uri
420        self.error_template = error_template
421        self.created = time.time()
422        self.templating_formatters = templating_formatters
423        self.head_html = head_html
424        self.footer_html = footer_html
425        self.exc_type, self.exc_value, self.tb = exc_info
426        __exception_formatter__ = 1
427        self.frames = []
428        n = 0
429        tb = self.tb
430        while tb is not None and (limit is None or n < limit):
431            if tb.tb_frame.f_locals.get('__exception_formatter__'):
432                # Stop recursion. @@: should make a fake ExceptionFrame
433                break
434            self.frames.append(tb)
435            tb = tb.tb_next
436            n += 1
437   
438    def json(self):
439        """Return the JSON-able representation of this object"""
440        return {
441            'uri': self.view_uri,
442            'created': time.strftime('%c', time.gmtime(self.created)),
443            'created_timestamp': self.created,
444            'exception_type': str(self.exc_type),
445            'exception': str(self.exc_value),
446            }
447
448    def frame(self, tbid):
449        for frame in self.frames:
450            if id(frame) == tbid:
451                return frame
452        else:
453            raise ValueError, (
454                "No frame by id %s found from %r" % (tbid, self.frames))
455
456    def wsgi_application(self, environ, start_response):
457        start_response('200 OK', [('content-type', 'text/html')])
458        return self.content()
459
460    def content(self):
461        traceback_body, extra_data = format_eval_html(self.exc_data, self.base_path, self.counter)
462        repost_button = make_repost_button(self.environ)
463        template_data = '<p>No Template information available.</p>'
464        tab = 'traceback_data'
465       
466        for tmpl_formatter in self.templating_formatters:
467            result = tmpl_formatter(self.exc_value)
468            if result:
469                tab = 'template_data'
470                template_data = result
471                break
472
473        template_data = template_data.replace('<h2>', '<h1 class="first">')
474        template_data = template_data.replace('</h2>', '</h1>')
475        if hasattr(self.exc_data.exception_type, '__name__'):
476            exc_name = self.exc_data.exception_type.__name__
477        else:
478            exc_name = str(self.exc_data.exception_type)
479        page = self.error_template.substitute(
480            head_html=self.head_html.substitute(prefix=self.base_path),
481            footer_html=self.footer_html.substitute(prefix=self.base_path),
482            repost_button=repost_button or '',
483            traceback_body=traceback_body,
484            exc_data=self.exc_data,
485            exc_name=exc_name,
486            extra_data=extra_data,
487            template_data=template_data,
488            set_tab=tab,
489            prefix=self.base_path,
490            counter=self.counter,
491            )
492        return [page]
493
494class EvalHTMLFormatter(formatter.HTMLFormatter):
495
496    def __init__(self, base_path, counter, **kw):
497        super(EvalHTMLFormatter, self).__init__(**kw)
498        self.base_path = base_path
499        self.counter = counter
500   
501    def format_source_line(self, filename, frame):
502        line = formatter.HTMLFormatter.format_source_line(
503            self, filename, frame)
504        location = '%s:%s' % (frame.modname, frame.lineno)
505        return (line +
506                '  <a href="#" class="switch_source" '
507                'tbid="%s" onClick="return showFrame(this)">&nbsp; &nbsp; '
508                '<img src="%s/media/plus.jpg" border=0 width=9 '
509                'height=9> &nbsp; &nbsp;</a> '
510                '<a href="#" class="" location="%s" '
511                'onClick="return showSource(this)">view</a>'
512                % (frame.tbid, self.base_path, location))
513
514
515def make_table(items):
516    if isinstance(items, dict):
517        items = items.items()
518        items.sort()
519    return table_template.substitute(
520        html_quote=html_quote,
521        items=items,
522        preserve_whitespace=preserve_whitespace,
523        make_wrappable=formatter.make_wrappable,
524        pprint_format=pprint_format)
525
526table_template = HTMLTemplate('''
527{{py:i = 0}}
528<table>
529{{for name, value in items:}}
530  {{py:i += 1}}
531{{py:
532value_html = html_quote(pprint_format(value, safe=True))
533value_html = make_wrappable(value_html)
534if len(value_html) > 100:
535    ## FIXME: This can break HTML; truncate before quoting?
536    value_html, expand_html = value_html[:100], value_html[100:]
537else:
538    expand_html = ''
539}}
540  <tr class="{{if i%2}}even{{else}}odd{{endif}}"
541      style="vertical-align: top">
542    <td><b>{{name}}</b></td>
543    <td style="overflow: auto">{{preserve_whitespace(value_html, quote=False)|html}}{{if expand_html}}
544      <a class="switch_source" style="background-color: #999" href="#" onclick="return expandLong(this)">...</a>
545      <span style="display: none">{{expand_html|html}}</span>
546    {{endif}}
547    </td>
548  </tr>
549{{endfor}}
550</table>
551''', name='table_template')
552
553def pprint_format(value, safe=False):
554    out = StringIO()
555    try:
556        pprint.pprint(value, out)
557    except Exception, e:
558        if safe:
559            out.write('Error: %s' % e)
560        else:
561            raise
562    return out.getvalue()
563
564def format_eval_html(exc_data, base_path, counter):
565    short_formatter = EvalHTMLFormatter(
566        base_path=base_path,
567        counter=counter,
568        include_reusable=False)
569    short_er, extra_data = short_formatter.format_collected_data(exc_data)
570    short_text_er, text_extra_data = formatter.format_text(exc_data, show_extra_data=False)
571    long_formatter = EvalHTMLFormatter(
572        base_path=base_path,
573        counter=counter,
574        show_hidden_frames=True,
575        show_extra_data=False,
576        include_reusable=False)
577    long_er, extra_data_none = long_formatter.format_collected_data(exc_data)
578    long_text_er = formatter.format_text(exc_data, show_hidden_frames=True,
579                                         show_extra_data=False)[0]
580    long_xml_er = formatter.format_xml(exc_data, show_hidden_frames=True,
581                                  show_extra_data=False)[0]
582    short_xml_er = formatter.format_xml(exc_data, show_hidden_frames=False,
583                                  show_extra_data=False)[0]
584   
585    if short_formatter.filter_frames(exc_data.frames) != \
586        long_formatter.filter_frames(exc_data.frames):
587        # Only display the full traceback when it differs from the
588        # short version
589        long_text_er = cgi.escape(long_text_er)
590        full_traceback_html = """
591        <div id="full_traceback" class="hidden-data">
592        %s
593        </div>
594        <div id="long_text_version" class="hidden-data">
595        <textarea style="width: 100%%" rows=%s cols=60>%s</textarea>
596        </div>
597        """ % (long_er, len(long_text_er.splitlines()), long_text_er)
598    else:
599        full_traceback_html = ''
600
601    short_text_er = cgi.escape(short_text_er)
602   
603    long_xml_leng = len(long_xml_er.splitlines())
604    if long_xml_leng > 50:
605        long_xml_leng = 50
606
607    short_xml_leng = len(short_xml_er.splitlines())
608    if short_xml_leng > 50:
609        short_xml_leng = 50
610
611    return """
612    <div id="short_traceback">
613    %s
614    </div>
615    <div id="short_text_version" class="hidden-data">
616    <textarea style="width: 100%%" rows=%s cols=60>%s</textarea>
617    </div>
618    <div id="long_xml_version" class="hidden-data">
619    <textarea style="width: 100%%" rows=%s cols=60>%s</textarea>
620    </div>
621    <div id="short_xml_version" class="hidden-data">
622    <textarea style="width: 100%%" rows=%s cols=60>%s</textarea>
623    </div>
624    %s
625    """ % (short_er, len(short_text_er.splitlines()), short_text_er,
626           long_xml_leng, cgi.escape(long_xml_er),
627           short_xml_leng, cgi.escape(short_xml_er),
628           full_traceback_html), extra_data
629
630def make_repost_button(environ):
631    url = request.construct_url(environ)
632    if environ['REQUEST_METHOD'] == 'GET':
633        return ('<button onclick="window.location.href=%r">'
634                'Re-GET Page</button><br>' % url)
635    else:
636        # @@: I'd like to reconstruct this, but I can't because
637        # the POST body is probably lost at this point, and
638        # I can't get it back :(
639        return None
640    # @@: Use or lose the following code block
641    """
642    fields = []
643    for name, value in request.parse_formvars(
644        environ, include_get_vars=False).items():
645        if hasattr(value, 'filename'):
646            # @@: Arg, we'll just submit the body, and leave out
647            # the filename :(
648            value = value.value
649        fields.append(
650            '<input type="hidden" name="%s" value="%s">'
651            % (html_quote(name), html_quote(value)))
652    return '''
653<form action="%s" method="POST">
654%s
655<input type="submit" value="Re-POST Page">
656</form>''' % (url, '\n'.join(fields))
657"""
658
659
660input_form = HTMLTemplate('''
661<form action="#" method="POST"
662 onsubmit="return submitInput($(\'#submit_{{tbid}}\').get(0), {{tbid}})">
663<div id="exec-output-{{tbid}}" style="width: 95%;
664 padding: 5px; margin: 5px; border: 2px solid #000;
665 display: none"></div>
666<input type="text" name="input" id="debug_input_{{tbid}}"
667 style="width: 100%"
668 autocomplete="off" onkeypress="upArrow(this, event)"><br>
669<input type="submit" value="Execute" name="submitbutton"
670 onclick="return submitInput(this, {{tbid}})"
671 id="submit_{{tbid}}"
672 input-from="debug_input_{{tbid}}"
673 output-to="exec-output-{{tbid}}">
674<input type="submit" value="Expand"
675 onclick="return expandInput(this)">
676</form>
677 ''', name='input_form')
678
679
680def make_eval_exception(app, global_conf, xmlhttp_key=None):
681    """
682    Wraps the application in an interactive debugger.
683
684    This debugger is a major security hole, and should only be
685    used during development.
686
687    xmlhttp_key is a string that, if present in QUERY_STRING,
688    indicates that the request is an XMLHttp request, and the
689    Javascript/interactive debugger should not be returned.  (If you
690    try to put the debugger somewhere with innerHTML, you will often
691    crash the browser)
692    """
693    if xmlhttp_key is None:
694        xmlhttp_key = global_conf.get('xmlhttp_key', '_')
695    return EvalException(app, xmlhttp_key=xmlhttp_key)
Note: リポジトリブラウザについてのヘルプは TracBrowser を参照してください。