From dc6a90d628ce5c80224f36d3eaa38c488c8a7a74 Mon Sep 17 00:00:00 2001 From: Soumendra Ganguly Date: Sat, 11 Feb 2023 15:20:11 -0600 Subject: [PATCH 1/2] Major revision of the pty library. Signed-off-by: Soumendra Ganguly --- Doc/library/pty.rst | 33 ++++++-- Lib/pty.py | 188 ++++++++++++++++++++++++++++++++++---------- 2 files changed, 175 insertions(+), 46 deletions(-) diff --git a/Doc/library/pty.rst b/Doc/library/pty.rst index 7f4da41e93802d..19f5f7b44cca71 100644 --- a/Doc/library/pty.rst +++ b/Doc/library/pty.rst @@ -23,23 +23,35 @@ platforms but it's not been thoroughly tested). The :mod:`pty` module defines the following functions: -.. function:: fork() +.. function:: fork(mode=None, winsz=None) Fork. Connect the child's controlling terminal to a pseudo-terminal. Return value is ``(pid, fd)``. Note that the child gets *pid* 0, and the *fd* is *invalid*. The parent's return value is the *pid* of the child, and *fd* is a file descriptor connected to the child's controlling terminal (and also to the - child's standard input and output). + child's standard input and output). The *mode* argument, which is expected + to be a termios attribute list like the one returned by + :func:`termios.tcgetattr`, will be applied to the slave end. The *winsz* + argument, which is expected to be a winsize pair like the one returned by + :func:`termios.tcgetwinsize`, will be applied to the slave end. -.. function:: openpty() +.. function:: openpty(mode=None, winsz=None, name=False) Open a new pseudo-terminal pair, using :func:`os.openpty` if possible, or - emulation code for generic Unix systems. Return a pair of file descriptors - ``(master, slave)``, for the master and the slave end, respectively. + emulation code for generic Unix systems. The *mode* argument, which is + expected to be a termios attribute list like the one returned by + :func:`termios.tcgetattr`, will be applied to the slave end. The *winsz* + argument, which is expected to be a winsize pair like the one returned by + :func:`termios.tcgetwinsize`, will be applied to the slave end. If *name* + is false, return a pair of file descriptors ``(master, slave)``, for the + master and the slave end, respectively. Otherwise, return + ``(master, slave, name)``, where the additional value is the filename of + the slave. -.. function:: spawn(argv[, master_read[, stdin_read]]) +.. function:: spawn(argv[, master_read[, stdin_read]], slave_echo=True, \ + handle_winch=False) Spawn a process, and connect its controlling terminal with the current process's standard io. This is often used to baffle programs which insist on @@ -69,6 +81,15 @@ The :mod:`pty` module defines the following functions: process will quit without any input, *spawn* will then loop forever. If *master_read* signals EOF the same behavior results (on linux at least). + The ECHO termios attribute of the slave end is turned on or off based on + the value of the argument *slave_echo* being true or false respectively. + + If *spawn* is called from the main thread, then a handler for + :const:`signal.SIGWINCH` will be installed if *handle_winch* is true, if + the pair of constants + (:const:`termios.TIOCGWINSZ`, :const:`termios.TIOCSWINSZ`) is defined, and + if STDIN of the current process is a terminal. + Return the exit status value from :func:`os.waitpid` on the child process. :func:`waitstatus_to_exitcode` can be used to convert the exit status into diff --git a/Lib/pty.py b/Lib/pty.py index 6571050886bd1d..eb7efc38d58922 100644 --- a/Lib/pty.py +++ b/Lib/pty.py @@ -1,15 +1,18 @@ """Pseudo terminal utilities.""" -# Bugs: No signal handling. Doesn't set slave termios and window size. -# Only tested on Linux, FreeBSD, and macOS. +# Author: Steen Lumholt -- with additions by Guido. +# +# Tested on Linux, FreeBSD, NetBSD, OpenBSD, and macOS. +# # See: W. Richard Stevens. 1992. Advanced Programming in the # UNIX Environment. Chapter 19. -# Author: Steen Lumholt -- with additions by Guido. + from select import select import os import sys import tty +import signal # names imported directly for test mocking purposes from os import close, waitpid @@ -23,17 +26,32 @@ CHILD = 0 -def openpty(): - """openpty() -> (master_fd, slave_fd) - Open a pty master/slave pair, using os.openpty() if possible.""" +ALL_SIGNALS = signal.valid_signals() +HAVE_WINSZ = hasattr(tty, "TIOCGWINSZ") and hasattr(tty, "TIOCSWINSZ") +HAVE_WINCH = HAVE_WINSZ and hasattr(signal, "SIGWINCH") + +def openpty(mode=None, winsz=None, name=False): + """Open a pty master/slave pair, using os.openpty() if possible.""" try: - return os.openpty() + master_fd, slave_fd = os.openpty() except (AttributeError, OSError): - pass - master_fd, slave_name = _open_terminal() - slave_fd = slave_open(slave_name) - return master_fd, slave_fd + master_fd, slave_name = _open_terminal() + slave_fd = slave_open(slave_name) + else: + if name: + slave_name = os.ttyname(slave_fd) + + if mode: + tty.tcsetattr(slave_fd, tty.TCSAFLUSH, mode) + + if HAVE_WINSZ and winsz: + tty.tcsetwinsize(slave_fd, winsz) + + if name: + return master_fd, slave_fd, slave_name + else: + return master_fd, slave_fd def master_open(): """master_open() -> (master_fd, slave_name) @@ -87,14 +105,18 @@ def slave_open(tty_name): pass return result -def fork(): - """fork() -> (pid, master_fd) - Fork and make the child a session leader with a controlling terminal.""" - +def fork(mode=None, winsz=None): + """Fork and make the child a session leader with controlling terminal.""" try: - pid, fd = os.forkpty() + pid, master_fd = os.forkpty() except (AttributeError, OSError): - pass + master_fd, slave_fd = openpty(mode, winsz) + pid = os.fork() + if pid == CHILD: + os.close(master_fd) + os.login_tty(slave_fd) + else: + os.close(slave_fd) else: if pid == CHILD: try: @@ -102,15 +124,15 @@ def fork(): except OSError: # os.forkpty() already set us session leader pass - return pid, fd - master_fd, slave_fd = openpty() - pid = os.fork() - if pid == CHILD: - os.close(master_fd) - os.login_tty(slave_fd) - else: - os.close(slave_fd) + # os.forkpty() makes sure that the slave end of + # the pty becomes the stdin of the child; this + # is usually done via a dup2() call + if mode: + tty.tcsetattr(STDIN_FILENO, tty.TCSAFLUSH, mode) + + if HAVE_WINSZ and winsz: + tty.tcsetwinsize(STDIN_FILENO, winsz) # Parent and child process. return pid, master_fd @@ -125,14 +147,90 @@ def _read(fd): """Default read function.""" return os.read(fd, 1024) -def _copy(master_fd, master_read=_read, stdin_read=_read): - """Parent copy loop. +def _getmask(): + """Get signal mask of current thread.""" + return signal.pthread_sigmask(signal.SIG_BLOCK, []) + +def _sigblock(): + """Block all signals.""" + signal.pthread_sigmask(signal.SIG_BLOCK, ALL_SIGNALS) + +def _sigreset(saved_mask): + """Restore signal mask.""" + signal.pthread_sigmask(signal.SIG_SETMASK, saved_mask) + +def _setup_pty(slave_echo): + """Open and setup a pty pair. + + If current stdin is a tty, then apply current stdin's + termios and winsize to the slave and set current stdin to + raw mode. Return (master, slave, original stdin mode/None, + stdin winsize/None).""" + stdin_mode = None + stdin_winsz = None + try: + mode = tty.tcgetattr(STDIN_FILENO) + except tty.error: + stdin_tty = False + fd = slave_fd + + master_fd, slave_fd, slave_path = openpty(name=True) + mode = tty.tcgetattr(slave_fd) + else: + stdin_tty = True + fd = STDIN_FILENO + + stdin_mode = list(mode) + if HAVE_WINSZ: + stdin_winsz = tty.tcgetwinsize(STDIN_FILENO) + + if slave_echo: + mode[tty.LFLAG] |= tty.ECHO + else: + mode[tty.LFLAG] &= ~tty.ECHO + + if stdin_tty: + master_fd, slave_fd, slave_path = openpty(mode, winsz, True) + tty.cfmakeraw(mode) + + tty.tcsetattr(fd, tty.TCSAFLUSH, mode) + + return master_fd, slave_fd, slave_path, stdin_mode, stdin_winsz + +def _setup_winch(slave_path, saved_mask, handle_winch): + """Install SIGWINCH handler. + + Returns old SIGWINCH handler if relevant; returns + None otherwise.""" + old_hwinch = None + if handle_winch: + def hwinch(signum, frame): + """Handle SIGWINCH.""" + _sigblock() + new_slave_fd = os.open(slave_path, os.O_RDWR) + tty.setwinsize(new_slave_fd, tty.getwinsize(STDIN_FILENO)) + os.close(new_slave_fd) + _sigreset(saved_mask) + + try: + # Raises ValueError if not called from main thread. + old_hwinch = signal.signal(signal.SIGWINCH, hwinch) + except ValueError: + pass + + return old_hwinch + +def _copy(master_fd, master_read=_read, stdin_read=_read, \ + saved_mask=set()): + """Parent copy loop for pty.spawn(). Copies pty master -> standard output (master_read) standard input -> pty master (stdin_read)""" fds = [master_fd, STDIN_FILENO] - while fds: - rfds, _wfds, _xfds = select(fds, [], []) + while True: + _sigreset(saved_mask) + rfds = select(fds, [], [])[0] + _sigblock() if master_fd in rfds: # Some OSes signal EOF by returning an empty byte string, @@ -154,28 +252,38 @@ def _copy(master_fd, master_read=_read, stdin_read=_read): else: _writen(master_fd, data) -def spawn(argv, master_read=_read, stdin_read=_read): +def spawn(argv, master_read=_read, stdin_read=_read, slave_echo=True, \ + handle_winch=False): """Create a spawned process.""" if isinstance(argv, str): argv = (argv,) sys.audit('pty.spawn', argv) - pid, master_fd = fork() + saved_mask = _getmask() + _sigblock() # Reset during select() in _copy. + + master_fd, slave_fd, slave_path, mode, winsz = _setup_pty(slave_echo) + handle_winch = handle_winch and (winsz != None) and HAVE_WINCH + old_hwinch = _setup_winch(slave_path, saved_mask, handle_winch) + + pid = os.fork() if pid == CHILD: + os.close(master_fd) + os.login_tty(slave_fd) + _sigreset(saved_mask) os.execlp(argv[0], *argv) - try: - mode = tcgetattr(STDIN_FILENO) - setraw(STDIN_FILENO) - restore = True - except tty.error: # This is the same as termios.error - restore = False + os.close(slave_fd) - try: - _copy(master_fd, master_read, stdin_read) + try: + _copy(master_fd, master_read, stdin_read, saved_mask) finally: - if restore: + if mode: tcsetattr(STDIN_FILENO, tty.TCSAFLUSH, mode) + if old_hwinch: + signal.signal(signal.SIGWINCH, old_hwinch) close(master_fd) + _sigreset(saved_mask) + return waitpid(pid, 0)[1] From edd1a12fdf03aa145003e5cb9441ea7ddeaeb181 Mon Sep 17 00:00:00 2001 From: "blurb-it[bot]" <43283697+blurb-it[bot]@users.noreply.github.com> Date: Sat, 11 Feb 2023 21:24:52 +0000 Subject: [PATCH 2/2] =?UTF-8?q?=F0=9F=93=9C=F0=9F=A4=96=20Added=20by=20blu?= =?UTF-8?q?rb=5Fit.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../next/Library/2023-02-11-21-24-50.gh-issue-85984.PIDr7c.rst | 1 + 1 file changed, 1 insertion(+) create mode 100644 Misc/NEWS.d/next/Library/2023-02-11-21-24-50.gh-issue-85984.PIDr7c.rst diff --git a/Misc/NEWS.d/next/Library/2023-02-11-21-24-50.gh-issue-85984.PIDr7c.rst b/Misc/NEWS.d/next/Library/2023-02-11-21-24-50.gh-issue-85984.PIDr7c.rst new file mode 100644 index 00000000000000..4aedcf2514bef0 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2023-02-11-21-24-50.gh-issue-85984.PIDr7c.rst @@ -0,0 +1 @@ +Major revision of the pty library.