Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
7e68016
bpo-19764: Implemented support for subprocess.Popen(close_fds=True) o…
segevfiner Apr 20, 2017
ad07b10
bpo-19764: Convert subprocess.Handle to int in handle_list
segevfiner Apr 20, 2017
9ad94f3
bpo-19764: Fixed the handle_list warning with close_fds=True
segevfiner Apr 21, 2017
24f3f39
bpo-19764: Added a test for the overriding close_fds warning
segevfiner Apr 21, 2017
1eab242
bpo-19764: The warning should be emitted for handle_list and close_fd…
segevfiner Apr 21, 2017
5b3cccc
Minor fixes in _winapi.c
segevfiner May 31, 2017
c84449c
bpo-19764: Addressed review comments by haypo
segevfiner Jun 6, 2017
f849f92
Merge remote-tracking branch 'upstream/master' into windows-subproces…
segevfiner Jun 6, 2017
0e60d28
bpo-19764: Simplified the handle_list cast to list
segevfiner Jun 6, 2017
167590b
bpo-19764: Add some extra documentation about handle_list
segevfiner Jun 7, 2017
59af91f
bpo-19764: Documentation update
segevfiner Jun 7, 2017
d4a9cdb
bpo-19764: Fixed bad reStructuredText markup
segevfiner Jun 8, 2017
15424fa
Merge remote-tracking branch 'upstream/master' into windows-subproces…
segevfiner Jun 10, 2017
dc1e551
Merge remote-tracking branch 'upstream/master' into windows-subproces…
segevfiner Jun 24, 2017
d99af74
Merge branch 'master' into windows-subprocess-close-fds
segevfiner Jul 2, 2017
c63e045
Merge remote-tracking branch 'upstream/master' into windows-subproces…
segevfiner Jul 6, 2017
f50ca37
Merge branch 'master' into windows-subprocess-close-fds
segevfiner Aug 4, 2017
38b4526
Merge remote-tracking branch 'upstream/master' into windows-subproces…
segevfiner Aug 18, 2017
8e5d21f
bpo-19764: Fix whitespace
segevfiner Aug 18, 2017
208274c
bpo-19764: Add NEWS.d entry
segevfiner Aug 18, 2017
869b54c
Merge branch 'master' into windows-subprocess-close-fds
segevfiner Sep 19, 2017
66b74f6
Merge remote-tracking branch 'upstream/master' into windows-subproces…
segevfiner Nov 17, 2017
ba19bd8
Merge remote-tracking branch 'upstream/master' into windows-subproces…
segevfiner Dec 16, 2017
3b0426e
Add Segev Finer to Misc/ACKS
segevfiner Dec 16, 2017
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
42 changes: 36 additions & 6 deletions Doc/library/subprocess.rst
Original file line number Diff line number Diff line change
Expand Up @@ -452,17 +452,20 @@ functions.
common use of *preexec_fn* to call os.setsid() in the child.

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. (POSIX only).
The default varies by platform: Always true on POSIX. On Windows it is
true when *stdin*/*stdout*/*stderr* are :const:`None`, false otherwise.
:const:`2` will be closed before the child process is executed.
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*.
child process unless explicitly passed in the ``handle_list`` element of
:attr:`STARTUPINFO.lpAttributeList`, or by standard handle redirection.

.. versionchanged:: 3.2
The default for *close_fds* was changed from :const:`False` to
what is described above.

.. versionchanged:: 3.7
On Windows the default for *close_fds* was changed from :const:`False` to
:const:`True` when redirecting the standard handles. It's now possible to
set *close_fds* to :const:`True` when redirecting the standard handles.

*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`. (POSIX only)
Expand Down Expand Up @@ -764,7 +767,7 @@ The :class:`STARTUPINFO` class and following constants are only available
on Windows.

.. class:: STARTUPINFO(*, dwFlags=0, hStdInput=None, hStdOutput=None, \
hStdError=None, wShowWindow=0)
hStdError=None, wShowWindow=0, lpAttributeList=None)

Partial support of the Windows
`STARTUPINFO <https://msdn.microsoft.com/en-us/library/ms686331(v=vs.85).aspx>`__
Expand Down Expand Up @@ -814,6 +817,33 @@ on Windows.
:data:`SW_HIDE` is provided for this attribute. It is used when
:class:`Popen` is called with ``shell=True``.

.. attribute:: lpAttributeList

A dictionary of additional attributes for process creation as given in
``STARTUPINFOEX``, see
`UpdateProcThreadAttribute <https://msdn.microsoft.com/en-us/library/windows/desktop/ms686880(v=vs.85).aspx>`__.

Supported attributes:

**handle_list**
Sequence of handles that will be inherited. *close_fds* must be true if
non-empty.

The handles must be temporarily made inheritable by
:func:`os.set_handle_inheritable` when passed to the :class:`Popen`
constructor, else :class:`OSError` will be raised with Windows error
``ERROR_INVALID_PARAMETER`` (87).

.. warning::

In a multithreaded process, use caution to avoid leaking handles
that are marked inheritable when combining this feature with
concurrent calls to other process creation functions that inherit
all handles such as :func:`os.system`. This also applies to
standard handle redirection, which temporarily creates inheritable
handles.

.. versionadded:: 3.7

Windows Constants
^^^^^^^^^^^^^^^^^
Expand Down
19 changes: 19 additions & 0 deletions Doc/whatsnew/3.7.rst
Original file line number Diff line number Diff line change
Expand Up @@ -437,6 +437,17 @@ string
expression pattern for braced placeholders and non-braced placeholders
separately. (Contributed by Barry Warsaw in :issue:`1198569`.)

subprocess
----------

On Windows the default for *close_fds* was changed from :const:`False` to
:const:`True` when redirecting the standard handles. It's now possible to set
*close_fds* to :const:`True` when redirecting the standard handles. See
:class:`subprocess.Popen`.

This means that *close_fds* now defaults to :const:`True` on all supported
platforms.

sys
---

Expand Down Expand Up @@ -883,6 +894,14 @@ Changes in the Python API

.. _Unicode Technical Standard #18: https://unicode.org/reports/tr18/

* On Windows the default for the *close_fds* argument of
:class:`subprocess.Popen` was changed from :const:`False` to :const:`True`
when redirecting the standard handles. If you previously depended on handles
being inherited when using :class:`subprocess.Popen` with standard io
redirection, you will have to pass ``close_fds=False`` to preserve the
previous behaviour, or use
:attr:`STARTUPINFO.lpAttributeList <subprocess.STARTUPINFO.lpAttributeList>`.


Changes in the C API
--------------------
Expand Down
65 changes: 46 additions & 19 deletions Lib/subprocess.py
Original file line number Diff line number Diff line change
Expand Up @@ -128,12 +128,13 @@ def stdout(self, value):
import _winapi
class STARTUPINFO:
def __init__(self, *, dwFlags=0, hStdInput=None, hStdOutput=None,
hStdError=None, wShowWindow=0):
hStdError=None, wShowWindow=0, lpAttributeList=None):
self.dwFlags = dwFlags
self.hStdInput = hStdInput
self.hStdOutput = hStdOutput
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It may be convenient to be able to pass lpAttributeList in the constructor.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For convenience, do you think the handles in handle_list should be automatically duplicated as inheritable via _make_inheritable? The problem with that is managing the lifetime of the Handle instances. It would have to make a private copy of STARTUPINFO, which maybe it should be doing anyway.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think the caller would expect the handles he passes to be inherited and not a duplicate of them. The usage is to be able to selectively inherit some specific handles after all. A script using this is very likely to pass the handles he passed to handle_list in the command line or some other IPC mechanism for the usage of the sub process, otherwise what is the point of inheriting those handles besides leaking them?

Imagine a process opening some anonymous shared memory and passing that handle on the command line for a new process, making sure to properly inherit it via handle_list. The new process will have access to the shared memory and no other additional handles which it won't even know about, unless any others are passed.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You're absolutely right. I was only thinking narrowly about a user passing in a handle list without considering the context.

Can you update the docs for handle_list to note that the handles in the list have to be made inheritable via os.set_handle_inheritable, else OSError will be raised for the error code ERROR_INVALID_PARAMETER (87). Otherwise users may expect it to work like the pass_fds parameter. Including the error code in the docs may help with searching, since it's such a generic error.

Do you think it needs a warning that concurrent calls to CreateProcess that inherit all handles may inadvertently inherit the handles in this list? The race condition can only be avoided with careful design, such as by always using Popen with a handle list and avoiding concurrent calls to os.system and os.spawn* or by using a lock to synchronize process creation.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I tried to add some more documentation about this but I'm not really sure it's the right place for it. Feel free to suggest better wording or a better place for it.

self.hStdError = hStdError
self.wShowWindow = wShowWindow
self.lpAttributeList = lpAttributeList or {"handle_list": []}
else:
import _posixsubprocess
import select
Expand Down Expand Up @@ -577,9 +578,6 @@ def getoutput(cmd):
return getstatusoutput(cmd)[1]


_PLATFORM_DEFAULT_CLOSE_FDS = object()


class Popen(object):
""" Execute a child program in a new process.

Expand Down Expand Up @@ -630,7 +628,7 @@ class Popen(object):

def __init__(self, args, bufsize=-1, executable=None,
stdin=None, stdout=None, stderr=None,
preexec_fn=None, close_fds=_PLATFORM_DEFAULT_CLOSE_FDS,
preexec_fn=None, close_fds=True,
shell=False, cwd=None, env=None, universal_newlines=None,
startupinfo=None, creationflags=0,
restore_signals=True, start_new_session=False,
Expand All @@ -655,21 +653,8 @@ def __init__(self, args, bufsize=-1, executable=None,
if preexec_fn is not None:
raise ValueError("preexec_fn is not supported on Windows "
"platforms")
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
Expand Down Expand Up @@ -1019,6 +1004,19 @@ def _make_inheritable(self, handle):
return Handle(h)


def _filter_handle_list(self, handle_list):
"""Filter out console handles that can't be used
in lpAttributeList["handle_list"] and make sure the list
isn't empty. This also removes duplicate handles."""
# An handle with it's lowest two bits set might be a special console
# handle that if passed in lpAttributeList["handle_list"], will
# cause it to fail.
return list({handle for handle in handle_list
if handle & 0x3 != 0x3
or _winapi.GetFileType(handle) !=
_winapi.FILE_TYPE_CHAR})


def _execute_child(self, args, executable, preexec_fn, close_fds,
pass_fds, cwd, env,
startupinfo, creationflags, shell,
Expand All @@ -1036,12 +1034,41 @@ def _execute_child(self, args, executable, preexec_fn, close_fds,
# Process startup details
if startupinfo is None:
startupinfo = STARTUPINFO()
if -1 not in (p2cread, c2pwrite, errwrite):

use_std_handles = -1 not in (p2cread, c2pwrite, errwrite)
if use_std_handles:
startupinfo.dwFlags |= _winapi.STARTF_USESTDHANDLES
startupinfo.hStdInput = p2cread
startupinfo.hStdOutput = c2pwrite
startupinfo.hStdError = errwrite

attribute_list = startupinfo.lpAttributeList
have_handle_list = bool(attribute_list and
"handle_list" in attribute_list and
attribute_list["handle_list"])

# If we were given an handle_list or need to create one
if have_handle_list or (use_std_handles and close_fds):
if attribute_list is None:
attribute_list = startupinfo.lpAttributeList = {}
handle_list = attribute_list["handle_list"] = \
list(attribute_list.get("handle_list", []))

if use_std_handles:
handle_list += [int(p2cread), int(c2pwrite), int(errwrite)]

handle_list[:] = self._filter_handle_list(handle_list)

if handle_list:
if not close_fds:
warnings.warn("startupinfo.lpAttributeList['handle_list'] "
"overriding close_fds", RuntimeWarning)

# When using the handle_list we always request to inherit
# handles but the only handles that will be inherited are
# the ones in the handle_list
close_fds = False

if shell:
startupinfo.dwFlags |= _winapi.STARTF_USESHOWWINDOW
startupinfo.wShowWindow = _winapi.SW_HIDE
Expand Down
66 changes: 61 additions & 5 deletions Lib/test/test_subprocess.py
Original file line number Diff line number Diff line change
Expand Up @@ -2743,11 +2743,6 @@ def test_invalid_args(self):
[sys.executable, "-c",
"import sys; sys.exit(47)"],
preexec_fn=lambda: 1)
self.assertRaises(ValueError, subprocess.call,
[sys.executable, "-c",
"import sys; sys.exit(47)"],
stdout=subprocess.PIPE,
close_fds=True)

@support.cpython_only
def test_issue31471(self):
Expand All @@ -2765,6 +2760,67 @@ def test_close_fds(self):
close_fds=True)
self.assertEqual(rc, 47)

def test_close_fds_with_stdio(self):
import msvcrt

fds = os.pipe()
self.addCleanup(os.close, fds[0])
self.addCleanup(os.close, fds[1])

handles = []
for fd in fds:
os.set_inheritable(fd, True)
handles.append(msvcrt.get_osfhandle(fd))

p = subprocess.Popen([sys.executable, "-c",
"import msvcrt; print(msvcrt.open_osfhandle({}, 0))".format(handles[0])],
stdout=subprocess.PIPE, close_fds=False)
stdout, stderr = p.communicate()
self.assertEqual(p.returncode, 0)
int(stdout.strip()) # Check that stdout is an integer

p = subprocess.Popen([sys.executable, "-c",
"import msvcrt; print(msvcrt.open_osfhandle({}, 0))".format(handles[0])],
stdout=subprocess.PIPE, stderr=subprocess.PIPE, close_fds=True)
stdout, stderr = p.communicate()
self.assertEqual(p.returncode, 1)
self.assertIn(b"OSError", stderr)

# The same as the previous call, but with an empty handle_list
handle_list = []
startupinfo = subprocess.STARTUPINFO()
startupinfo.lpAttributeList = {"handle_list": handle_list}
p = subprocess.Popen([sys.executable, "-c",
"import msvcrt; print(msvcrt.open_osfhandle({}, 0))".format(handles[0])],
stdout=subprocess.PIPE, stderr=subprocess.PIPE,
startupinfo=startupinfo, close_fds=True)
stdout, stderr = p.communicate()
self.assertEqual(p.returncode, 1)
self.assertIn(b"OSError", stderr)

# Check for a warning due to using handle_list and close_fds=False
with support.check_warnings((".*overriding close_fds", RuntimeWarning)):
startupinfo = subprocess.STARTUPINFO()
startupinfo.lpAttributeList = {"handle_list": handles[:]}
p = subprocess.Popen([sys.executable, "-c",
"import msvcrt; print(msvcrt.open_osfhandle({}, 0))".format(handles[0])],
stdout=subprocess.PIPE, stderr=subprocess.PIPE,
startupinfo=startupinfo, close_fds=False)
stdout, stderr = p.communicate()
self.assertEqual(p.returncode, 0)

def test_empty_attribute_list(self):
startupinfo = subprocess.STARTUPINFO()
startupinfo.lpAttributeList = {}
subprocess.call([sys.executable, "-c", "import sys; sys.exit(0)"],
startupinfo=startupinfo)

def test_empty_handle_list(self):
startupinfo = subprocess.STARTUPINFO()
startupinfo.lpAttributeList = {"handle_list": []}
subprocess.call([sys.executable, "-c", "import sys; sys.exit(0)"],
startupinfo=startupinfo)

def test_shell_sequence(self):
# Run command through the shell (sequence)
newenv = os.environ.copy()
Expand Down
1 change: 1 addition & 0 deletions Misc/ACKS
Original file line number Diff line number Diff line change
Expand Up @@ -466,6 +466,7 @@ Carl Feynman
Vincent Fiack
Anastasia Filatova
Tomer Filiba
Segev Finer
Jeffrey Finkelstein
Russell Finn
Dan Finnie
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
Implement support for `subprocess.Popen(close_fds=True)` on Windows. Patch
by Segev Finer.
Loading