diff --git a/README.rst b/README.rst index 6107f3a..dd893f7 100644 --- a/README.rst +++ b/README.rst @@ -100,6 +100,20 @@ Note that newer Ubuntu disabled readline support in socat, so if you get rlwrap socat - tcp:127.0.0.1:4444 +Reverse connection +================== + +To use a reverse connection, install netcat on your system, determine your host IP (using ip addr or ifconfig), +and run a listener on your host, e.g., ``nc -lvp 8888``. To enable a reverse connection in your code, use +the ``reverse=True`` option as follows: + +.. code:: python + + import remote_pdb + rpdb = remote_pdb.RemotePdb(host='your_host_ip', port=8888, reverse=True) + rpdb.set_trace() + + Using in containers =================== @@ -141,6 +155,10 @@ To quiet the output, set ``REMOTE_PDB_QUIET=1``, this will prevent ``RemotePdb`` from producing any output -- you'll probably want to specify ``REMOTE_PDB_PORT`` as well since the randomized port won't be printed. +If you want to use a reverse connection, you can set ``REMOTE_PDB_REVERSE=1``. +In this case, the host specified by ``REMOTE_PDB_HOST`` and the port ``REMOTE_PDB_PORT`` will be used as the address +to connect to. Be careful: the reverse connection will not work if the host is not reachable from the container, +which may cause additional errors. Note about OS X =============== diff --git a/src/remote_pdb.py b/src/remote_pdb.py index 265fc9e..f065421 100644 --- a/src/remote_pdb.py +++ b/src/remote_pdb.py @@ -67,18 +67,22 @@ class RemotePdb(Pdb): """ active_instance = None - def __init__(self, host, port, patch_stdstreams=False, quiet=False): + def __init__(self, host, port, patch_stdstreams=False, quiet=False, reverse=False): self._quiet = quiet listen_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) listen_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, True) - listen_socket.bind((host, port)) - if not self._quiet: - cry("RemotePdb session open at %s:%s, waiting for connection ..." % listen_socket.getsockname()) - listen_socket.listen(1) - connection, address = listen_socket.accept() - if not self._quiet: - cry("RemotePdb accepted connection from %s." % repr(address)) - self.handle = LF2CRLF_FileWrapper(connection) + if reverse: + listen_socket.connect((host, port)) + self.handle = LF2CRLF_FileWrapper(listen_socket) + else: + listen_socket.bind((host, port)) + if not self._quiet: + cry("RemotePdb session open at %s:%s, waiting for connection ..." % listen_socket.getsockname()) + listen_socket.listen(1) + connection, address = listen_socket.accept() + if not self._quiet: + cry("RemotePdb accepted connection from %s." % repr(address)) + self.handle = LF2CRLF_FileWrapper(connection) Pdb.__init__(self, completekey='tab', stdin=self.handle, stdout=self.handle) self.backup = [] if patch_stdstreams: @@ -118,7 +122,7 @@ def set_trace(self, frame=None): raise -def set_trace(host=None, port=None, patch_stdstreams=False, quiet=None): +def set_trace(host=None, port=None, patch_stdstreams=False, quiet=None, reverse=None): """ Opens a remote PDB on first available port. """ @@ -128,5 +132,7 @@ def set_trace(host=None, port=None, patch_stdstreams=False, quiet=None): port = int(os.environ.get('REMOTE_PDB_PORT', '0')) if quiet is None: quiet = bool(os.environ.get('REMOTE_PDB_QUIET', '')) - rdb = RemotePdb(host=host, port=port, patch_stdstreams=patch_stdstreams, quiet=quiet) + if reverse is None: + reverse = bool(os.environ.get('REMOTE_PDB_REVERSE', '')) + rdb = RemotePdb(host=host, port=port, patch_stdstreams=patch_stdstreams, quiet=quiet, reverse=reverse) rdb.set_trace(frame=sys._getframe().f_back) diff --git a/tests/test_remote_pdb.py b/tests/test_remote_pdb.py index c085b5d..cec7d02 100644 --- a/tests/test_remote_pdb.py +++ b/tests/test_remote_pdb.py @@ -138,6 +138,27 @@ def test_trash_input(): wait_for_strings(proc.read, TIMEOUT, 'DIED.') +def test_reverse_connection(): + server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + with server_socket: + server_socket.bind(('127.0.0.1', 0)) + server_socket.listen(1) + host, port = server_socket.getsockname() + + with TestProcess(sys.executable, __file__, 'daemon', 'test_reverse_connection', host, str(port)) as proc: + with dump_on_error(proc.read): + wait_for_strings(proc.read, TIMEOUT, '{a1}', '{b1}') + + client_socket, addr = server_socket.accept() + with client_socket: + with TestSocket(client_socket) as client: + with dump_on_error(client.read): + wait_for_strings(client.read, TIMEOUT, "-> print('{b2}')") + client.fh.write(b'continue\r\n') + client.fh.flush() + + wait_for_strings(proc.read, TIMEOUT, 'DIED.') + def func_b(**kwargs): print('{b1}') set_trace(**kwargs) @@ -173,6 +194,10 @@ def func_a(block=lambda _: None, **kwargs): elif test_name == 'test_redirect': func_a(patch_stdstreams=True) time.sleep(TIMEOUT) + elif test_name == 'test_reverse_connection': + host = sys.argv[3] + port = int(sys.argv[4]) + func_a(host=host, port=port, reverse=True) else: raise RuntimeError('Invalid test spec %r.' % test_name) logging.info('DIED.')