diff --git a/ptyprocess/ptyprocess.py b/ptyprocess/ptyprocess.py index 29b4e43..5bfc634 100644 --- a/ptyprocess/ptyprocess.py +++ b/ptyprocess/ptyprocess.py @@ -42,7 +42,7 @@ def _byte(i): else: def _byte(i): return chr(i) - + class FileNotFoundError(OSError): pass class TimeoutError(OSError): pass @@ -50,7 +50,7 @@ class TimeoutError(OSError): pass def _make_eof_intr(): """Set constants _EOF and _INTR. - + This avoids doing potentially costly operations on module load. """ global _EOF, _INTR @@ -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)' @@ -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 @@ -148,7 +148,7 @@ def write_to_stdout(b): write_to_stdout = sys.stdout.write encoding = None - + argv = None env = None launch_dir = None @@ -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 @@ -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. @@ -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) @@ -295,7 +301,7 @@ def spawn( # Parent inst = cls(pid, fd) - + # Set some informational attributes inst.argv = argv if env is not None: @@ -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) @@ -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 @@ -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) diff --git a/tests/test_spawn.py b/tests/test_spawn.py index 696e126..d372756 100755 --- a/tests/test_spawn.py +++ b/tests/test_spawn.py @@ -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 @@ -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'