diff options
author | Gregory P. Smith <greg@mad-scientist.com> | 2010-12-13 07:59:39 (GMT) |
---|---|---|
committer | Gregory P. Smith <greg@mad-scientist.com> | 2010-12-13 07:59:39 (GMT) |
commit | 51ee270876f4e18ec579e57772e16fce146d805e (patch) | |
tree | ca518334c744fca3ed243def81dd72d4b9f0c8ba /Lib | |
parent | f5604853889bfbbf84b48311c63c0e775dff38cc (diff) | |
download | cpython-51ee270876f4e18ec579e57772e16fce146d805e.zip cpython-51ee270876f4e18ec579e57772e16fce146d805e.tar.gz cpython-51ee270876f4e18ec579e57772e16fce146d805e.tar.bz2 |
issue7213: Open the pipes used by subprocesses with the FD_CLOEXEC flag from
the C code, using pipe2() when available. Adds unittests for close_fds and
cloexec behaviors.
Diffstat (limited to 'Lib')
-rw-r--r-- | Lib/subprocess.py | 36 | ||||
-rw-r--r-- | Lib/test/subprocessdata/fd_status.py | 24 | ||||
-rw-r--r-- | Lib/test/subprocessdata/input_reader.py | 7 | ||||
-rw-r--r-- | Lib/test/subprocessdata/qcat.py | 7 | ||||
-rw-r--r-- | Lib/test/subprocessdata/qgrep.py | 10 | ||||
-rw-r--r-- | Lib/test/test_subprocess.py | 78 |
6 files changed, 147 insertions, 15 deletions
diff --git a/Lib/subprocess.py b/Lib/subprocess.py index 62dfd36..729a53b 100644 --- a/Lib/subprocess.py +++ b/Lib/subprocess.py @@ -390,6 +390,23 @@ else: # POSIX defines PIPE_BUF as >= 512. _PIPE_BUF = getattr(select, 'PIPE_BUF', 512) + if _posixsubprocess: + _create_pipe = _posixsubprocess.cloexec_pipe + else: + def _create_pipe(): + try: + cloexec_flag = fcntl.FD_CLOEXEC + except AttributeError: + cloexec_flag = 1 + + fds = os.pipe() + + old = fcntl.fcntl(fds[0], fcntl.F_GETFD) + fcntl.fcntl(fds[0], fcntl.F_SETFD, old | cloexec_flag) + old = fcntl.fcntl(fds[1], fcntl.F_GETFD) + fcntl.fcntl(fds[1], fcntl.F_SETFD, old | cloexec_flag) + + return fds __all__ = ["Popen", "PIPE", "STDOUT", "call", "check_call", "getstatusoutput", "getoutput", "check_output", "CalledProcessError"] @@ -1031,7 +1048,7 @@ class Popen(object): if stdin is None: pass elif stdin == PIPE: - p2cread, p2cwrite = os.pipe() + p2cread, p2cwrite = _create_pipe() elif isinstance(stdin, int): p2cread = stdin else: @@ -1041,7 +1058,7 @@ class Popen(object): if stdout is None: pass elif stdout == PIPE: - c2pread, c2pwrite = os.pipe() + c2pread, c2pwrite = _create_pipe() elif isinstance(stdout, int): c2pwrite = stdout else: @@ -1051,7 +1068,7 @@ class Popen(object): if stderr is None: pass elif stderr == PIPE: - errread, errwrite = os.pipe() + errread, errwrite = _create_pipe() elif stderr == STDOUT: errwrite = c2pwrite elif isinstance(stderr, int): @@ -1065,16 +1082,6 @@ class Popen(object): errread, errwrite) - def _set_cloexec_flag(self, fd): - try: - cloexec_flag = fcntl.FD_CLOEXEC - except AttributeError: - cloexec_flag = 1 - - old = fcntl.fcntl(fd, fcntl.F_GETFD) - fcntl.fcntl(fd, fcntl.F_SETFD, old | cloexec_flag) - - def _close_fds(self, but): os.closerange(3, but) os.closerange(but + 1, MAXFD) @@ -1116,10 +1123,9 @@ class Popen(object): # For transferring possible exec failure from child to parent. # Data format: "exception name:hex errno:description" # Pickle is not used; it is complex and involves memory allocation. - errpipe_read, errpipe_write = os.pipe() + errpipe_read, errpipe_write = _create_pipe() try: try: - self._set_cloexec_flag(errpipe_write) if _posixsubprocess: # We must avoid complex work that could involve diff --git a/Lib/test/subprocessdata/fd_status.py b/Lib/test/subprocessdata/fd_status.py new file mode 100644 index 0000000..083b2f9 --- /dev/null +++ b/Lib/test/subprocessdata/fd_status.py @@ -0,0 +1,24 @@ +"""When called as a script, print a comma-separated list of the open +file descriptors on stdout.""" + +import errno +import os +import fcntl + +try: + _MAXFD = os.sysconf("SC_OPEN_MAX") +except: + _MAXFD = 256 + +def isopen(fd): + """Return True if the fd is open, and False otherwise""" + try: + fcntl.fcntl(fd, fcntl.F_GETFD, 0) + except IOError as e: + if e.errno == errno.EBADF: + return False + raise + return True + +if __name__ == "__main__": + print(','.join(str(fd) for fd in range(0, _MAXFD) if isopen(fd))) diff --git a/Lib/test/subprocessdata/input_reader.py b/Lib/test/subprocessdata/input_reader.py new file mode 100644 index 0000000..ccae5f3 --- /dev/null +++ b/Lib/test/subprocessdata/input_reader.py @@ -0,0 +1,7 @@ +"""When called as a script, consumes the input""" + +import sys + +if __name__ = "__main__": + for line in sys.stdin: + pass diff --git a/Lib/test/subprocessdata/qcat.py b/Lib/test/subprocessdata/qcat.py new file mode 100644 index 0000000..fe6f9db --- /dev/null +++ b/Lib/test/subprocessdata/qcat.py @@ -0,0 +1,7 @@ +"""When ran as a script, simulates cat with no arguments.""" + +import sys + +if __name__ == "__main__": + for line in sys.stdin: + sys.stdout.write(line) diff --git a/Lib/test/subprocessdata/qgrep.py b/Lib/test/subprocessdata/qgrep.py new file mode 100644 index 0000000..6990637 --- /dev/null +++ b/Lib/test/subprocessdata/qgrep.py @@ -0,0 +1,10 @@ +"""When called with a single argument, simulated fgrep with a single +argument and no options.""" + +import sys + +if __name__ == "__main__": + pattern = sys.argv[1] + for line in sys.stdin: + if pattern in line: + sys.stdout.write(line) diff --git a/Lib/test/test_subprocess.py b/Lib/test/test_subprocess.py index eaa26d2..74e7404 100644 --- a/Lib/test/test_subprocess.py +++ b/Lib/test/test_subprocess.py @@ -10,6 +10,7 @@ import time import re import sysconfig import warnings +import select try: import gc except ImportError: @@ -964,6 +965,83 @@ class POSIXProcessTestCase(BaseTestCase): exitcode = subprocess.call([program, "-c", "pass"], env=envb) self.assertEqual(exitcode, 0) + def test_pipe_cloexec(self): + sleeper = support.findfile("input_reader.py", subdir="subprocessdata") + fd_status = support.findfile("fd_status.py", subdir="subprocessdata") + + p1 = subprocess.Popen([sys.executable, sleeper], + stdin=subprocess.PIPE, stdout=subprocess.PIPE, + stderr=subprocess.PIPE, close_fds=False) + + self.addCleanup(p1.communicate, b'') + + p2 = subprocess.Popen([sys.executable, fd_status], + stdout=subprocess.PIPE, close_fds=False) + + output, error = p2.communicate() + result_fds = set(map(int, output.split(b','))) + unwanted_fds = set([p1.stdin.fileno(), p1.stdout.fileno(), + p1.stderr.fileno()]) + + self.assertFalse(result_fds & unwanted_fds, + "Expected no fds from %r to be open in child, " + "found %r" % + (unwanted_fds, result_fds & unwanted_fds)) + + def test_pipe_cloexec_real_tools(self): + qcat = support.findfile("qcat.py", subdir="subprocessdata") + qgrep = support.findfile("qgrep.py", subdir="subprocessdata") + + subdata = b'zxcvbn' + data = subdata * 4 + b'\n' + + p1 = subprocess.Popen([sys.executable, qcat], + stdin=subprocess.PIPE, stdout=subprocess.PIPE, + close_fds=False) + + p2 = subprocess.Popen([sys.executable, qgrep, subdata], + stdin=p1.stdout, stdout=subprocess.PIPE, + close_fds=False) + + self.addCleanup(p1.wait) + self.addCleanup(p2.wait) + self.addCleanup(p1.terminate) + self.addCleanup(p2.terminate) + + p1.stdin.write(data) + p1.stdin.close() + + readfiles, ignored1, ignored2 = select.select([p2.stdout], [], [], 10) + + self.assertTrue(readfiles, "The child hung") + self.assertEqual(p2.stdout.read(), data) + + def test_close_fds(self): + fd_status = support.findfile("fd_status.py", subdir="subprocessdata") + + fds = os.pipe() + self.addCleanup(os.close, fds[0]) + self.addCleanup(os.close, fds[1]) + + open_fds = set(fds) + + p = subprocess.Popen([sys.executable, fd_status], + stdout=subprocess.PIPE, close_fds=False) + output, ignored = p.communicate() + remaining_fds = set(map(int, output.split(b','))) + + self.assertEqual(remaining_fds & open_fds, open_fds, + "Some fds were closed") + + p = subprocess.Popen([sys.executable, fd_status], + stdout=subprocess.PIPE, close_fds=True) + output, ignored = p.communicate() + remaining_fds = set(map(int, output.split(b','))) + + self.assertFalse(remaining_fds & open_fds, + "Some fds were left open") + self.assertIn(1, remaining_fds, "Subprocess failed") + @unittest.skipUnless(mswindows, "Windows specific tests") class Win32ProcessTestCase(BaseTestCase): |