mirror of
https://github.com/python/cpython.git
synced 2025-08-14 05:52:50 +00:00
bpo-37424: Avoid a hang in subprocess.run timeout output capture (GH-14490)
Fixes a possible hang when using a timeout on subprocess.run() while
capturing output. If the child process spawned its own children or otherwise
connected its stdout or stderr handles with another process, we could hang
after the timeout was reached and our child was killed when attempting to read
final output from the pipes.
(cherry picked from commit 580d2782f7
)
Co-authored-by: Gregory P. Smith <greg@krypto.org>
This commit is contained in:
parent
f357cd022e
commit
872c85a179
3 changed files with 53 additions and 9 deletions
|
@ -489,11 +489,20 @@ def run(*popenargs,
|
|||
with Popen(*popenargs, **kwargs) as process:
|
||||
try:
|
||||
stdout, stderr = process.communicate(input, timeout=timeout)
|
||||
except TimeoutExpired:
|
||||
except TimeoutExpired as exc:
|
||||
process.kill()
|
||||
stdout, stderr = process.communicate()
|
||||
raise TimeoutExpired(process.args, timeout, output=stdout,
|
||||
stderr=stderr)
|
||||
if _mswindows:
|
||||
# Windows accumulates the output in a single blocking
|
||||
# read() call run on child threads, with the timeout
|
||||
# being done in a join() on those threads. communicate()
|
||||
# _after_ kill() is required to collect that and add it
|
||||
# to the exception.
|
||||
exc.stdout, exc.stderr = process.communicate()
|
||||
else:
|
||||
# POSIX _communicate already populated the output so
|
||||
# far into the TimeoutExpired exception.
|
||||
process.wait()
|
||||
raise
|
||||
except: # Including KeyboardInterrupt, communicate handled that.
|
||||
process.kill()
|
||||
# We don't call process.wait() as .__exit__ does that for us.
|
||||
|
@ -1050,12 +1059,16 @@ class Popen(object):
|
|||
return endtime - _time()
|
||||
|
||||
|
||||
def _check_timeout(self, endtime, orig_timeout):
|
||||
def _check_timeout(self, endtime, orig_timeout, stdout_seq, stderr_seq,
|
||||
skip_check_and_raise=False):
|
||||
"""Convenience for checking if a timeout has expired."""
|
||||
if endtime is None:
|
||||
return
|
||||
if _time() > endtime:
|
||||
raise TimeoutExpired(self.args, orig_timeout)
|
||||
if skip_check_and_raise or _time() > endtime:
|
||||
raise TimeoutExpired(
|
||||
self.args, orig_timeout,
|
||||
output=b''.join(stdout_seq) if stdout_seq else None,
|
||||
stderr=b''.join(stderr_seq) if stderr_seq else None)
|
||||
|
||||
|
||||
def wait(self, timeout=None):
|
||||
|
@ -1843,10 +1856,15 @@ class Popen(object):
|
|||
while selector.get_map():
|
||||
timeout = self._remaining_time(endtime)
|
||||
if timeout is not None and timeout < 0:
|
||||
raise TimeoutExpired(self.args, orig_timeout)
|
||||
self._check_timeout(endtime, orig_timeout,
|
||||
stdout, stderr,
|
||||
skip_check_and_raise=True)
|
||||
raise RuntimeError( # Impossible :)
|
||||
'_check_timeout(..., skip_check_and_raise=True) '
|
||||
'failed to raise TimeoutExpired.')
|
||||
|
||||
ready = selector.select(timeout)
|
||||
self._check_timeout(endtime, orig_timeout)
|
||||
self._check_timeout(endtime, orig_timeout, stdout, stderr)
|
||||
|
||||
# XXX Rewrite these to use non-blocking I/O on the file
|
||||
# objects; they are no longer using C stdio!
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue