From 4f1d3e79b4c64ee4488c38c4f74d24690f06c7d3 Mon Sep 17 00:00:00 2001 From: Massimo Di Pierro Date: Sun, 19 Feb 2012 22:30:59 -0600 Subject: [PATCH] debugger --- VERSION | 2 +- applications/admin/controllers/debug.py | 162 +++++++++++++++++++- applications/admin/models/access.py | 4 +- applications/admin/models/menu.py | 2 + applications/admin/static/css/styles.css | 13 ++ applications/admin/static/js/ajax_editor.js | 48 ++++++ applications/admin/views/default/edit.html | 8 + gluon/dal.py | 2 +- gluon/debug.py | 100 ++++++++++++ gluon/highlight.py | 9 ++ gluon/html.py | 2 + gluon/main.py | 8 +- 12 files changed, 353 insertions(+), 7 deletions(-) diff --git a/VERSION b/VERSION index d68ce612..113d0fdb 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -Version 1.99.4 (2012-02-19 22:24:06) stable +Version 1.99.4 (2012-02-19 22:30:50) stable diff --git a/applications/admin/controllers/debug.py b/applications/admin/controllers/debug.py index a2c8e31c..ce30f15e 100644 --- a/applications/admin/controllers/debug.py +++ b/applications/admin/controllers/debug.py @@ -1,8 +1,13 @@ +import os import sys import cStringIO import gluon.contrib.shell +import gluon.dal +import gluon.html +import gluon.validators import code, thread -from gluon.debug import communicate +from gluon.debug import communicate, web_debugger, qdb_debugger +import pydoc if DEMO_MODE or MULTI_USER_MODE: @@ -32,4 +37,159 @@ def reset(): return 'done' +# new implementation using qdb + +def interact(): + app = request.args(0) or 'admin' + reset() + + # process all pending messages in the frontend + web_debugger.run() + + # if debugging, filename and lineno should have valid values + filename = web_debugger.filename + lineno = web_debugger.lineno + if filename: + lines = dict([(i+1, l) for (i, l) in enumerate( + [l.strip("\n").strip("\r") for l + in open(filename).readlines()])]) + filename = os.path.basename(filename) + else: + lines = {} + + if filename: + env = web_debugger.do_environment() + f_locals = env['locals'] + f_globals = {} + for name, value in env['globals'].items(): + if name not in gluon.html.__all__ and \ + name not in gluon.validators.__all__ and \ + name not in gluon.dal.__all__: + f_globals[name] = pydoc.text.repr(value) + else: + f_locals = {} + f_globals = {} + response.headers['refresh'] = "3" + + if web_debugger.exception_info: + response.flash = T('"User Exception" debug mode. ' + 'An error ticket could be issued!') + + return dict(app=app, data="", + filename=web_debugger.filename, lines=lines, lineno=lineno, + f_globals=f_globals, f_locals=f_locals, + exception=web_debugger.exception_info) + +def step(): + web_debugger.do_step() + redirect(URL("interact")) + +def next(): + web_debugger.do_next() + redirect(URL("interact")) + +def cont(): + web_debugger.do_continue() + redirect(URL("interact")) + +def ret(): + web_debugger.do_return() + redirect(URL("interact")) + +def stop(): + web_debugger.do_quit() + redirect(URL("interact")) + +def execute(): + app = request.args[0] + command = request.vars.statement + session['debug_commands:'+app].append(command) + try: + output = web_debugger.do_exec(command) + if output is None: + output = "" + except Exception, e: + output = T("Exception %s") % str(e) + k = len(session['debug_commands:'+app]) - 1 + return '[%i] %s%s\n' % (k + 1, command, output) + + +def breakpoints(): + "Add or remove breakpoints" + + # Get all .py files + files = listdir(apath('', r=request), '.*\.py$') + files = [filename for filename in files + if filename and 'languages' not in filename + and not filename.startswith("admin") + and not filename.startswith("examples")] + + form = SQLFORM.factory( + Field('filename', requires=IS_IN_SET(files), label=T("Filename")), + Field('lineno', 'integer', label=T("Line number"), + requires=IS_NOT_EMPTY()), + Field('temporary', 'boolean', label=T("Temporary"), + comment=T("deleted after first hit")), + Field('condition', 'string', label=T("Condition"), + comment=T("honored only if the expression evaluates to true")), + ) + + if form.accepts(request.vars, session): + filename = os.path.join(request.env['applications_parent'], + 'applications', form.vars.filename) + err = qdb_debugger.do_set_breakpoint(filename, + form.vars.lineno, + form.vars.temporary, + form.vars.condition) + response.flash = T("Set Breakpoint on %s at line %s: %s") % ( + filename, form.vars.lineno, err or T('successful')) + + for item in request.vars: + if item[:7] == 'delete_': + qdb_debugger.do_clear(item[7:]) + + breakpoints = [{'number': bp[0], 'filename': os.path.basename(bp[1]), + 'path': bp[1], 'lineno': bp[2], + 'temporary': bp[3], 'enabled': bp[4], 'hits': bp[5], + 'condition': bp[6]} + for bp in qdb_debugger.do_list_breakpoint()] + + return dict(breakpoints=breakpoints, form=form) + + +def toggle_breakpoint(): + "Set or clear a breakpoint" + + lineno = None + ok = None + try: + filename = os.path.join(request.env['applications_parent'], + 'applications', request.vars.filename) + start = 0 + sel_start = int(request.vars.sel_start) + for lineno, line in enumerate(request.vars.data.split("\n")): + if sel_start <= start: + break + start += len(line) + 1 + else: + lineno = None + if lineno is not None: + for bp in qdb_debugger.do_list_breakpoint(): + no, bp_filename, bp_lineno, temporary, enabled, hits, cond = bp + if filename == bp_filename and lineno == bp_lineno: + err = qdb_debugger.do_clear_breakpoint(filename, lineno) + response.flash = T("Removed Breakpoint on %s at line %s", ( + filename, lineno)) + ok = False + break + else: + err = qdb_debugger.do_set_breakpoint(filename, lineno) + response.flash = T("Set Breakpoint on %s at line %s: %s") % ( + filename, lineno, err or T('successful')) + ok = True + else: + response.flash = T("Unable to determine the line number!") + except Exception, e: + session.flash = str(e) + return response.json({'ok': ok, 'lineno': lineno}) diff --git a/applications/admin/models/access.py b/applications/admin/models/access.py index 26c90b28..3344294b 100644 --- a/applications/admin/models/access.py +++ b/applications/admin/models/access.py @@ -120,7 +120,9 @@ if session.authorized: else: session.last_time = t0 -if not session.authorized and not \ +if request.controller == "webservices": + pass +elif not session.authorized and not \ (request.controller == 'default' and \ request.function in ('index','user')): diff --git a/applications/admin/models/menu.py b/applications/admin/models/menu.py index 6963c596..aed119f3 100644 --- a/applications/admin/models/menu.py +++ b/applications/admin/models/menu.py @@ -26,6 +26,8 @@ if not session.authorized: else: response.menu.append((T('Logout'), False, URL(_a,'default',f='logout'))) + response.menu.append((T('Debug'), False, + URL(_a, 'debug','interact'))) if os.path.exists('applications/examples'): response.menu.append((T('Help'), False, URL('examples','default','index'))) diff --git a/applications/admin/static/css/styles.css b/applications/admin/static/css/styles.css index abf9f399..7fc8b0ff 100644 --- a/applications/admin/static/css/styles.css +++ b/applications/admin/static/css/styles.css @@ -930,6 +930,19 @@ ul#snapshot > li { background-image: url(../images/dim_bullet.gif); } +/* Debug */ + +.debug h3.not_paused { + background-image: url(../images/red_bullet.gif); +} + +.debug h3.exception { + margin-top: 0.5em; + background: url(../images/ticket_section.png) no-repeat; + padding: 5px 40px; +} + + /* Footer */ #footer { diff --git a/applications/admin/static/js/ajax_editor.js b/applications/admin/static/js/ajax_editor.js index 48c0e14a..508282d7 100644 --- a/applications/admin/static/js/ajax_editor.js +++ b/applications/admin/static/js/ajax_editor.js @@ -36,6 +36,7 @@ function doClickSave() { prepareDataForSave('data', data), prepareDataForSave('file_hash', jQuery("input[name='file_hash']").val()), prepareDataForSave('saved_on', jQuery("input[name='saved_on']").val()), + prepareDataForSave('saved_on', jQuery("input[name='saved_on']").val()), prepareDataForSave('from_ajax','true'))); // console.info(area.textarea.value); jQuery("input[name='saved_on']").attr('style','background-color:yellow'); @@ -94,6 +95,53 @@ function doClickSave() { return false; } +function doToggleBreakpoint(filename, url) { + try { + var data = eamy.instances[0].getText(); + } catch(e) { + var data = area.textarea.value; + var sel = editAreaLoader.getSelectionRange('body'); + } + var dataForPost = prepareMultiPartPOST(new Array( + prepareDataForSave('filename', filename), + prepareDataForSave('sel_start', sel["start"]), + prepareDataForSave('sel_end', sel["end"]), + prepareDataForSave('data', data))); + jQuery.ajax({ + type: "POST", + contentType: 'multipart/form-data;boundary="' + dataForPost[1] + '"', + url: url, + dataType: "json", + data: dataForPost[0], + timeout: 5000, + beforeSend: function(xhr) { + xhr.setRequestHeader('web2py-component-location',document.location); + xhr.setRequestHeader('web2py-component-element','doSetBreakpoint');}, + success: function(json,text,xhr){ + + // show flash message (if any) + var flash=xhr.getResponseHeader('web2py-component-flash'); + if (flash) jQuery('.flash').html(flash).slideDown(); + else jQuery('.flash').hide(); + try { + if (json.error) { + window.location.href=json.redirect; + } else { + // mark the breakpoint if ok=True, remove mark if ok=False + // do nothing if ok = null + // alert(json.ok + json.lineno); + } + } catch(e) { + on_error(); + } + + }, + error: function(json) { on_error(); } + }); + return false; +} + + function keepalive(url) { jQuery.ajax({ type: "GET", diff --git a/applications/admin/views/default/edit.html b/applications/admin/views/default/edit.html index 768aff65..c2491762 100644 --- a/applications/admin/views/default/edit.html +++ b/applications/admin/views/default/edit.html @@ -36,6 +36,14 @@ jQuery(document).ready(function(){ {{pass}}

+ {{if filetype=='python':}} + {{=A(SPAN(T('toggle breakpoint')), + _value="breakpoint", _name="breakpoint", + _onclick="return doToggleBreakpoint('%s','%s://%s%s');" % (filename, + request.env['wsgi_url_scheme'], request.env['http_host'], + URL(c='debug', f='toggle_breakpoint')), + _class="button special")}} + {{pass}} {{=button(URL('design',args=request.args[0]), T('back'))}} {{if edit_controller:}} {{=button(edit_controller, T('edit controller'))}} diff --git a/gluon/dal.py b/gluon/dal.py index 39f6084c..aa0958ec 100644 --- a/gluon/dal.py +++ b/gluon/dal.py @@ -1970,7 +1970,7 @@ class PostgreSQLAdapter(BaseAdapter): if self.drivers.get('psycopg2'): self.driver = self.drivers['psycopg2'] elif self.drivers.get('pg8000'): - self.driver = drivers['pg8000'] + self.driver = drivers.get('pg8000') elif library == "postgres:psycopg2": self.driver = self.drivers.get('psycopg2') elif library == "postgres:pg8000": diff --git a/gluon/debug.py b/gluon/debug.py index 83dec38b..0fa7a33d 100644 --- a/gluon/debug.py +++ b/gluon/debug.py @@ -10,6 +10,7 @@ License: LGPLv3 (http://www.gnu.org/licenses/lgpl.html) """ import logging +import os import pdb import Queue import sys @@ -82,7 +83,106 @@ def communicate(command=None): return ''.join(result) +# New debugger implementation using qdb and a web UI + +import gluon.contrib.qdb as qdb +from gluon import portalocker + + +def lock(name=''): + from gluon.globals import current + locker = open(os.path.join(current.request.folder, 'debug_%s.lock' % name), + 'a') + portalocker.lock(locker, portalocker.LOCK_EX) + return locker + + +def unlock(locker): + portalocker.unlock(locker) + locker.close() + + +def check_interaction(fn): + "Decorator to clean and prevent interaction when not available" + def check_fn(self, *args, **kwargs): + locker = lock('interact') + try: + if self.filename: + self.clear_interaction() + fn(self, *args, **kwargs) + finally: + unlock(locker) + return check_fn + + +class WebDebugger(qdb.Frontend): + "Qdb web2py interface" + + def __init__(self, pipe, completekey='tab', stdin=None, stdout=None, skip=None): + qdb.Frontend.__init__(self, pipe) + self.clear_interaction() + + def clear_interaction(self): + self.filename = None + self.lineno = None + self.exception_info = None + + # redefine Frontend methods: + + def run(self): + locker = lock('run') + try: + while self.pipe.poll(): + qdb.Frontend.run(self) + finally: + unlock(locker) + + def interaction(self, filename, lineno, line): + # store current status + locker = lock('interact') + try: + self.filename = filename + self.lineno = lineno + finally: + unlock(locker) + + def exception(self, title, extype, exvalue, trace, request): + self.exception_info = {'title': title, + 'extype': extype, 'exvalue': exvalue, + 'trace': trace, 'request': request} + + @check_interaction + def do_continue(self): + qdb.Frontend.do_continue(self) + + @check_interaction + def do_step(self): + qdb.Frontend.do_step(self) + + @check_interaction + def do_return(self): + qdb.Frontend.do_return(self) + + @check_interaction + def do_next(self): + qdb.Frontend.do_next(self) + + @check_interaction + def do_quit(self): + qdb.Frontend.do_quit(self) +# create the connection between threads: + +parent_queue, child_queue = Queue.Queue(), Queue.Queue() +front_conn = qdb.QueuePipe("parent", parent_queue, child_queue) +child_conn = qdb.QueuePipe("child", child_queue, parent_queue) + +web_debugger = WebDebugger(front_conn) # frontend +qdb_debugger = qdb.Qdb(pipe=child_conn, redirect_stdio=False) # backend +dbg = qdb_debugger + +import gluon.main +gluon.main.global_settings.debugging = True diff --git a/gluon/highlight.py b/gluon/highlight.py index 7b95f1d7..fb68a62d 100644 --- a/gluon/highlight.py +++ b/gluon/highlight.py @@ -255,6 +255,7 @@ def highlight( counter=1, styles=None, highlight_line=None, + context_lines=None, attributes=None, ): styles = styles or {} @@ -311,6 +312,14 @@ def highlight( lines[lineno] = '

%s
' % (linehighlight_style, lines[lineno]) linenumbers[lineno] = '
%s
' % (linehighlight_style, linenumbers[lineno]) + if context_lines: + if lineno + context_lines < len(lines): + del lines[lineno + context_lines:] + del linenumbers[lineno + context_lines:] + if lineno -context_lines > 0: + del lines[0:lineno - context_lines] + del linenumbers[0:lineno - context_lines] + code = '
'.join(lines) numbers = '
'.join(linenumbers) diff --git a/gluon/html.py b/gluon/html.py index 26cc7f65..b3539161 100644 --- a/gluon/html.py +++ b/gluon/html.py @@ -1413,6 +1413,7 @@ class CODE(DIV): link = self['link'] counter = self.attributes.get('counter', 1) highlight_line = self.attributes.get('highlight_line', None) + context_lines = self.attributes.get('context_lines', None) styles = self['styles'] or {} return highlight( join(self.components), @@ -1422,6 +1423,7 @@ class CODE(DIV): styles=styles, attributes=self.attributes, highlight_line=highlight_line, + context_lines=context_lines, ) diff --git a/gluon/main.py b/gluon/main.py index 7210b443..36b970df 100644 --- a/gluon/main.py +++ b/gluon/main.py @@ -490,6 +490,11 @@ def wsgibase(environ, responder): # run controller # ################################################## + if global_settings.debugging and request.application != "admin": + import gluon.debug + # activate the debugger and wait to reach application code + gluon.debug.dbg.do_debug(mainpyfile=request.folder) + serve_controller(request, response, session) except HTTP, http_response: @@ -591,9 +596,6 @@ def wsgibase(environ, responder): if response and hasattr(response, 'session_file') \ and response.session_file: response.session_file.close() -# if global_settings.debugging: -# import gluon.debug -# gluon.debug.stop_trace() session._unlock(response) http_response, new_environ = rewrite.try_rewrite_on_error(