If the process does not terminate after *timeout* seconds, a
:exc:`TimeoutExpired` exception will be raised. Catching this exception and
- retrying communication will not lose any output.
+ retrying communication will not lose any output. Supplying *input* to a
+ subsequent post-timeout :meth:`communicate` call is in undefined behavior
+ and may become an error in the future.
The child process is not killed if the timeout expires, so in order to
cleanup properly a well-behaved application should kill the child process and
input_view = memoryview(self._input)
with _PopenSelector() as selector:
- if self.stdin and input:
+ if self.stdin and not self.stdin.closed and self._input:
selector.register(self.stdin, selectors.EVENT_WRITE)
if self.stdout and not self.stdout.closed:
selector.register(self.stdout, selectors.EVENT_READ)
self.assertEqual(proc.wait(), 0)
+ def test_post_timeout_communicate_sends_input(self):
+ """GH-141473 regression test; the stdin pipe must close"""
+ with subprocess.Popen(
+ [sys.executable, "-uc", """\
+import sys
+while c := sys.stdin.read(512):
+ sys.stdout.write(c)
+print()
+"""],
+ stdin=subprocess.PIPE,
+ stdout=subprocess.PIPE,
+ stderr=subprocess.PIPE,
+ text=True,
+ ) as proc:
+ try:
+ data = f"spam{'#'*4096}beans"
+ proc.communicate(
+ input=data,
+ timeout=0,
+ )
+ except subprocess.TimeoutExpired as exc:
+ pass
+ # Prior to the bugfix, this would hang as the stdin
+ # pipe to the child had not been closed.
+ try:
+ stdout, stderr = proc.communicate(timeout=15)
+ except subprocess.TimeoutExpired as exc:
+ self.fail("communicate() hung waiting on child process that should have seen its stdin pipe close and exit")
+ self.assertEqual(
+ proc.returncode, 0,
+ msg=f"STDERR:\n{stderr}\nSTDOUT:\n{stdout}")
+ self.assertStartsWith(stdout, "spam")
+ self.assertIn("beans", stdout)
+
class RunFuncTestCase(BaseTestCase):
def run_python(self, code, **kwargs):