Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

gh-85984: Major revision of the pty library. #101833

Draft
wants to merge 2 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 27 additions & 6 deletions Doc/library/pty.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
188 changes: 148 additions & 40 deletions Lib/pty.py
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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)
Expand Down Expand Up @@ -87,30 +105,34 @@ 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:
os.setsid()
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
Expand All @@ -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:
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I do not think that fds is ever empty in this loop. Therefore, I replaced while fds: with the original while True:. Please correct me if I am wrong.

_sigreset(saved_mask)
rfds = select(fds, [], [])[0]
_sigblock()

if master_fd in rfds:
# Some OSes signal EOF by returning an empty byte string,
Expand All @@ -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]
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Major revision of the pty library.