Ticket #3527: django_debugger.patch

File django_debugger.patch, 17.0 KB (added by Marek Kubica <pythonmailing@…>, 8 years ago)

Patch by Armin Ronacher to add a debugger to Django's exceptions

  • django/conf/global_settings.py

     
    1111
    1212DEBUG = False
    1313TEMPLATE_DEBUG = False
     14DEBUGGER_ENABLED = False
    1415
    1516# Whether to use the "Etag" header. This saves bandwidth but slows down performance.
    1617USE_ETAGS = False
  • django/views/debug.py

     
    22from django.template import Template, Context, TemplateDoesNotExist
    33from django.utils.html import escape
    44from django.http import HttpResponseServerError, HttpResponseNotFound
    5 import os, re
     5import sys, os, re, code, threading
    66
    77HIDDEN_SETTINGS = re.compile('SECRET|PASSWORD|PROFANITIES_LIST')
    88
     9def new_debugger_id():
     10    import time
     11    import md5
     12    return md5.new(str(time.time())).hexdigest()
     13
    914def linebreak_iter(template_source):
    1015    yield 0
    1116    p = template_source.find('\n')
     
    6671    Create a technical server error response. The last three arguments are
    6772    the values returned from sys.exc_info() and friends.
    6873    """
     74    if settings.DEBUGGER_ENABLED:
     75        debugger_id = new_debugger_id()
     76        debugger_enabled = True
     77        frame_storage = console.debug_sessions[debugger_id] = {}
     78    else:
     79        debugger_id = None
     80        debugger_enabled = False
    6981    template_info = None
    7082    template_does_not_exist = False
    7183    loader_debug_info = None
     
    90102        exc_type, exc_value, tb, template_info = get_template_exception_info(exc_type, exc_value, tb)
    91103    frames = []
    92104    while tb is not None:
     105        # support for __traceback_hide__ which is used by a few libraries
     106        # to hide internal frames.
     107        if tb.tb_frame.f_locals.get('__traceback_hide__'):
     108            tb = tb.tb_next
     109            continue
     110        if debugger_enabled:
     111            frame_storage[str(id(tb))] = tb.tb_frame
    93112        filename = tb.tb_frame.f_code.co_filename
    94113        function = tb.tb_frame.f_code.co_name
    95114        lineno = tb.tb_lineno - 1
    96         pre_context_lineno, pre_context, context_line, post_context = _get_lines_from_file(filename, lineno, 7)
    97         if pre_context_lineno:
     115        loader = tb.tb_frame.f_globals.get('__loader__')
     116        module_name = tb.tb_frame.f_globals.get('__name__')
     117        pre_context_lineno, pre_context, context_line, post_context = _get_lines_from_file(filename, lineno, 7, loader, module_name)
     118        if pre_context_lineno is not None:
    98119            frames.append({
    99120                'tb': tb,
    100121                'filename': filename,
     
    117138        }]
    118139    t = Template(TECHNICAL_500_TEMPLATE, name='Technical 500 template')
    119140    c = Context({
     141        'debugger_id': debugger_id,
     142        'debugger_enabled': debugger_enabled,
    120143        'exception_type': exc_type.__name__,
    121144        'exception_value': exc_value,
    122145        'frames': frames,
     
    161184    })
    162185    return HttpResponseNotFound(t.render(c), mimetype='text/html')
    163186
    164 def _get_lines_from_file(filename, lineno, context_lines):
     187def _get_lines_from_file(filename, lineno, context_lines, loader=None, module_name=None):
    165188    """
    166189    Returns context_lines before and after lineno from file.
    167190    Returns (pre_context_lineno, pre_context, context_line, post_context).
    168191    """
    169     try:
    170         source = open(filename).readlines()
    171         lower_bound = max(0, lineno - context_lines)
    172         upper_bound = lineno + context_lines
     192    source = None
     193    if loader is not None:
     194        source = loader.get_source(module_name).splitlines()
     195    else:
     196        try:
     197            f = open(filename)
     198            try:
     199                source = f.readlines()
     200            finally:
     201                f.close()
     202        except (OSError, IOError):
     203            pass
     204    if source is None:
     205        return None, [], None, []
    173206
    174         pre_context = [line.strip('\n') for line in source[lower_bound:lineno]]
    175         context_line = source[lineno].strip('\n')
    176         post_context = [line.strip('\n') for line in source[lineno+1:upper_bound]]
     207    lower_bound = max(0, lineno - context_lines)
     208    upper_bound = lineno + context_lines
    177209
    178         return lower_bound, pre_context, context_line, post_context
    179     except (OSError, IOError):
    180         return None, [], None, []
     210    pre_context = [line.strip('\n') for line in source[lower_bound:lineno]]
     211    context_line = source[lineno].strip('\n')
     212    post_context = [line.strip('\n') for line in source[lineno+1:upper_bound]]
    181213
     214    return lower_bound, pre_context, context_line, post_context
     215
     216
     217class DebugCapture(object):
     218    """
     219    Class that wraps sys.stdout in order to get the output
     220    for the debugger. threadsafe but quite slow.
     221    """
     222    _orig = None
     223
     224    def __init__(self):
     225        self._buffer = {}
     226
     227    def install(cls):
     228        if cls._orig:
     229            return
     230        cls._orig = sys.stdout
     231        sys.stdout = cls()
     232    install = classmethod(install)
     233
     234    def push(self):
     235        from cStringIO import StringIO
     236        tid = threading.currentThread()
     237        self._buffer[tid] = StringIO()
     238
     239    def release(self):
     240        tid = threading.currentThread()
     241        if tid in self._buffer:
     242            result = self._buffer[tid].getvalue()
     243            del self._buffer[tid]
     244        else:
     245            result = ''
     246        return result
     247
     248    def write(self, d):
     249        tid = threading.currentThread()
     250        if tid in self._buffer:
     251            self._buffer[tid].write(d)
     252        else:
     253            self._orig.write(d)
     254
     255
     256class PlainDebugger(code.InteractiveInterpreter):
     257    """
     258    Subclass of the python interactive interpreter that
     259    automatically captures stdout and buffers older input.
     260    """
     261
     262    def __init__(self, locals=None, globals=None):
     263        self.globals = globals
     264        code.InteractiveInterpreter.__init__(self, locals)
     265        self.prompt = '>>> '
     266        self.buffer = []
     267
     268    def runsource(self, source):
     269        # installs the debug capture on first access
     270        DebugCapture.install()
     271        prompt = self.prompt
     272        sys.stdout.push()
     273        try:
     274            source_to_eval = ''.join(self.buffer + [source])
     275            if code.InteractiveInterpreter.runsource(self,
     276               source_to_eval, '<debugger>', 'single'):
     277                self.prompt = '... '
     278                self.buffer.append(source)
     279            else:
     280                self.prompt = '>>> '
     281                del self.buffer[:]
     282        finally:
     283            return prompt + source + sys.stdout.release()
     284
     285    def runcode(self, code):
     286        try:
     287            exec code in self.globals, self.locals
     288        except:
     289            self.showtraceback()
     290
     291    def write(self, data):
     292        sys.stdout.write(data)
     293
     294
     295class AjaxDebugger(object):
     296    """
     297    The AJAX Debugger
     298    """
     299
     300    def __init__(self):
     301        self.debug_sessions = {}
     302        self.consoles = {}
     303
     304    def send(self, debugger, frame, cmd):
     305        if debugger not in self.debug_sessions:
     306            return '!!! expired debugger !!!'
     307        session = self.debug_sessions[debugger]
     308        if frame not in session:
     309            return '!!! unknown frame !!!'
     310        key = '%s|%s' % (debugger, frame)
     311        if key not in self.consoles:
     312            self.consoles[key] = PlainDebugger(
     313                session[frame].f_globals,
     314                session[frame].f_locals
     315            )
     316        return self.consoles[key].runsource(cmd)
     317
     318
     319console = AjaxDebugger()
     320
    182321#
    183322# Templates are embedded in the file so that we know the error handler will
    184323# always work even if the template loader is broken.
    185324#
    186325
    187 TECHNICAL_500_TEMPLATE = """
     326TECHNICAL_500_TEMPLATE = r"""
    188327<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
    189328<html lang="en">
    190329<head>
     
    197336    body * * { padding:0; }
    198337    body { font:small sans-serif; }
    199338    body>div { border-bottom:1px solid #ddd; }
     339    a { color: #333; }
    200340    h1 { font-weight:normal; }
    201341    h2 { margin-bottom:.8em; }
    202342    h2 span { font-size:80%; color:#666; font-weight:normal; }
     
    212352    table td.code div { overflow:hidden; }
    213353    table.source th { color:#666; }
    214354    table.source td { font-family:monospace; white-space:pre; border-bottom:1px solid #eee; }
     355    form.debug { display: block; padding: 10px 20px 10px 40px; }
     356    form.debug input { margin-top: 5px; width: 100%; }
    215357    ul.traceback { list-style-type:none; }
    216358    ul.traceback li.frame { margin-bottom:1em; }
    217359    div.context { margin: 10px 0; }
     
    219361    div.context ol li { font-family:monospace; white-space:pre; color:#666; cursor:pointer; }
    220362    div.context ol.context-line li { color:black; background-color:#ccc; }
    221363    div.context ol.context-line li span { float: right; }
    222     div.commands { margin-left: 40px; }
    223     div.commands a { color:black; text-decoration:none; }
     364    ul.commands { margin-left: 40px; padding: 0; list-style: none; }
     365    ul.commands a { color:black; text-decoration:none; }
    224366    #summary { background: #ffc; }
    225367    #summary h2 { font-weight: normal; color: #666; }
    226368    #explanation { background:#eee; }
     
    236378  </style>
    237379  <script type="text/javascript">
    238380  //<!--
    239     function getElementsByClassName(oElm, strTagName, strClassName){
    240         // Written by Jonathan Snook, http://www.snook.ca/jon; Add-ons by Robert Nyman, http://www.robertnyman.com
    241         var arrElements = (strTagName == "*" && document.all)? document.all :
     381    var DEBUG_ID = {% if debugger_enabled %}'{{ debugger_id }}'{% else %}null{% endif %};
     382
     383    function getElementsByClassName(oElm, strTagName, strClassName) {
     384      // Written by Jonathan Snook, http://www.snook.ca/jon; Add-ons by Robert Nyman, http://www.robertnyman.com
     385      var arrElements = (strTagName == "*" && document.all) ? document.all :
    242386        oElm.getElementsByTagName(strTagName);
    243         var arrReturnElements = new Array();
    244         strClassName = strClassName.replace(/\-/g, "\\-");
    245         var oRegExp = new RegExp("(^|\\s)" + strClassName + "(\\s|$)");
    246         var oElement;
    247         for(var i=0; i<arrElements.length; i++){
    248             oElement = arrElements[i];
    249             if(oRegExp.test(oElement.className)){
    250                 arrReturnElements.push(oElement);
    251             }
     387      var arrReturnElements = new Array();
     388      strClassName = strClassName.replace(/\-/g, "\\-");
     389      var oRegExp = new RegExp("(^|\\s)" + strClassName + "(\\s|$)");
     390      var oElement;
     391      for(var i=0; i<arrElements.length; i++) {
     392        oElement = arrElements[i];
     393        if(oRegExp.test(oElement.className)) {
     394          arrReturnElements.push(oElement);
    252395        }
    253         return (arrReturnElements)
     396      }
     397      return arrReturnElements;
    254398    }
    255399    function hideAll(elems) {
    256400      for (var e = 0; e < elems.length; e++) {
     
    259403    }
    260404    window.onload = function() {
    261405      hideAll(getElementsByClassName(document, 'table', 'vars'));
     406      hideAll(getElementsByClassName(document, 'form', 'debug'));
    262407      hideAll(getElementsByClassName(document, 'ol', 'pre-context'));
    263408      hideAll(getElementsByClassName(document, 'ol', 'post-context'));
    264409      hideAll(getElementsByClassName(document, 'div', 'pastebin'));
     
    280425      s.innerHTML = s.innerHTML == uarr ? darr : uarr;
    281426      return false;
    282427    }
     428    function debugToggle(link, id) {
     429      toggle('d' + id);
     430      var s = link.getElementsByTagName('span')[0];
     431      var uarr = String.fromCharCode(0x25b6);
     432      var darr = String.fromCharCode(0x25bc);
     433      s.innerHTML = s.innerHTML == uarr ? darr : uarr;
     434      return false;
     435    }
    283436    function switchPastebinFriendly(link) {
    284437      s1 = "Switch to copy-and-paste view";
    285438      s2 = "Switch back to interactive view";
     
    287440      toggle('browserTraceback', 'pastebinTraceback');
    288441      return false;
    289442    }
     443    function sendDebugCommand(frameID, command, callback) {
     444      var activex = ['Msxml2.XMLHTTP', 'Microsoft.XMLHTTP', 'Msxml2.XMLHTTP.4.0'];
     445      var con = null;
     446      try {
     447        con = new XMLHttpRequest();
     448      }
     449      catch (e) {
     450        for (var i=0; i < activex.length; i++) {
     451          try {
     452            con = new ActiveXObject(activex[i]);
     453          }
     454          catch (e) {};
     455          if (con) {
     456            break;
     457          }
     458        }
     459      }
     460      var data = 'tb=' + DEBUG_ID + '&' +
     461                 'frame=' + frameID + '&' +
     462                 'cmd=' + encodeURIComponent(command);
     463      con.onreadystatechange = function() {
     464        if (con.readyState == 4) {
     465          callback(con.responseText);
     466        }
     467      };
     468      con.open('POST', '?__send_to_django_debugger__=yes');
     469      con.send(data);
     470    }
     471    function submitCommand(frameID) {
     472      var input = document.getElementById('di' + frameID);
     473      var output = document.getElementById('do' + frameID);
     474      output = (output.firstChild || output.appendChild(document.createTextNode('')));
     475      if (input.value == 'clear') {
     476        output.nodeValue = '';
     477      }
     478      else {
     479        sendDebugCommand(frameID, input.value + '\n', function(value) {
     480          output.nodeValue += value;
     481        });
     482      }
     483      input.value = '';
     484      input.focus();
     485      return false;
     486    }
    290487    //-->
    291488  </script>
    292489</head>
     
    314511    </tr>
    315512    <tr>
    316513      <th>Exception Location:</th>
    317       <td>{{ lastframe.filename }} in {{ lastframe.function }}, line {{ lastframe.lineno }}</td>
     514      <td>{{ lastframe.filename|escape }} in {{ lastframe.function|escape }}, line {{ lastframe.lineno }}</td>
    318515    </tr>
    319516  </table>
    320517</div>
     
    361558    <ul class="traceback">
    362559      {% for frame in frames %}
    363560        <li class="frame">
    364           <code>{{ frame.filename }}</code> in <code>{{ frame.function }}</code>
     561          <code>{{ frame.filename|escape }}</code> in <code>{{ frame.function|escape }}</code>
    365562
    366563          {% if frame.context_line %}
    367564            <div class="context" id="c{{ frame.id }}">
     
    375572            </div>
    376573          {% endif %}
    377574
    378           {% if frame.vars %}
    379             <div class="commands">
    380                 <a href="#" onclick="return varToggle(this, '{{ frame.id }}')"><span>&#x25b6;</span> Local vars</a>
    381             </div>
    382             <table class="vars" id="v{{ frame.id }}">
    383               <thead>
    384                 <tr>
    385                   <th>Variable</th>
    386                   <th>Value</th>
    387                 </tr>
    388               </thead>
    389               <tbody>
    390                 {% for var in frame.vars|dictsort:"0" %}
     575          {% if frame.vars or debugger_enabled %}
     576            <ul class="commands">
     577              {% if frame.vars %}
     578                <li><a href="#" onclick="return varToggle(this, '{{ frame.id }}')"><span>&#x25b6;</span> Local vars</a></li>
     579              {% endif %}
     580              {% if debugger_enabled %}
     581                <li><a href="#" onclick="return debugToggle(this, '{{ frame.id }}')"><span>&#x25b6;</span> Debug</a></li>
     582              {% endif %}
     583            </ul>
     584            {% if frame.vars %}
     585              <table class="vars" id="v{{ frame.id }}">
     586                <thead>
    391587                  <tr>
    392                     <td>{{ var.0 }}</td>
    393                     <td class="code"><div>{{ var.1|pprint|escape }}</div></td>
     588                    <th>Variable</th>
     589                    <th>Value</th>
    394590                  </tr>
    395                 {% endfor %}
    396               </tbody>
    397             </table>
     591                </thead>
     592                <tbody>
     593                  {% for var in frame.vars|dictsort:"0" %}
     594                    <tr>
     595                      <td>{{ var.0 }}</td>
     596                      <td class="code"><div>{{ var.1|pprint|escape }}</div></td>
     597                    </tr>
     598                  {% endfor %}
     599                </tbody>
     600              </table>
     601            {% endif %}
     602            {% if debugger_enabled %}
     603              <form class="debug" id="d{{ frame.id }}" onsubmit="return submitCommand('{{ frame.id }}')">
     604                <pre id="do{{ frame.id }}"></pre>
     605                <input type="text" id="di{{ frame.id }}">
     606              </form>
     607            {% endif %}
    398608          {% endif %}
    399609        </li>
    400610      {% endfor %}
  • django/middleware/common.py

     
    2424        settings.APPEND_SLASH and settings.PREPEND_WWW
    2525        """
    2626
     27        # Handle Debugger AJAX Requests
     28        if settings.DEBUGGER_ENABLED and \
     29           request.GET.get('__send_to_django_debugger__') == 'yes':
     30            from django.views.debug import console
     31            rv = console.send(
     32                debugger=request.POST.get('tb'),
     33                frame=request.POST.get('frame'),
     34                cmd=request.POST.get('cmd', '')
     35            )
     36            return http.HttpResponse(rv, mimetype='text/plain')
     37
    2738        # Check for denied User-Agents
    2839        if request.META.has_key('HTTP_USER_AGENT'):
    2940            for user_agent_regex in settings.DISALLOWED_USER_AGENTS:
Back to Top