diff --git a/examples/foo.py b/examples/foo.py new file mode 100755 index 0000000..444c155 --- /dev/null +++ b/examples/foo.py @@ -0,0 +1,27 @@ +#!/usr/bin/env python + +from __future__ import print_function + +import logging +import rpdb + +logger = logging.getLogger() +logger.setLevel(logging.DEBUG) +if not logger.handlers: + lh = logging.StreamHandler() + logger.addHandler(lh) + +def main(): + count = 0 + while count < 10: + rpdb.set_trace() + print("here %d" % count) + count += 1 + + print("done") + +try: + main() +except Exception as e: + logger.exception(e) + rpdb.post_mortem() diff --git a/rpdb/__init__.py b/rpdb/__init__.py index 7ba2f14..9f82e7c 100644 --- a/rpdb/__init__.py +++ b/rpdb/__init__.py @@ -3,6 +3,7 @@ __author__ = "Bertrand Janin " __version__ = "0.1.6" +import logging import pdb import socket import threading @@ -11,6 +12,9 @@ import traceback from functools import partial +_logger = logging.getLogger(__name__) +_logger.addHandler(logging.NullHandler()) + DEFAULT_ADDR = "127.0.0.1" DEFAULT_PORT = 4444 @@ -30,11 +34,21 @@ def __getattr__(self, attr): return attr -class Rpdb(pdb.Pdb): +def finally_shutdown(owner, method): + """Wrapper to a clean-up happens after `method` is called.""" + def _wrapper(*args, **kwargs): + """Clean-up after calling method.""" + try: + return method(*args, **kwargs) + finally: + owner.shutdown() + return _wrapper - def __init__(self, addr=DEFAULT_ADDR, port=DEFAULT_PORT): - """Initialize the socket and initialize pdb.""" +class Rpdb(object): + """Wrap a Pdb object with a remote session.""" + + def __init__(self, addr=DEFAULT_ADDR, port=DEFAULT_PORT): # Backup stdin and stdout before replacing them by the socket handle self.old_stdout = sys.stdout self.old_stdin = sys.stdin @@ -46,52 +60,42 @@ def __init__(self, addr=DEFAULT_ADDR, port=DEFAULT_PORT): self.skt.bind((addr, port)) self.skt.listen(1) - # Writes to stdout are forbidden in mod_wsgi environments - try: - sys.stderr.write("pdb is running on %s:%d\n" - % self.skt.getsockname()) - except IOError: - pass + _logger.info("pdb is running on %s:%d" % self.skt.getsockname()) + + self._pdb = None + def start_debugger(self): + """Accept external connections and run Pdb.""" (clientsocket, address) = self.skt.accept() handle = clientsocket.makefile('rw') - pdb.Pdb.__init__(self, completekey='tab', - stdin=FileObjectWrapper(handle, self.old_stdin), - stdout=FileObjectWrapper(handle, self.old_stdin)) + self._pdb = pdb.Pdb(completekey='tab', + stdin=FileObjectWrapper(handle, self.old_stdin), + stdout=FileObjectWrapper(handle, self.old_stdout)) + # wrap the methods that need extra logic + for method in ('do_continue', 'do_c', 'do_cont', + 'do_quit', 'do_exit', 'do_q', + 'do_EOF'): + setattr(self._pdb, method, finally_shutdown(self, getattr(self._pdb, method))) + sys.stdout = sys.stdin = handle - OCCUPIED.claim(port, sys.stdout) + self.handle = handle + OCCUPIED.claim(self.port, self.handle) + _logger.debug("pdb client connected") def shutdown(self): """Revert stdin and stdout, close the socket.""" sys.stdout = self.old_stdout sys.stdin = self.old_stdin + self.handle.close() OCCUPIED.unclaim(self.port) + self.skt.shutdown(socket.SHUT_RDWR) self.skt.close() - def do_continue(self, arg): - """Clean-up and do underlying continue.""" - try: - return pdb.Pdb.do_continue(self, arg) - finally: - self.shutdown() - - do_c = do_cont = do_continue - - def do_quit(self, arg): - """Clean-up and do underlying quit.""" - try: - return pdb.Pdb.do_quit(self, arg) - finally: - self.shutdown() - - do_q = do_exit = do_quit - - def do_EOF(self, arg): - """Clean-up and do underlying EOF.""" - try: - return pdb.Pdb.do_EOF(self, arg) - finally: - self.shutdown() + def __getattr__(self, name): + """Pass on requests to the Pdb object.""" + if hasattr(self._pdb, name): + return getattr(self._pdb, name) + return self.__getattribute__(self, name) def set_trace(addr=DEFAULT_ADDR, port=DEFAULT_PORT, frame=None): @@ -102,10 +106,11 @@ def set_trace(addr=DEFAULT_ADDR, port=DEFAULT_PORT, frame=None): """ try: debugger = Rpdb(addr=addr, port=port) + debugger.start_debugger() except socket.error: if OCCUPIED.is_claimed(port, sys.stdout): # rpdb is already on this port - good enough, let it go on: - sys.stdout.write("(Recurrent rpdb invocation ignored)\n") + _logger.info("Recurrent rpdb invocation ignored") return else: # Port occupied by something else. @@ -129,6 +134,7 @@ def post_mortem(addr=DEFAULT_ADDR, port=DEFAULT_PORT): debugger = Rpdb(addr=addr, port=port) type, value, tb = sys.exc_info() traceback.print_exc() + debugger.start_debugger() debugger.reset() debugger.interaction(None, tb)