-
-
Notifications
You must be signed in to change notification settings - Fork 31.1k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
bpo-19764: Implemented support for subprocess.Popen(close_fds=True) on Windows #1218
bpo-19764: Implemented support for subprocess.Popen(close_fds=True) on Windows #1218
Conversation
Hello, and thanks for your contribution! I'm a bot set up to make sure that the project can legally accept your contribution by verifying you have signed the PSF contributor agreement (CLA). Unfortunately we couldn't find an account corresponding to your GitHub username on bugs.python.org (b.p.o) to verify you have signed the CLA. This is necessary for legal reasons before we can look at your contribution. Please follow the steps outlined in the CPython devguide to rectify this issue. Thanks again to your contribution and we look forward to looking at it! |
@segevfiner, thanks for your PR! By analyzing the history of the files in this pull request, we identified @gpshead, @zware and @serhiy-storchaka to be potential reviewers. |
2573232
to
cd2a322
Compare
Lib/subprocess.py
Outdated
handle_list = attribute_list.setdefault("handle_list", []) | ||
|
||
if use_std_handles: | ||
handle_list += [p2cread, c2pwrite, errwrite] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's probably better to add these handles as [int(p2cread), int(c2pwrite), int(errwrite)]
. The subprocess.Handle
instances aren't needed here.
Also, maybe it needs another issue, but I noticed that it's not setting self._closed_child_pipe_fds = True
after closing the handles in the finally
block in _execute_child
. Consequently the except
block in Popen.__init__
tries to call os.close
on a Windows handle when CreateProcess
fails. In a debug build this raises a failed assertion dialog.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actually, I vaguely remember seeing this debug assertion too, but I didn't look into it then. I will submit a separate issue and PR for this.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It actually seems to be worse! 😝 It's calling os.close on an HANDLE (os.close
is for file descriptors, emulated in user mode by msvcrt).
See: Issue #30121
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, I take for granted that we all know it's a bad thing to call os.close
on a Windows handle. I noticed it because self._closed_child_pipe_fds = True
isn't being set, but it needs to be fixed in both places because in principle instantiating Popen
could fail before the try/finally in _execute_child
.
Lib/subprocess.py
Outdated
handle_list[:] = self._filter_handle_list(handle_list) | ||
|
||
if handle_list: | ||
if have_handle_list and not close_fds: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This should just check not close_fds
because have_handle_list
must be true if that's the case, on account of the outer check on line 989.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You can get here with have_handle_list = False
by not passing an handle_list explicitly and requesting stdio redirection.
import subprocess
subprocess.Popen(['notepad.exe'], stdout=subprocess.PIPE)
The condition on line 989 is for either have_handle_list
or having requested stdio redirection with close_fds
.
have_handle_list
really means that the user has explicitly passed in a non empty handle_list. I placed the check for the warning there instead of someplace else, that might have been clearer, so that we will only give the warning if we actually override close_fds
. Maybe a different name for have_handle_list
will make this clearer? If you can come up with one...
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We want the warning when using handle_list
with close_fds
overridden as false, which can only occur when have_handle_list
is true. If we get to the check on line 1000 and close_fds
is false, then we already know that have_handle_list
is true. We don't need to check it.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Whoops the if
there was wrong in a different way... not close_fds
should have been close_fds
and have_handle_list was pointing to have reference to handle_list which was later modified also changing have_handle_list.
The following should not generate a warning:
>>> import subprocess
>>> subprocess.Popen(['notepad.exe'], stdout=subprocess.PIPE)
<subprocess.Popen object at 0x03A0FFC0>
>>> # No warning
The following should:
>>> import os, subprocess, msvcrt
>>> f=open('bla.txt', 'w')
>>> handle = msvcrt.get_osfhandle(f.fileno())
>>> startupinfo = subprocess.STARTUPINFO()
>>> startupinfo.lpAttributeList["handle_list"] = [handle]
>>> os.set_inheritable(f.fileno(), True)
>>> subprocess.Popen(['notepad.exe'], startupinfo=startupinfo)
C:\Users\Segev\prj\python\cpython\lib\subprocess.py:1002: RuntimeWarning: startupinfo.lpAttributeList['handle_list'] overriding close_fds
"overriding close_fds", RuntimeWarning)
<subprocess.Popen object at 0x03B0E9A0>
>>>
If I remove have_handle_list
from the if than you get a warning for the first example:
>>> import subprocess
>>> subprocess.Popen(['notepad.exe'], stdout=subprocess.PIPE)
C:\Users\Segev\prj\python\cpython\lib\subprocess.py:1002: RuntimeWarning: startupinfo.lpAttributeList['handle_list'] overriding close_fds
"overriding close_fds", RuntimeWarning)
<subprocess.Popen object at 0x02C0FFC0>
>>>
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
At the level of the subprocess API, close_fds
means the child will not inherit all of the inheritable handles of the current process, except for the standard handles, and if close_fds
is false, it means all inheritable handles will be inherited. Using an explicit handle_list
inherits only a given subset of handles and, like pass_fds
, it's acting like an extension of the standard set of 3 handles or file descriptors. As such, it should be used with close_fds
as true. For pass_fds
a warning is raised when close_fds
is false because it's a mixed message and the code is opting to override close_fds
. For consistency, using handle_list
with close_fds
as false should raise a similar warning. I know it's different in the Windows API, which instead modifies what bInheritHandles means when given a handle list. But I think the goal in subprocess.Popen
is try to make the semantics as universal as possible. pass_fds
and handle_list
are obviously different, but they share the semantics of the common close_fds
parameter.
As far as I can see -- both from reading and testing -- removing have_handle_list
from the if
statement should never cause a warning to be raised when only redirecting the standard handles. If there's no explicitly passed handle_list
, this code block executes only when close_fds
is true. So if you're checking not close_fds
to gate the warning, there's no way it will be raised. If close_fds
is false when redirecting the standard handles, normal inheritance of the entire process handle table is used -- unless of course a handle_list
is already present.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Haaaa. I misunderstood that you wanted the warning for the close_fds=True
which, as you stated, is kinda confusing in regards to portable behavior.
…n Windows Even though Python marks any handles it opens as non-inheritable there is still a race when using `subprocess.Popen` since creating a process with redirected stdio requires temporarily creating inheritable handles. By implementing support for `subprocess.Popen(close_fds=True)` we fix this race. In order to implement this we use PROC_THREAD_ATTRIBUTE_HANDLE_LIST which is available since Windows Vista. Which allows to pass an explicit list of handles to inherit when creating a process. This commit also adds `STARTUPINFO.lpAttributeList["handle_list"]` which can be used to control PROC_THREAD_ATTRIBUTE_HANDLE_LIST directly.
Also fixed the documentation for handle_list.
af4e1d5
to
9ad94f3
Compare
BTW. I'm not sure if you commonly do this for CPython. but I can squash the fixup commits once you are done reviewing, if you wish. |
Please do not squash, rebase, or otherwise overwrite the history of your PR. It will be squashed by the core developer who merges it. |
@zware This is why I asked rather than doing it 👍 |
@zooba, @eryksun: Would you mind to review this change. IMHO it's a major enhancement for Windows of the subprocess module. It would solve old an important use cases which currently requires an ugly close_fds=False workaround. See http://bugs.python.org/issue19764 for the rationale. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Couple of minor comments, but looks good to me.
goto cleanup; | ||
|
||
for (i = 0; i < PySequence_Fast_GET_SIZE(value_fast); i++) { | ||
ret[i] = PYNUM_TO_HANDLE(PySequence_Fast_GET_ITEM(value_fast, i)); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
PYNUM_TO_HANDLE
(actually PyLong_AsUnsignedLong[Long]()
) could raise OverflowError or TypeError, which should really cause us to break out of the loop immediately. Otherwise we may end up masking the error or hitting a codepath that causes an immediate failure with an error set (though this seems unlikely here).
Modules/_winapi.c
Outdated
PyObject *value; | ||
Py_ssize_t handle_list_size; | ||
DWORD attribute_count = 0; | ||
SIZE_T attribute_list_size; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should be initialized (yes, the first use is as an out parameter, but it's actually annotated as in/out and so should be set to something before calling)
Modules/_winapi.c
Outdated
attribute_list->attribute_list = NULL; | ||
|
||
ret = -1; | ||
PyErr_SetFromWindowsErr(GetLastError()); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Need to call GetLastError before PyMem_Free, which might overwrite the error code.
Codecov Report
@@ Coverage Diff @@
## master #1218 +/- ##
==========================================
- Coverage 83.44% 82.68% -0.77%
==========================================
Files 1367 1432 +65
Lines 346128 353486 +7358
==========================================
+ Hits 288823 292272 +3449
- Misses 57305 61214 +3909
Continue to review full report at Codecov.
|
@zooba I implemented the fixes. (Just in case GitHub didn't notify you for some reason 😖) |
I'd like @eryksun's approval as well, since he did a couple of reviews already. We can ignore |
Doc/library/subprocess.rst
Outdated
|
||
**handle_list** | ||
When *close_fds* is :const:`True`, supplies a list of | ||
handles that will be inherited. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I suggest the doc:
"Sequence of handles that will be inherited. close_fds must be true if non-empty."
(And make sure that any sequence type is accepted.)
@@ -134,6 +134,7 @@ def __init__(self, *, dwFlags=0, hStdInput=None, hStdOutput=None, | |||
self.hStdOutput = hStdOutput |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
Lib/subprocess.py
Outdated
@@ -608,25 +609,15 @@ def __init__(self, args, bufsize=-1, executable=None, | |||
if not isinstance(bufsize, int): | |||
raise TypeError("bufsize must be an integer") | |||
|
|||
if close_fds is _PLATFORM_DEFAULT_CLOSE_FDS: | |||
close_fds = True |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I suggest to now remove _PLATFORM_DEFAULT_CLOSE_FDS and always set default to True.
Lib/subprocess.py
Outdated
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""" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Please document also that it removes duplicate handles. (Move the comment into the docstring.)
I suggest to document the change in Doc/whatsnew/3.7.rst. IMHO it's a major change. Is there a risk of backward incompatibility issue? If yes, mention also the change in the "Porting to Python 3.7" section. |
Should I rebase or merge to fix conflicts? |
Merge. |
Oh, I see. If there is a risk of race condition: don't do anything, raise an exception and document the requirement. asyncio does something similar to sockets expected to be non-blocking. |
Doc/library/subprocess.rst
Outdated
Sequence of handles that will be inherited. *close_fds* must be true if | ||
non-empty. | ||
|
||
The handles must be made inheritable by |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You could say "The handles must be temporarily made inheritable by os.set_handle_inheritable
when passed to the Popen
constructor, else OSError
will be raised...". Then the second sentence can be removed.
The second paragraph I think should be a warning. How about this?
.. warning::
In a multithreaded process, use caution to avoid leaking handles
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.
4ce6fd0
to
d4a9cdb
Compare
Merged from master due to argument clinic changes. |
@eryksun Is there anything else that you think should be improved/fixed? It would be nice to know if everything seems fine. 😉 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
While I'm not in a position to comment on Windows specifics, I think what you have done here is a good thing and will make Windows users happy. +1 for accepting this change. I must defer to someone with actual Windows knowledge to press the merge button. Your documentation and common subprocess.py code dealing with the formerly different default close_fds
behavior updates look reasonable to me.
Applied some anti bit rot grease... |
Merged from master... Again... Hopefully this won't end up missing 3.7 entirely... 😔 I guess CPython is completely swamped what with the PR list getting bigger rather than smaller over time... |
Oops sorry, I wanted this feature but I didn't follow closely the PR. I don't know well the Windows API, so I didn't want to take the responsability of reviewing (approving) such PR. But I see that @zooba and @gpshead approved it, so I'm now confortable to merge it :-) Moreover, AppVeyor validated the PR, so let me merge it. I prefer to merge the PR right now to not miss the Python 3.7 feature freeze, and maybe fix issues later if needed, before 3.7 final. Thank you @segevfiner for this major subprocess enhancement. I really love to see close_fds default changing to True on Windows. It will help to fix many corner cases which are very tricky to debug. Sorry for the slow review, but the subprocess is a critical module of Python, and we lack of Windows developers to review changes specific to Windows. |
Even though Python marks any handles it opens as non-inheritable there
is still a race when using
subprocess.Popen
since creating a processwith redirected stdio requires temporarily creating inheritable handles.
By implementing support for
subprocess.Popen(close_fds=True)
we fixthis race.
In order to implement this we use PROC_THREAD_ATTRIBUTE_HANDLE_LIST
which is available since Windows Vista. Which allows to pass an explicit
list of handles to inherit when creating a process.
This commit also adds
STARTUPINFO.lpAttributeList["handle_list"]
which can be used to control PROC_THREAD_ATTRIBUTE_HANDLE_LIST directly.
https://bugs.python.org/issue19764