Skip to content

Commit

Permalink
Add pass_fds parameter to PtyProcess:spawn()
Browse files Browse the repository at this point in the history
  • Loading branch information
eugpermar committed Dec 28, 2018
1 parent 5436e55 commit 916206e
Show file tree
Hide file tree
Showing 2 changed files with 43 additions and 14 deletions.
34 changes: 20 additions & 14 deletions ptyprocess/ptyprocess.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,15 +42,15 @@ def _byte(i):
else:
def _byte(i):
return chr(i)

class FileNotFoundError(OSError): pass
class TimeoutError(OSError): pass

_EOF, _INTR = None, None

def _make_eof_intr():
"""Set constants _EOF and _INTR.
This avoids doing potentially costly operations on module load.
"""
global _EOF, _INTR
Expand Down Expand Up @@ -84,13 +84,13 @@ def _make_eof_intr():
except ImportError:
# ^C, ^D
(intr, eof) = (3, 4)

_INTR = _byte(intr)
_EOF = _byte(eof)

# setecho and setwinsize are pulled out here because on some platforms, we need
# to do this from the child before we exec()

def _setecho(fd, state):
errmsg = 'setecho() may not be called on this platform (it may still be possible to enable/disable echo when spawning the child process)'

Expand Down Expand Up @@ -127,7 +127,7 @@ def _setwinsize(fd, rows, cols):

class PtyProcess(object):
'''This class represents a process running in a pseudoterminal.
The main constructor is the :meth:`spawn` classmethod.
'''
string_type = bytes
Expand All @@ -148,7 +148,7 @@ def write_to_stdout(b):
write_to_stdout = sys.stdout.write

encoding = None

argv = None
env = None
launch_dir = None
Expand Down Expand Up @@ -178,7 +178,7 @@ def __init__(self, pid, fd):
@classmethod
def spawn(
cls, argv, cwd=None, env=None, echo=True, preexec_fn=None,
dimensions=(24, 80)):
dimensions=(24, 80), pass_fds=()):
'''Start the given command in a child process in a pseudo terminal.
This does all the fork/exec type of stuff for a pty, and returns an
Expand All @@ -190,6 +190,10 @@ def spawn(
Dimensions of the psuedoterminal used for the subprocess can be
specified as a tuple (rows, cols), or the default (24, 80) will be used.
By default, all file descriptors except 0, 1 and 2 are closed. This
behavior can be overridden with pass_fds, a list of file descriptors to
keep open between the parent and the child.
'''
# Note that it is difficult for this method to fail.
# You cannot detect if the child process cannot start.
Expand Down Expand Up @@ -255,12 +259,14 @@ def spawn(

# 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])
os.closerange(3, exec_err_pipe_write)
os.closerange(exec_err_pipe_write+1, max_fd)
spass_fds = sorted(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)
Expand Down Expand Up @@ -295,7 +301,7 @@ def spawn(

# Parent
inst = cls(pid, fd)

# Set some informational attributes
inst.argv = argv
if env is not None:
Expand Down Expand Up @@ -345,9 +351,9 @@ def __repr__(self):
args.append("env=%r" % self.env)
if self.launch_dir is not None:
args.append("cwd=%r" % self.launch_dir)

return "{}.spawn({})".format(clsname, ", ".join(args))

else:
return "{}(pid={}, fd={})".format(clsname, self.pid, self.fd)

Expand Down Expand Up @@ -505,7 +511,7 @@ def read(self, size=1024):
Can block if there is nothing to read. Raises :exc:`EOFError` if the
terminal was closed.
Unlike Pexpect's ``read_nonblocking`` method, this doesn't try to deal
with the vagaries of EOF on platforms that do strange things, like IRIX
or older Solaris systems. It handles the errno=EIO pattern used on
Expand Down Expand Up @@ -556,7 +562,7 @@ def _writeb(self, b, flush=True):

def write(self, s, flush=True):
"""Write bytes to the pseudoterminal.
Returns the number of bytes written.
"""
return self._writeb(s, flush=flush)
Expand Down
23 changes: 23 additions & 0 deletions tests/test_spawn.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import fcntl
import os
import time
import select
import tempfile
import unittest
from ptyprocess.ptyprocess import which
from ptyprocess import PtyProcess, PtyProcessUnicode
Expand Down Expand Up @@ -111,3 +113,24 @@ def test_interactive_repl_unicode_noecho(self):
@unittest.skipIf(which('bc') is None, "bc(1) not found on this server.")
def test_interactive_repl_unicode_echo(self):
self._interactive_repl_unicode(echo=True)

def test_pass_fds(self):
with tempfile.NamedTemporaryFile() as temp_file:
temp_file_fd = temp_file.fileno()
temp_file_name = temp_file.name

# Temporary files are CLOEXEC by default
fcntl.fcntl(temp_file_fd,
fcntl.F_SETFD,
fcntl.fcntl(temp_file_fd, fcntl.F_GETFD) &
~fcntl.FD_CLOEXEC)

p = PtyProcess.spawn(['bash',
'-c',
'printf hello >&{}'.format(temp_file_fd)],
echo=True,
pass_fds=(temp_file_fd,))
p.wait()

with open(temp_file_name, 'r') as temp_file_r:
assert temp_file_r.read() == 'hello'

0 comments on commit 916206e

Please sign in to comment.