-
-
Notifications
You must be signed in to change notification settings - Fork 34.5k
bpo-25942: make subprocess more graceful on ^C #5026
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
Changes from all commits
84e68de
4ee70f7
5190fe4
1c7cabb
3bd1021
ba5235b
db44980
424de82
4acf172
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -304,9 +304,9 @@ def call(*popenargs, timeout=None, **kwargs): | |
| with Popen(*popenargs, **kwargs) as p: | ||
| try: | ||
| return p.wait(timeout=timeout) | ||
| except: | ||
| except: # Including KeyboardInterrupt, wait handled that. | ||
| p.kill() | ||
| p.wait() | ||
| # We don't call p.wait() again as p.__exit__ does that for us. | ||
| raise | ||
|
|
||
|
|
||
|
|
@@ -450,9 +450,9 @@ def run(*popenargs, input=None, timeout=None, check=False, **kwargs): | |
| stdout, stderr = process.communicate() | ||
| raise TimeoutExpired(process.args, timeout, output=stdout, | ||
| stderr=stderr) | ||
| except: | ||
| except: # Including KeyboardInterrupt, communicate handled that. | ||
| process.kill() | ||
| process.wait() | ||
| # We don't call process.wait() as .__exit__ does that for us. | ||
| raise | ||
| retcode = process.poll() | ||
| if check and retcode: | ||
|
|
@@ -714,6 +714,11 @@ def __init__(self, args, bufsize=-1, executable=None, | |
|
|
||
| self.text_mode = encoding or errors or text or universal_newlines | ||
|
|
||
| # How long to resume waiting on a child after the first ^C. | ||
| # There is no right value for this. The purpose is to be polite | ||
| # yet remain good for interactive users trying to exit a tool. | ||
| self._sigint_wait_secs = 0.25 # 1/xkcd221.getRandomNumber() | ||
|
|
||
| self._closed_child_pipe_fds = False | ||
|
|
||
| try: | ||
|
|
@@ -787,7 +792,7 @@ def _translate_newlines(self, data, encoding, errors): | |
| def __enter__(self): | ||
| return self | ||
|
|
||
| def __exit__(self, type, value, traceback): | ||
| def __exit__(self, exc_type, value, traceback): | ||
| if self.stdout: | ||
| self.stdout.close() | ||
| if self.stderr: | ||
|
|
@@ -796,6 +801,22 @@ def __exit__(self, type, value, traceback): | |
| if self.stdin: | ||
| self.stdin.close() | ||
| finally: | ||
| if exc_type == KeyboardInterrupt: | ||
| # https://bugs.python.org/issue25942 | ||
| # In the case of a KeyboardInterrupt we assume the SIGINT | ||
| # was also already sent to our child processes. We can't | ||
| # block indefinitely as that is not user friendly. | ||
| # If we have not already waited a brief amount of time in | ||
| # an interrupted .wait() or .communicate() call, do so here | ||
| # for consistency. | ||
| if self._sigint_wait_secs > 0: | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Since this code is repeated is several places (
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. exit and the Popen instance have no knowledge of timeouts or whether the child process should be killed and in what circumstances. I didn't want to move that into the Popen instance. If I do, it would be internal-only APIs. I don't want to complicate the public Popen API surface further.
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. You could simply have a |
||
| try: | ||
| self._wait(timeout=self._sigint_wait_secs) | ||
| except TimeoutExpired: | ||
| pass | ||
| self._sigint_wait_secs = 0 # Note that this has been done. | ||
| return # resume the KeyboardInterrupt | ||
|
|
||
| # Wait for the process to terminate, to avoid zombies. | ||
| self.wait() | ||
|
|
||
|
|
@@ -804,7 +825,7 @@ def __del__(self, _maxsize=sys.maxsize, _warn=warnings.warn): | |
| # We didn't get to successfully create a child process. | ||
| return | ||
| if self.returncode is None: | ||
| # Not reading subprocess exit status creates a zombi process which | ||
| # Not reading subprocess exit status creates a zombie process which | ||
| # is only destroyed at the parent python process exit | ||
| _warn("subprocess %s is still running" % self.pid, | ||
| ResourceWarning, source=self) | ||
|
|
@@ -889,6 +910,21 @@ def communicate(self, input=None, timeout=None): | |
|
|
||
| try: | ||
| stdout, stderr = self._communicate(input, endtime, timeout) | ||
| except KeyboardInterrupt: | ||
| # https://bugs.python.org/issue25942 | ||
| # See the detailed comment in .wait(). | ||
| if timeout is not None: | ||
| sigint_timeout = min(self._sigint_wait_secs, | ||
| self._remaining_time(endtime)) | ||
| else: | ||
| sigint_timeout = self._sigint_wait_secs | ||
| self._sigint_wait_secs = 0 # nothing else should wait. | ||
| try: | ||
| self._wait(timeout=sigint_timeout) | ||
| except TimeoutExpired: | ||
| pass | ||
| raise # resume the KeyboardInterrupt | ||
|
|
||
| finally: | ||
| self._communication_started = True | ||
|
|
||
|
|
@@ -919,6 +955,30 @@ def _check_timeout(self, endtime, orig_timeout): | |
| raise TimeoutExpired(self.args, orig_timeout) | ||
|
|
||
|
|
||
| def wait(self, timeout=None): | ||
| """Wait for child process to terminate; returns self.returncode.""" | ||
| if timeout is not None: | ||
| endtime = _time() + timeout | ||
| try: | ||
| return self._wait(timeout=timeout) | ||
| except KeyboardInterrupt: | ||
| # https://bugs.python.org/issue25942 | ||
| # The first keyboard interrupt waits briefly for the child to | ||
| # exit under the common assumption that it also received the ^C | ||
| # generated SIGINT and will exit rapidly. | ||
| if timeout is not None: | ||
| sigint_timeout = min(self._sigint_wait_secs, | ||
| self._remaining_time(endtime)) | ||
| else: | ||
| sigint_timeout = self._sigint_wait_secs | ||
| self._sigint_wait_secs = 0 # nothing else should wait. | ||
| try: | ||
| self._wait(timeout=sigint_timeout) | ||
| except TimeoutExpired: | ||
| pass | ||
| raise # resume the KeyboardInterrupt | ||
|
|
||
|
|
||
| if _mswindows: | ||
| # | ||
| # Windows methods | ||
|
|
@@ -1127,16 +1187,16 @@ def _internal_poll(self, _deadstate=None, | |
| return self.returncode | ||
|
|
||
|
|
||
| def wait(self, timeout=None): | ||
| """Wait for child process to terminate. Returns returncode | ||
| attribute.""" | ||
| def _wait(self, timeout): | ||
| """Internal implementation of wait() on Windows.""" | ||
| if timeout is None: | ||
| timeout_millis = _winapi.INFINITE | ||
| else: | ||
| timeout_millis = int(timeout * 1000) | ||
| if self.returncode is None: | ||
| # API note: Returns immediately if timeout_millis == 0. | ||
| result = _winapi.WaitForSingleObject(self._handle, | ||
| timeout_millis) | ||
| timeout_millis) | ||
| if result == _winapi.WAIT_TIMEOUT: | ||
| raise TimeoutExpired(self.args, timeout) | ||
| self.returncode = _winapi.GetExitCodeProcess(self._handle) | ||
|
|
@@ -1498,9 +1558,8 @@ def _try_wait(self, wait_flags): | |
| return (pid, sts) | ||
|
|
||
|
|
||
| def wait(self, timeout=None): | ||
| """Wait for child process to terminate. Returns returncode | ||
| attribute.""" | ||
| def _wait(self, timeout): | ||
| """Internal implementation of wait() on POSIX.""" | ||
| if self.returncode is not None: | ||
| return self.returncode | ||
|
|
||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,6 @@ | ||
| The subprocess module is now more graceful when handling a Ctrl-C | ||
| KeyboardInterrupt during subprocess.call, subprocess.run, or a Popen context | ||
| manager. It now waits a short amount of time for the child (presumed to | ||
| have also gotten the SIGINT) to exit, before continuing the | ||
| KeyboardInterrupt exception handling. This still includes a SIGKILL in the | ||
| call() and run() APIs, but at least the child had a chance first. |
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.
Original work at https://www.random.org/analysis/dilbert.jpg :-)