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

bpo-34605, pty: Avoid master/slave terms #9100

Closed
wants to merge 3 commits into from
Closed
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
4 changes: 2 additions & 2 deletions Doc/library/os.rst
Original file line number Diff line number Diff line change
Expand Up @@ -1007,7 +1007,7 @@ or `the MSDN <https://msdn.microsoft.com/en-us/library/z0kc8e3z.aspx>`_ on Windo
.. index:: module: pty

Open a new pseudo-terminal pair. Return a pair of file descriptors
``(master, slave)`` for the pty and the tty, respectively. The new file
``(parent, child)`` for the pty and the tty, respectively. The new file
descriptors are :ref:`non-inheritable <fd_inheritance>`. For a (slightly) more
portable approach, use the :mod:`pty` module.

Expand Down Expand Up @@ -3315,7 +3315,7 @@ written in Python, such as a mail server's external command delivery program.
Fork a child process, using a new pseudo-terminal as the child's controlling
terminal. Return a pair of ``(pid, fd)``, where *pid* is ``0`` in the child, the
new child's process id in the parent, and *fd* is the file descriptor of the
master end of the pseudo-terminal. For a more portable approach, use the
parent end of the pseudo-terminal. For a more portable approach, use the
:mod:`pty` module. If an error occurs :exc:`OSError` is raised.

Availability: some flavors of Unix.
Expand Down
6 changes: 3 additions & 3 deletions Doc/library/pty.rst
Original file line number Diff line number Diff line change
Expand Up @@ -36,16 +36,16 @@ The :mod:`pty` module defines the following functions:

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.
``(parent, child)``, for the parent and the child end, respectively.


.. function:: spawn(argv[, master_read[, stdin_read]])
.. function:: spawn(argv[, parent_read[, stdin_read]])

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
reading from the controlling terminal.

The functions *master_read* and *stdin_read* should be functions which read from
The functions *parent_read* and *stdin_read* should be functions which read from
a file descriptor. The defaults try to read 1024 bytes each time they are
called.

Expand Down
86 changes: 46 additions & 40 deletions Lib/pty.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
"""Pseudo terminal utilities."""

# Bugs: No signal handling. Doesn't set slave termios and window size.
# Bugs: No signal handling. Doesn't set child termios and window size.
# Only tested on Linux.
# See: W. Richard Stevens. 1992. Advanced Programming in the
# UNIX Environment. Chapter 19.
Expand All @@ -19,35 +19,35 @@
CHILD = 0

def openpty():
"""openpty() -> (master_fd, slave_fd)
Open a pty master/slave pair, using os.openpty() if possible."""
"""openpty() -> (parent_fd, child_fd)
Open a pty parent/child pair, using os.openpty() if possible."""

try:
return os.openpty()
except (AttributeError, OSError):
pass
master_fd, slave_name = _open_terminal()
slave_fd = slave_open(slave_name)
return master_fd, slave_fd
parent_fd, child_name = _open_terminal()
child_fd = child_open(child_name)
return parent_fd, child_fd

def master_open():
"""master_open() -> (master_fd, slave_name)
Open a pty master and return the fd, and the filename of the slave end.
def parent_open():
"""parent_open() -> (parent_fd, child_name)
Open a pty parent and return the fd, and the filename of the child end.
Deprecated, use openpty() instead."""

try:
master_fd, slave_fd = os.openpty()
parent_fd, child_fd = os.openpty()
except (AttributeError, OSError):
pass
else:
slave_name = os.ttyname(slave_fd)
os.close(slave_fd)
return master_fd, slave_name
child_name = os.ttyname(child_fd)
os.close(child_fd)
return parent_fd, child_name

return _open_terminal()

def _open_terminal():
"""Open pty master and return (master_fd, tty_name)."""
"""Open pty parent and return (parent_fd, tty_name)."""
for x in 'pqrstuvwxyzPQRST':
for y in '0123456789abcdef':
pty_name = '/dev/pty' + x + y
Expand All @@ -58,9 +58,9 @@ def _open_terminal():
return (fd, '/dev/tty' + x + y)
raise OSError('out of pty devices')

def slave_open(tty_name):
"""slave_open(tty_name) -> slave_fd
Open the pty slave and acquire the controlling terminal, returning
def child_open(tty_name):
"""child_open(tty_name) -> child_fd
Open the pty child and acquire the controlling terminal, returning
opened filedescriptor.
Deprecated, use openpty() instead."""

Expand All @@ -76,8 +76,14 @@ def slave_open(tty_name):
pass
return result

# bpo-34605: master_open()/child_open() has been renamed
# to parent_open()/child_open(), but keep master_open/slave_open aliases
# for backward compatibility.
master_open = parent_open
slave_open = child_open

def fork():
"""fork() -> (pid, master_fd)
"""fork() -> (pid, parent_fd)
Fork and make the child a session leader with a controlling terminal."""

try:
Expand All @@ -93,28 +99,28 @@ def fork():
pass
return pid, fd

master_fd, slave_fd = openpty()
parent_fd, child_fd = openpty()
pid = os.fork()
if pid == CHILD:
# Establish a new session.
os.setsid()
os.close(master_fd)
os.close(parent_fd)

# Slave becomes stdin/stdout/stderr of child.
os.dup2(slave_fd, STDIN_FILENO)
os.dup2(slave_fd, STDOUT_FILENO)
os.dup2(slave_fd, STDERR_FILENO)
if (slave_fd > STDERR_FILENO):
os.close (slave_fd)
# Child becomes stdin/stdout/stderr of child.
os.dup2(child_fd, STDIN_FILENO)
os.dup2(child_fd, STDOUT_FILENO)
os.dup2(child_fd, STDERR_FILENO)
if (child_fd > STDERR_FILENO):
os.close (child_fd)

# Explicitly open the tty to make it become a controlling tty.
tmp_fd = os.open(os.ttyname(STDOUT_FILENO), os.O_RDWR)
os.close(tmp_fd)
else:
os.close(slave_fd)
os.close(child_fd)

# Parent and child process.
return pid, master_fd
return pid, parent_fd

def _writen(fd, data):
"""Write all the data to a descriptor."""
Expand All @@ -126,32 +132,32 @@ def _read(fd):
"""Default read function."""
return os.read(fd, 1024)

def _copy(master_fd, master_read=_read, stdin_read=_read):
def _copy(parent_fd, parent_read=_read, stdin_read=_read):
"""Parent copy loop.
Copies
pty master -> standard output (master_read)
standard input -> pty master (stdin_read)"""
fds = [master_fd, STDIN_FILENO]
pty parent -> standard output (parent_read)
standard input -> pty parent (stdin_read)"""
fds = [parent_fd, STDIN_FILENO]
while True:
rfds, wfds, xfds = select(fds, [], [])
if master_fd in rfds:
data = master_read(master_fd)
if parent_fd in rfds:
data = parent_read(parent_fd)
if not data: # Reached EOF.
fds.remove(master_fd)
fds.remove(parent_fd)
else:
os.write(STDOUT_FILENO, data)
if STDIN_FILENO in rfds:
data = stdin_read(STDIN_FILENO)
if not data:
fds.remove(STDIN_FILENO)
else:
_writen(master_fd, data)
_writen(parent_fd, data)

def spawn(argv, master_read=_read, stdin_read=_read):
def spawn(argv, parent_read=_read, stdin_read=_read):
"""Create a spawned process."""
if type(argv) == type(''):
argv = (argv,)
pid, master_fd = fork()
pid, parent_fd = fork()
if pid == CHILD:
os.execlp(argv[0], *argv)
try:
Expand All @@ -161,10 +167,10 @@ def spawn(argv, master_read=_read, stdin_read=_read):
except tty.error: # This is the same as termios.error
restore = 0
try:
_copy(master_fd, master_read, stdin_read)
_copy(parent_fd, parent_read, stdin_read)
except OSError:
if restore:
tty.tcsetattr(STDIN_FILENO, tty.TCSAFLUSH, mode)

os.close(master_fd)
os.close(parent_fd)
return os.waitpid(pid, 0)[1]
44 changes: 22 additions & 22 deletions Lib/test/test_asyncio/test_events.py
Original file line number Diff line number Diff line change
Expand Up @@ -1500,29 +1500,29 @@ async def connect():
def test_read_pty_output(self):
proto = MyReadPipeProto(loop=self.loop)

master, slave = os.openpty()
master_read_obj = io.open(master, 'rb', 0)
parent, child = os.openpty()
parent_read_obj = io.open(parent, 'rb', 0)

async def connect():
t, p = await self.loop.connect_read_pipe(lambda: proto,
master_read_obj)
parent_read_obj)
self.assertIs(p, proto)
self.assertIs(t, proto.transport)
self.assertEqual(['INITIAL', 'CONNECTED'], proto.state)
self.assertEqual(0, proto.nbytes)

self.loop.run_until_complete(connect())

os.write(slave, b'1')
os.write(child, b'1')
test_utils.run_until(self.loop, lambda: proto.nbytes)
self.assertEqual(1, proto.nbytes)

os.write(slave, b'2345')
os.write(child, b'2345')
test_utils.run_until(self.loop, lambda: proto.nbytes >= 5)
self.assertEqual(['INITIAL', 'CONNECTED'], proto.state)
self.assertEqual(5, proto.nbytes)

os.close(slave)
os.close(child)
proto.transport.close()
self.loop.run_until_complete(proto.done)
self.assertEqual(
Expand Down Expand Up @@ -1598,11 +1598,11 @@ def test_write_pipe_disconnect_on_close(self):
# older than 10.6 (Snow Leopard)
@support.requires_mac_ver(10, 6)
def test_write_pty(self):
master, slave = os.openpty()
slave_write_obj = io.open(slave, 'wb', 0)
parent, child = os.openpty()
child_write_obj = io.open(child, 'wb', 0)

proto = MyWritePipeProto(loop=self.loop)
connect = self.loop.connect_write_pipe(lambda: proto, slave_write_obj)
connect = self.loop.connect_write_pipe(lambda: proto, child_write_obj)
transport, p = self.loop.run_until_complete(connect)
self.assertIs(p, proto)
self.assertIs(transport, proto.transport)
Expand All @@ -1612,7 +1612,7 @@ def test_write_pty(self):

data = bytearray()
def reader(data):
chunk = os.read(master, 1024)
chunk = os.read(parent, 1024)
data += chunk
return len(data)

Expand All @@ -1626,7 +1626,7 @@ def reader(data):
self.assertEqual(b'12345', data)
self.assertEqual('CONNECTED', proto.state)

os.close(master)
os.close(parent)

# extra info is available
self.assertIsNotNone(proto.transport.get_extra_info('pipe'))
Expand All @@ -1642,33 +1642,33 @@ def reader(data):
# older than 10.6 (Snow Leopard)
@support.requires_mac_ver(10, 6)
def test_bidirectional_pty(self):
master, read_slave = os.openpty()
write_slave = os.dup(read_slave)
tty.setraw(read_slave)
parent, read_child = os.openpty()
write_child = os.dup(read_child)
tty.setraw(read_child)

slave_read_obj = io.open(read_slave, 'rb', 0)
child_read_obj = io.open(read_child, 'rb', 0)
read_proto = MyReadPipeProto(loop=self.loop)
read_connect = self.loop.connect_read_pipe(lambda: read_proto,
slave_read_obj)
child_read_obj)
read_transport, p = self.loop.run_until_complete(read_connect)
self.assertIs(p, read_proto)
self.assertIs(read_transport, read_proto.transport)
self.assertEqual(['INITIAL', 'CONNECTED'], read_proto.state)
self.assertEqual(0, read_proto.nbytes)


slave_write_obj = io.open(write_slave, 'wb', 0)
child_write_obj = io.open(write_child, 'wb', 0)
write_proto = MyWritePipeProto(loop=self.loop)
write_connect = self.loop.connect_write_pipe(lambda: write_proto,
slave_write_obj)
child_write_obj)
write_transport, p = self.loop.run_until_complete(write_connect)
self.assertIs(p, write_proto)
self.assertIs(write_transport, write_proto.transport)
self.assertEqual('CONNECTED', write_proto.state)

data = bytearray()
def reader(data):
chunk = os.read(master, 1024)
chunk = os.read(parent, 1024)
data += chunk
return len(data)

Expand All @@ -1678,7 +1678,7 @@ def reader(data):
self.assertEqual(['INITIAL', 'CONNECTED'], read_proto.state)
self.assertEqual('CONNECTED', write_proto.state)

os.write(master, b'a')
os.write(parent, b'a')
test_utils.run_until(self.loop, lambda: read_proto.nbytes >= 1,
timeout=10)
self.assertEqual(['INITIAL', 'CONNECTED'], read_proto.state)
Expand All @@ -1691,14 +1691,14 @@ def reader(data):
self.assertEqual(['INITIAL', 'CONNECTED'], read_proto.state)
self.assertEqual('CONNECTED', write_proto.state)

os.write(master, b'bcde')
os.write(parent, b'bcde')
test_utils.run_until(self.loop, lambda: read_proto.nbytes >= 5,
timeout=10)
self.assertEqual(['INITIAL', 'CONNECTED'], read_proto.state)
self.assertEqual(5, read_proto.nbytes)
self.assertEqual('CONNECTED', write_proto.state)

os.close(master)
os.close(parent)

read_transport.close()
self.loop.run_until_complete(read_proto.done)
Expand Down
2 changes: 1 addition & 1 deletion Lib/test/test_builtin.py
Original file line number Diff line number Diff line change
Expand Up @@ -1669,7 +1669,7 @@ def run_child(self, child, terminal_input):
# Check the result was got and corresponds to the user's terminal input
if len(lines) != 2:
# Something went wrong, try to get at stderr
# Beware of Linux raising EIO when the slave is closed
# Beware of Linux raising EIO when the child is closed
child_output = bytearray()
while True:
try:
Expand Down
14 changes: 7 additions & 7 deletions Lib/test/test_openpty.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,14 @@

class OpenptyTest(unittest.TestCase):
def test(self):
master, slave = os.openpty()
self.addCleanup(os.close, master)
self.addCleanup(os.close, slave)
if not os.isatty(slave):
self.fail("Slave-end of pty is not a terminal.")
parent, child = os.openpty()
self.addCleanup(os.close, parent)
self.addCleanup(os.close, child)
if not os.isatty(child):
self.fail("Child-end of pty is not a terminal.")

os.write(slave, b'Ping!')
self.assertEqual(os.read(master, 1024), b'Ping!')
os.write(child, b'Ping!')
self.assertEqual(os.read(parent, 1024), b'Ping!')

if __name__ == '__main__':
unittest.main()
Loading