Plain is headed towards 1.0! Subscribe for development updates →

  1import errno
  2import logging
  3import re
  4import socket
  5import sys
  6from collections.abc import Iterator
  7from pdb import Pdb
  8from types import FrameType
  9from typing import Any
 10
 11log = logging.getLogger(__name__)
 12
 13
 14def cry(message: str, stderr: Any = sys.__stderr__) -> None:
 15    log.critical(message)
 16    print(message, file=stderr)
 17    stderr.flush()
 18
 19
 20class LF2CRLF_FileWrapper:
 21    _send: Any  # Can be lambda or socket.sendall
 22
 23    def __init__(self, connection: socket.socket) -> None:
 24        self.connection = connection
 25        self.stream = fh = connection.makefile("rw")
 26        self.read = fh.read
 27        self.readline = fh.readline
 28        self.readlines = fh.readlines
 29        self.close = fh.close
 30        self.flush = fh.flush
 31        self.fileno = fh.fileno
 32        if hasattr(fh, "encoding"):
 33            self._send = lambda data: connection.sendall(data.encode(fh.encoding))
 34        else:
 35            self._send = connection.sendall
 36
 37    @property
 38    def encoding(self) -> str | None:
 39        return self.stream.encoding
 40
 41    def __iter__(self) -> Iterator[str]:
 42        return self.stream.__iter__()
 43
 44    def write(self, data: str, nl_rex: re.Pattern[str] = re.compile("\r?\n")) -> None:
 45        data = nl_rex.sub("\r\n", data)
 46        self._send(data)
 47
 48    def writelines(
 49        self, lines: list[str], nl_rex: re.Pattern[str] = re.compile("\r?\n")
 50    ) -> None:
 51        for line in lines:
 52            self.write(line, nl_rex)
 53
 54
 55class DevPdb(Pdb):
 56    """
 57    This will run pdb as a ephemeral telnet service. Once you connect no one
 58    else can connect. On construction this object will block execution till a
 59    client has connected.
 60
 61    Based on https://github.com/tamentis/rpdb I think ...
 62
 63    To use this::
 64
 65        DevPdb(host='0.0.0.0', port=4444).set_trace()
 66
 67    Then run: telnet 127.0.0.1 4444
 68    """
 69
 70    active_instance = None
 71
 72    def __init__(
 73        self, host: str, port: int, patch_stdstreams: bool = False, quiet: bool = False
 74    ) -> None:
 75        self._quiet = quiet
 76        listen_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
 77        listen_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, True)
 78        listen_socket.bind((host, port))
 79        if not self._quiet:
 80            cry(
 81                "DevPdb session open at {}:{}, waiting for connection ...".format(
 82                    *listen_socket.getsockname()
 83                )
 84            )
 85        listen_socket.listen(1)
 86        connection, address = listen_socket.accept()
 87        if not self._quiet:
 88            cry(f"DevPdb accepted connection from {repr(address)}.")
 89        self.handle = LF2CRLF_FileWrapper(connection)
 90        Pdb.__init__(self, completekey="tab", stdin=self.handle, stdout=self.handle)  # type: ignore[arg-type]
 91        self.backup = []
 92        if patch_stdstreams:
 93            for name in (
 94                "stderr",
 95                "stdout",
 96                "__stderr__",
 97                "__stdout__",
 98                "stdin",
 99                "__stdin__",
100            ):
101                self.backup.append((name, getattr(sys, name)))
102                setattr(sys, name, self.handle)
103        DevPdb.active_instance = self
104
105    def __restore(self) -> None:
106        if self.backup and not self._quiet:
107            cry(f"Restoring streams: {self.backup} ...")
108        for name, fh in self.backup:
109            setattr(sys, name, fh)
110        self.handle.close()
111        DevPdb.active_instance = None
112
113    def do_quit(self, arg: str) -> Any:
114        self.__restore()
115        return Pdb.do_quit(self, arg)
116
117    do_q = do_exit = do_quit
118
119    def set_trace(self, frame: FrameType | None = None) -> None:
120        if frame is None:
121            frame = sys._getframe().f_back
122        try:
123            Pdb.set_trace(self, frame)
124        except OSError as exc:
125            if exc.errno != errno.ECONNRESET:
126                raise
127
128
129def set_trace(
130    frame: FrameType | None = None,
131    host: str = "127.0.0.1",
132    port: int = 4444,
133    patch_stdstreams: bool = False,
134    quiet: bool = False,
135) -> None:
136    """
137    Opens a remote PDB over a host:port.
138    """
139    devpdb = DevPdb(
140        host=host, port=port, patch_stdstreams=patch_stdstreams, quiet=quiet
141    )
142    devpdb.set_trace(frame=frame)