Search code examples
pythonshellsocketsgevent

How to make stdin/stdout redirection of pdb, in the context of a remote shell?


I am writing a web shell, using ptpython, gevent and Flask on the server side and xtermjs and websockets on the client side.

I would like the normal breakpoint() function of Python to work, using pdb.

I was happy to find out Pdb class takes custom stdin and stdout, however I cannot manage to get the thing working :(

In the web shell, what user types goes to the server process via a websocket: there is a listening greenlet, that writes to a pipe that is read by ptpython. Then, ptpython output is sent via the websocket. So far so good, it works beautifully.

Now with pdb: thanks to sys.breakpointhook I create a custom Pdb instance with stdin being a pipe and stdout another pipe. I carefully use gevent.fileobject.FileObjectPosix to have non-blocking, cooperative I/O streams. I take care of having user input writing to the input pipe and I expect output to the other pipe, so I can redirect it to the appropriate "standard output" handling routine in my code.

But, after having received the first messages corresponding to pdb prompt, I am stuck everything seems to be blocked.

I reproduce the behaviour I have with the following code, any help would be appreciated:

from pdb import Pdb as _Pdb
import gevent
from gevent.fileobject import FileObjectPosix as File
import io
import os
import sys
import socket

debugger = None


class GeventPdb(_Pdb):
    def __init__(self, own_stdout):
        self._stdout = own_stdout
        r1, w1 = os.pipe()
        r2, w2 = os.pipe()
        self.r = File(r1)
        self.w = File(w1, "w")
        self.r2 = File(r2)
        self.w2 = File(w2, "w")
        super().__init__(stdin=self.r, stdout=self.w2)
        self.stdout_handling = gevent.spawn(self.handle_stdout)

    def send_text(self, txt):
        # this should write to the 'stdin' pipe,
        # thus provoking input reading from Pdb
        print("WRITING TO PDB's STDIN")
        self.w.write(txt.decode())

    def handle_stdout(self):
        # this should read what is written by Pdb
        # to Pdb's stdout, to redirect it to our
        # own stdout
        while True:
            print("BEFORE READING PDB's STDOUT")
            char = self.r2.read(1)
            print(f"WRITING TO CUSTOM STDOUT, stdout len is {len(self._stdout.getvalue())}")
            self._stdout.write(char)

def test_func():
    debugger.set_trace()

if __name__ == '__main__':
    custom_stdout = io.StringIO() # this simulates a custom stdout pipe
    debugger = GeventPdb(custom_stdout)

    # the next socket is the server socket,
    # for 'remote' operation
    s = socket.socket()
    s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
    s.bind(('', 4444))
    s.listen(1)

    print("Connect client to continue... The client is used to send commands to the debugger")
    client, client_addr = s.accept()

    def handle_client(client):
      while True:
        cmd = client.recv(1024)
        if not cmd:
            break
        print("SENDING TEXT TO DEBUGGER...")
        debugger.send_text(cmd)

    gevent.spawn(handle_client, client)

    print("now start a function which starts the debugger...")
    print("we should see the pdb prompt")
    test_func()

I ran the following code, then connect via telnet localhost 4444 then I can see I receive pdb prompt, then I can type commands and press [enter]: nothing seems to be received on pdb's input pipe. It blocks.

Any help would be appreciated !


Solution

  • You must flush your write (in addition to monkey patching):

        def send_text(self, txt):
            # this should write to the 'stdin' pipe,
            # thus provoking input reading from Pdb
            print("WRITING TO PDB's STDIN")
            self.w.write(txt.decode())
            self.w.flush()                   # !!!