root/galaxy-central/eggs/Paste-1.6-py2.6.egg/paste/evalexception/middleware.py

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

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

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