From 18cd5a4364115ddc234cf74e093e3129edf3a0b5 Mon Sep 17 00:00:00 2001 From: Sean Perry Date: Thu, 17 Mar 2016 14:56:19 -0700 Subject: [PATCH 1/3] Ensure socket closes properly The socket is being held open by the writable handle. Close both the handle and the socket. To do this we need to store the handle as a member variable. --- rpdb/__init__.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/rpdb/__init__.py b/rpdb/__init__.py index 7ba2f14..c79c622 100644 --- a/rpdb/__init__.py +++ b/rpdb/__init__.py @@ -59,13 +59,16 @@ def __init__(self, addr=DEFAULT_ADDR, port=DEFAULT_PORT): stdin=FileObjectWrapper(handle, self.old_stdin), stdout=FileObjectWrapper(handle, self.old_stdin)) sys.stdout = sys.stdin = handle + self.handle = handle OCCUPIED.claim(port, sys.stdout) 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): From 031b685d60648952a0b1f0b6d6b9257b8334250a Mon Sep 17 00:00:00 2001 From: Sean Perry Date: Thu, 17 Mar 2016 16:40:07 -0700 Subject: [PATCH 2/3] Refactor Rpdb object. Rpdb now contains a Pdb instance instead of deriving from one. This lets the Rpdb object be created separately from the socket accept() call. Closes #4. --- rpdb/__init__.py | 65 +++++++++++++++++++++++++----------------------- 1 file changed, 34 insertions(+), 31 deletions(-) diff --git a/rpdb/__init__.py b/rpdb/__init__.py index c79c622..6a27d32 100644 --- a/rpdb/__init__.py +++ b/rpdb/__init__.py @@ -30,11 +30,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 @@ -53,14 +63,24 @@ def __init__(self, addr=DEFAULT_ADDR, port=DEFAULT_PORT): except IOError: pass + 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 self.handle = handle - OCCUPIED.claim(port, sys.stdout) + OCCUPIED.claim(self.port, self.handle) def shutdown(self): """Revert stdin and stdout, close the socket.""" @@ -71,30 +91,11 @@ def shutdown(self): 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): @@ -105,6 +106,7 @@ 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: @@ -132,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) From 96f9901beea8d5552f45862a85f955c569a5fa97 Mon Sep 17 00:00:00 2001 From: Sean Perry Date: Thu, 17 Mar 2016 16:58:20 -0700 Subject: [PATCH 3/3] Move from printing to logging Setup and use Python's logging module instead of printing directly to stdout or stderr. Also, add an example program used for testing. --- examples/foo.py | 27 +++++++++++++++++++++++++++ rpdb/__init__.py | 14 +++++++------- 2 files changed, 34 insertions(+), 7 deletions(-) create mode 100755 examples/foo.py 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 6a27d32..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 @@ -56,12 +60,7 @@ 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 @@ -81,6 +80,7 @@ def start_debugger(self): sys.stdout = sys.stdin = handle self.handle = handle OCCUPIED.claim(self.port, self.handle) + _logger.debug("pdb client connected") def shutdown(self): """Revert stdin and stdout, close the socket.""" @@ -110,7 +110,7 @@ def set_trace(addr=DEFAULT_ADDR, port=DEFAULT_PORT, frame=None): 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.