]> granicus.if.org Git - python/commitdiff
Issue #6559: fix the subprocess.Popen pass_fds implementation. Add a unittest.
authorGregory P. Smith <greg@mad-scientist.com>
Tue, 14 Dec 2010 13:43:30 +0000 (13:43 +0000)
committerGregory P. Smith <greg@mad-scientist.com>
Tue, 14 Dec 2010 13:43:30 +0000 (13:43 +0000)
Issue #7213: Change the close_fds default on Windows to better match the new
default on POSIX.  True when possible (False if stdin/stdout/stderr are
supplied).

Update the documentation to reflect all of the above.

Doc/library/subprocess.rst
Lib/subprocess.py
Lib/test/test_subprocess.py
Misc/NEWS
Modules/_posixsubprocess.c

index df22c3355575704b376e6e2b9abfe952bacfa07b..e08dc8e95ee3a60858eddc7d5843c57bb688b225 100644 (file)
@@ -28,7 +28,7 @@ Using the subprocess Module
 This module defines one class called :class:`Popen`:
 
 
-.. class:: Popen(args, bufsize=0, executable=None, stdin=None, stdout=None, stderr=None, preexec_fn=None, close_fds=_PLATFORM_DEFAULT, shell=False, cwd=None, env=None, universal_newlines=False, startupinfo=None, creationflags=0, restore_signals=True, start_new_session=False)
+.. class:: Popen(args, bufsize=0, executable=None, stdin=None, stdout=None, stderr=None, preexec_fn=None, close_fds=True, shell=False, cwd=None, env=None, universal_newlines=False, startupinfo=None, creationflags=0, restore_signals=True, start_new_session=False, pass_fds=())
 
    Arguments are:
 
@@ -153,14 +153,22 @@ This module defines one class called :class:`Popen`:
 
    If *close_fds* is true, all file descriptors except :const:`0`, :const:`1` and
    :const:`2` will be closed before the child process is executed. (Unix only).
-   The default varies by platform: :const:`False` on Windows and :const:`True`
-   on POSIX and other platforms.
+   The default varies by platform:  Always true on Unix.  On Windows it is
+   true when *stdin*/*stdout*/*stderr* are :const:`None`, false otherwise.
    On Windows, if *close_fds* is true then no handles will be inherited by the
    child process.  Note that on Windows, you cannot set *close_fds* to true and
    also redirect the standard handles by setting *stdin*, *stdout* or *stderr*.
 
-.. versionchanged:: 3.2
-   The default was changed to True on non Windows platforms.
+   .. versionchanged:: 3.2
+      The default for *close_fds* was changed from :const:`False` to
+      what is described above.
+
+   *pass_fds* is an optional sequence of file descriptors to keep open
+   between the parent and child.  Providing any *pass_fds* forces
+   *close_fds* to be :const:`True`.  (Unix only)
+
+   .. versionadded:: 3.2
+      The *pass_fds* parameter was added.
 
    If *cwd* is not ``None``, the child's current directory will be changed to *cwd*
    before it is executed.  Note that this directory is not considered when
index 729a53b622ba9fc90cafab776d306d7ce826fbeb..ac46ce85c07610addcef06449e97c21593889975 100644 (file)
@@ -27,10 +27,10 @@ This module defines one class called Popen:
 
 class Popen(args, bufsize=0, executable=None,
             stdin=None, stdout=None, stderr=None,
-            preexec_fn=None, close_fds=_PLATFORM_DEFAULT, shell=False,
+            preexec_fn=None, close_fds=True, shell=False,
             cwd=None, env=None, universal_newlines=False,
             startupinfo=None, creationflags=0,
-            restore_signals=True, start_new_session=False):
+            restore_signals=True, start_new_session=False, pass_fds=()):
 
 
 Arguments are:
@@ -81,8 +81,11 @@ is executed.
 
 If close_fds is true, all file descriptors except 0, 1 and 2 will be
 closed before the child process is executed.  The default for close_fds
-varies by platform: False on Windows and True on all other platforms
-such as POSIX.
+varies by platform:  Always true on POSIX.  True when stdin/stdout/stderr
+are None on Windows, false otherwise.
+
+pass_fds is an optional sequence of file descriptors to keep open between the
+parent and child.  Providing any pass_fds implicitly sets close_fds to true.
 
 if shell is true, the specified command will be executed through the
 shell.
@@ -621,17 +624,14 @@ def getoutput(cmd):
     return getstatusoutput(cmd)[1]
 
 
-if mswindows:
-    _PLATFORM_DEFAULT = False
-else:
-    _PLATFORM_DEFAULT = True
+_PLATFORM_DEFAULT_CLOSE_FDS = object()
 
 
 class Popen(object):
     def __init__(self, args, bufsize=0, executable=None,
                  stdin=None, stdout=None, stderr=None,
-                 preexec_fn=None, close_fds=_PLATFORM_DEFAULT, shell=False,
-                 cwd=None, env=None, universal_newlines=False,
+                 preexec_fn=None, close_fds=_PLATFORM_DEFAULT_CLOSE_FDS,
+                 shell=False, cwd=None, env=None, universal_newlines=False,
                  startupinfo=None, creationflags=0,
                  restore_signals=True, start_new_session=False,
                  pass_fds=()):
@@ -648,12 +648,24 @@ class Popen(object):
             if preexec_fn is not None:
                 raise ValueError("preexec_fn is not supported on Windows "
                                  "platforms")
-            if close_fds and (stdin is not None or stdout is not None or
-                              stderr is not None):
-                raise ValueError("close_fds is not supported on Windows "
-                                 "platforms if you redirect stdin/stdout/stderr")
+            any_stdio_set = (stdin is not None or stdout is not None or
+                             stderr is not None)
+            if close_fds is _PLATFORM_DEFAULT_CLOSE_FDS:
+                if any_stdio_set:
+                    close_fds = False
+                else:
+                    close_fds = True
+            elif close_fds and any_stdio_set:
+                raise ValueError(
+                        "close_fds is not supported on Windows platforms"
+                        " if you redirect stdin/stdout/stderr")
         else:
             # POSIX
+            if close_fds is _PLATFORM_DEFAULT_CLOSE_FDS:
+                close_fds = True
+            if pass_fds and not close_fds:
+                warnings.warn("pass_fds overriding close_fds.", RuntimeWarning)
+                close_fds = True
             if startupinfo is not None:
                 raise ValueError("startupinfo is only supported on Windows "
                                  "platforms")
@@ -661,9 +673,6 @@ class Popen(object):
                 raise ValueError("creationflags is only supported on Windows "
                                  "platforms")
 
-        if pass_fds and not close_fds:
-            raise ValueError("pass_fds requires close_fds=True.")
-
         self.stdin = None
         self.stdout = None
         self.stderr = None
@@ -876,7 +885,7 @@ class Popen(object):
                            unused_restore_signals, unused_start_new_session):
             """Execute program (MS Windows version)"""
 
-            assert not pass_fds, "pass_fds not yet supported on Windows"
+            assert not pass_fds, "pass_fds not supported on Windows."
 
             if not isinstance(args, str):
                 args = list2cmdline(args)
@@ -1091,7 +1100,7 @@ class Popen(object):
             # precondition: fds_to_keep must be sorted and unique
             start_fd = 3
             for fd in fds_to_keep:
-                if fd > start_fd:
+                if fd >= start_fd:
                     os.closerange(start_fd, fd)
                     start_fd = fd + 1
             if start_fd <= MAXFD:
index 74e740438957af444f6ac45d9e066a906cac941a..0804a1322542750497c60c91182c84f3acc3ccd9 100644 (file)
@@ -1042,6 +1042,37 @@ class POSIXProcessTestCase(BaseTestCase):
                          "Some fds were left open")
         self.assertIn(1, remaining_fds, "Subprocess failed")
 
+    def test_pass_fds(self):
+        fd_status = support.findfile("fd_status.py", subdir="subprocessdata")
+
+        open_fds = set()
+
+        for x in range(5):
+            fds = os.pipe()
+            self.addCleanup(os.close, fds[0])
+            self.addCleanup(os.close, fds[1])
+            open_fds.update(fds)
+
+        for fd in open_fds:
+            p = subprocess.Popen([sys.executable, fd_status],
+                                 stdout=subprocess.PIPE, close_fds=True,
+                                 pass_fds=(fd, ))
+            output, ignored = p.communicate()
+
+            remaining_fds = set(map(int, output.split(b',')))
+            to_be_closed = open_fds - {fd}
+
+            self.assertIn(fd, remaining_fds, "fd to be passed not passed")
+            self.assertFalse(remaining_fds & to_be_closed,
+                             "fd to be closed passed")
+
+            # pass_fds overrides close_fds with a warning.
+            with self.assertWarns(RuntimeWarning) as context:
+                self.assertFalse(subprocess.call(
+                        [sys.executable, "-c", "import sys; sys.exit(0)"],
+                        close_fds=False, pass_fds=(fd, )))
+            self.assertIn('overriding close_fds', str(context.warning))
+
 
 @unittest.skipUnless(mswindows, "Windows specific tests")
 class Win32ProcessTestCase(BaseTestCase):
index 6ca5fc8f1a922326ef7198965b158dfdb2cad749..aa90c16a43f2e2fef150971d52a972d1d5e493f6 100644 (file)
--- a/Misc/NEWS
+++ b/Misc/NEWS
@@ -23,8 +23,12 @@ Library
 - Issue #10107: Warn about unsaved files in IDLE on OSX.
 
 - Issue #7213: subprocess.Popen's default for close_fds has been changed.
-  It is now platform specific, keeping its default of False on Windows and
-  changing the default to True on POSIX and other platforms.
+  It is now True in most cases other than on Windows when input, output or
+  error handles are provided.
+
+- Issue #6559: subprocess.Popen has a new pass_fds parameter (actually
+  added in 3.2beta1) to allow specifying a specific list of file descriptors
+  to keep open in the child process.
 
 
 What's New in Python 3.2 Beta 1?
index 0b5e54470fa8d50deb33f961d67ad51927e93fe2..5f226a851d318ae2d8d18f35c33d0fa96a1e1e16 100644 (file)
@@ -107,7 +107,7 @@ static void child_exec(char *const exec_array[],
                 errno = 0;  /* We don't want to report an OSError. */
                 goto error;
             }
-            if (keep_fd <= start_fd)
+            if (keep_fd < start_fd)
                 continue;
             for (fd_num = start_fd; fd_num < keep_fd; ++fd_num) {
                 close(fd_num);