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

Use and prefer os.posix_spawn() when available #54

Open
wants to merge 5 commits into
base: master
Choose a base branch
from
Open
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
270 changes: 176 additions & 94 deletions ptyprocess/ptyprocess.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,19 +10,34 @@
import sys
import termios
import time
import threading

try:
import builtins # Python 3
except ImportError:
import __builtin__ as builtins # Python 2

# Constants
from pty import (STDIN_FILENO, CHILD)
from pty import (STDIN_FILENO, CHILD, STDOUT_FILENO, STDERR_FILENO)

from .util import which, PtyProcessError

_platform = sys.platform.lower()

# Is posix_spawn() robust? Require both os.posix_spawn() and the
# POSIX_SPAWN_SETSID extension (the latter, via setsid(),
# disassociates the child from the parent's session creating a new
# session and group and should be called when the system has things
# like job control) (there doesn't seem to be a runtime test for the
# latter, see bpo-36619). On systems that don't have job control,
# POSIX_SPAWN_SETPGROUP is probably sufficient but detecting that case
# is even harder.
_have_posix_spawn = (
hasattr(os, 'posix_spawn') and
_platform.startswith("linux"))
if _have_posix_spawn:
_posix_spawn_lock = threading.Lock()

# Solaris uses internal __fork_pty(). All others use pty.fork().
_is_solaris = (
_platform.startswith('solaris') or
Expand Down Expand Up @@ -217,124 +232,191 @@ def spawn(
command = command_with_path
argv[0] = command

# [issue #119] To prevent the case where exec fails and the user is
# stuck interacting with a python child process instead of whatever
# was expected, we implement the solution from
# http://stackoverflow.com/a/3703179 to pass the exception to the
# parent process

# [issue #119] 1. Before forking, open a pipe in the parent process.
exec_err_pipe_read, exec_err_pipe_write = os.pipe()

if use_native_pty_fork:
pid, fd = pty.fork()
else:
# Use internal fork_pty, for Solaris
pid, fd = _fork_pty.fork_pty()

# Some platforms must call setwinsize() and setecho() from the
# child process, and others from the master process. We do both,
# allowing IOError for either.

if pid == CHILD:
# set window size
if _have_posix_spawn:
with _posix_spawn_lock:
# Try to ensure that the tty/pty have O_CLOEXEC set
# (aka non-inheritable) so that a parallel call to
# this code won't end up with an open PTY/TTY.
# Unfortunately os.openpty() (openpty(3)) never sets
# O_CLOEXEC and pty.open() (Issue 36603) only
# sometimes sets O_CLOEXEC.
#
# Is this a bug? Arguably yes. However it isn't easy
# to fix - unless the native openpty(3) atomically
# opens the pty/tty with O_CLOEXEC there's always
# going to be a race. This lock mitigates the case
# where fork()/exec() is using this path (or other
# code paths such as subprocess that brute force a
# close of all FDs).
fd, tty = os.openpty()
os.set_inheritable(fd, False)
os.set_inheritable(tty, False)
# Try to set window size on TTY per below; but is this
# needed?
try:
_setwinsize(STDIN_FILENO, *dimensions)
_setwinsize(tty, *dimensions)
except IOError as err:
if err.args[0] not in (errno.EINVAL, errno.ENOTTY):
raise

# disable echo if spawn argument echo was unset
# Try to disable echo if spawn argument echo was unset per
# below; but does this work?
if not echo:
try:
_setecho(STDIN_FILENO, False)
_setecho(tty, False)
except (IOError, termios.error) as err:
if err.args[0] not in (errno.EINVAL, errno.ENOTTY):
raise
# Create the child:
#
# - convert the tty into STDIN / STDOUT / STDERR
#
# - always specify ENV (use the default if needed)
#
# - use setsid=True to create a new session (and
# disassociate the child from the parent's session /
# terminal) and make the child the process group leader
#
# - assume that all files have inheritable (close-on-exec)
# correctly set.
file_actions=[
(os.POSIX_SPAWN_DUP2, tty, STDIN_FILENO),
(os.POSIX_SPAWN_DUP2, tty, STDOUT_FILENO),
(os.POSIX_SPAWN_DUP2, tty, STDERR_FILENO),
# not needed?
(os.POSIX_SPAWN_CLOSE, tty),
(os.POSIX_SPAWN_CLOSE, fd),
]
spawn_env = env or os.environ
pid = os.posix_spawn(command, argv, spawn_env,
file_actions=file_actions,
setsid=0)
# Child started; close the child's tty.
os.close(tty)
else:

# [issue #119] To prevent the case where exec fails and
# the user is stuck interacting with a python child
# process instead of whatever was expected, we implement
# the solution from http://stackoverflow.com/a/3703179 to
# pass the exception to the parent process

# [issue #119] 1. Before forking, open a pipe in the
# parent process.
exec_err_pipe_read, exec_err_pipe_write = os.pipe()

# XXX: only use fork_pty()?
if use_native_pty_fork:
pid, fd = pty.fork()
else:
# Use internal fork_pty, for Solaris
pid, fd = _fork_pty.fork_pty()

# Some platforms must call setwinsize() and setecho() from
# the child process, and others from the master
# process. We do both, allowing IOError for either.

if pid == CHILD:
# set window size
try:
_setwinsize(STDIN_FILENO, *dimensions)
except IOError as err:
if err.args[0] not in (errno.EINVAL, errno.ENOTTY):
raise

# disable echo if spawn argument echo was unset
if not echo:
try:
_setecho(STDIN_FILENO, False)
except (IOError, termios.error) as err:
if err.args[0] not in (errno.EINVAL, errno.ENOTTY):
raise

# [issue #119] 3. The child closes the reading end and
# sets the close-on-exec flag for the writing end.
os.close(exec_err_pipe_read)
fcntl.fcntl(exec_err_pipe_write, fcntl.F_SETFD, fcntl.FD_CLOEXEC)

# Do not allow child to inherit open file descriptors from parent,
# with the exception of the exec_err_pipe_write of the pipe
# and pass_fds.

# Impose ceiling on max_fd: AIX bugfix for users with
# unlimited nofiles where resource.RLIMIT_NOFILE is
# 2^63-1 and os.closerange() occasionally raises out
# of range error
max_fd = min(1048576, resource.getrlimit(resource.RLIMIT_NOFILE)[0])
spass_fds = sorted(set(pass_fds) | {exec_err_pipe_write})
for pair in zip([2] + spass_fds, spass_fds + [max_fd]):
os.closerange(pair[0]+1, pair[1])

if cwd is not None:
os.chdir(cwd)

if preexec_fn is not None:
try:
preexec_fn()
except Exception as e:
ename = type(e).__name__
tosend = '{}:0:{}'.format(ename, str(e))
if PY3:
tosend = tosend.encode('utf-8')

os.write(exec_err_pipe_write, tosend)
os.close(exec_err_pipe_write)
os._exit(1)

# [issue #119] 3. The child closes the reading end and sets the
# close-on-exec flag for the writing end.
os.close(exec_err_pipe_read)
fcntl.fcntl(exec_err_pipe_write, fcntl.F_SETFD, fcntl.FD_CLOEXEC)

# Do not allow child to inherit open file descriptors from parent,
# with the exception of the exec_err_pipe_write of the pipe
# and pass_fds.
# Impose ceiling on max_fd: AIX bugfix for users with unlimited
# nofiles where resource.RLIMIT_NOFILE is 2^63-1 and os.closerange()
# occasionally raises out of range error
max_fd = min(1048576, resource.getrlimit(resource.RLIMIT_NOFILE)[0])
spass_fds = sorted(set(pass_fds) | {exec_err_pipe_write})
for pair in zip([2] + spass_fds, spass_fds + [max_fd]):
os.closerange(pair[0]+1, pair[1])

if cwd is not None:
os.chdir(cwd)

if preexec_fn is not None:
try:
preexec_fn()
except Exception as e:
ename = type(e).__name__
tosend = '{}:0:{}'.format(ename, str(e))
if env is None:
os.execv(command, argv)
else:
os.execvpe(command, argv, env)
except OSError as err:
# [issue #119] 5. If exec fails, the child writes
# the error code back to the parent using the
# pipe, then exits.
tosend = 'OSError:{}:{}'.format(err.errno, str(err))
if PY3:
tosend = tosend.encode('utf-8')
os.write(exec_err_pipe_write, tosend)
os.close(exec_err_pipe_write)
os._exit(os.EX_OSERR)

# [issue #119] 2. After forking, the parent closes the
# writing end of the pipe and reads from the reading end.
os.close(exec_err_pipe_write)
exec_err_data = os.read(exec_err_pipe_read, 4096)
os.close(exec_err_pipe_read)

os.write(exec_err_pipe_write, tosend)
os.close(exec_err_pipe_write)
os._exit(1)

try:
if env is None:
os.execv(command, argv)
# [issue #119] 6. The parent reads eof (a zero-length
# read) if the child successfully performed exec, since
# close-on-exec made successful exec close the writing end
# of the pipe. Or, if exec failed, the parent reads the
# error code and can proceed accordingly. Either way, the
# parent blocks until the child calls exec.
if len(exec_err_data) != 0:
try:
errclass, errno_s, errmsg = exec_err_data.split(b':', 2)
exctype = getattr(builtins, errclass.decode('ascii'), Exception)

exception = exctype(errmsg.decode('utf-8', 'replace'))
if exctype is OSError:
exception.errno = int(errno_s)
except:
raise Exception('Subprocess failed, got bad error data: %r'
% exec_err_data)
else:
os.execvpe(command, argv, env)
except OSError as err:
# [issue #119] 5. If exec fails, the child writes the error
# code back to the parent using the pipe, then exits.
tosend = 'OSError:{}:{}'.format(err.errno, str(err))
if PY3:
tosend = tosend.encode('utf-8')
os.write(exec_err_pipe_write, tosend)
os.close(exec_err_pipe_write)
os._exit(os.EX_OSERR)
raise exception

# Parent
inst = cls(pid, fd)

# Set some informational attributes
inst.argv = argv
if env is not None:
inst.env = env
if cwd is not None:
inst.launch_dir = cwd

# [issue #119] 2. After forking, the parent closes the writing end
# of the pipe and reads from the reading end.
os.close(exec_err_pipe_write)
exec_err_data = os.read(exec_err_pipe_read, 4096)
os.close(exec_err_pipe_read)

# [issue #119] 6. The parent reads eof (a zero-length read) if the
# child successfully performed exec, since close-on-exec made
# successful exec close the writing end of the pipe. Or, if exec
# failed, the parent reads the error code and can proceed
# accordingly. Either way, the parent blocks until the child calls
# exec.
if len(exec_err_data) != 0:
try:
errclass, errno_s, errmsg = exec_err_data.split(b':', 2)
exctype = getattr(builtins, errclass.decode('ascii'), Exception)

exception = exctype(errmsg.decode('utf-8', 'replace'))
if exctype is OSError:
exception.errno = int(errno_s)
except:
raise Exception('Subprocess failed, got bad error data: %r'
% exec_err_data)
else:
raise exception

try:
inst.setwinsize(*dimensions)
except IOError as err:
Expand Down