diff --git a/Doc/library/subprocess.rst b/Doc/library/subprocess.rst --- a/Doc/library/subprocess.rst +++ b/Doc/library/subprocess.rst @@ -444,12 +444,18 @@ untrusted input. See the warning under :ref:`frequently-used-arguments` for details. - *bufsize* will be supplied as the corresponding argument to the :func:`open` - function when creating the stdin/stdout/stderr pipe file objects: :const:`0` - means unbuffered (read and write are one system call and can return short), - :const:`1` means line buffered, any other positive value means use a buffer - of approximately that size. A negative bufsize (the default) means the - system default of io.DEFAULT_BUFFER_SIZE will be used. + *bufsize* will be supplied as the corresponding argument to the + :func:`open` function when creating the stdin/stdout/stderr pipe + file objects: + + - :const:`0` means unbuffered (read and write are one + system call and can return short) + - :const:`1` means line buffered + (only usable if ``universal_newlines=True`` i.e., in a text mode) + - any other positive value means use a buffer of approximately that + size + - negative bufsize (the default) means the system default of + io.DEFAULT_BUFFER_SIZE will be used. .. versionchanged:: 3.3.1 *bufsize* now defaults to -1 to enable buffering by default to match the diff --git a/Lib/subprocess.py b/Lib/subprocess.py --- a/Lib/subprocess.py +++ b/Lib/subprocess.py @@ -848,7 +848,8 @@ if p2cwrite != -1: self.stdin = io.open(p2cwrite, 'wb', bufsize) if universal_newlines: - self.stdin = io.TextIOWrapper(self.stdin, write_through=True) + self.stdin = io.TextIOWrapper(self.stdin, write_through=True, + line_buffering=(bufsize == 1)) if c2pread != -1: self.stdout = io.open(c2pread, 'rb', bufsize) if universal_newlines: diff --git a/Lib/test/test_subprocess.py b/Lib/test/test_subprocess.py --- a/Lib/test/test_subprocess.py +++ b/Lib/test/test_subprocess.py @@ -1007,6 +1007,53 @@ p = subprocess.Popen([sys.executable, "-c", "pass"], bufsize=None) self.assertEqual(p.wait(), 0) + def _test_bufsize_equal_one(self, universal_newlines): + # subprocess may deadlock with bufsize=1, see issue #21332 + + kill_called = False + def kill(process): + nonlocal kill_called + kill_called = True + try: + process.kill() + except OSError: # ignore + pass + + with subprocess.Popen([sys.executable, "-c", "import sys;" + "sys.stdout.write(sys.stdin.readline());" + "sys.stdout.flush()"], + stdin=subprocess.PIPE, + stdout=subprocess.PIPE, + stderr=subprocess.DEVNULL, + bufsize=1, + universal_newlines=universal_newlines) as p: + if universal_newlines: + line = "line\n" + else: + line = b'line' + os.linesep.encode() # assume ascii-based locale + p.stdin.write(line) # deadlock happens if it is not flushed + if threading: # avoid deadlock if we can help it + t = threading.Timer(10, kill, [p]) + t.start() + else: + t = None + self.assertEqual(p.stdout.readline(), line) + if t: + t.cancel() + self.assertEqual(p.returncode, 0) + # either 10 seconds is not enough to write/read a line or + # the deadlock happened + self.assertFalse(kill_called) + + def test_bufsize_equal_one_text_mode(self): + self._test_bufsize_equal_one(universal_newlines=True) + + # don't run without threading, otherwise it deadlocks + @unittest.skipIf(threading is None, "threading required") + def test_bufsize_equal_one_binary_mode(self): + with self.assertRaises((AssertionError, OSError)): + self._test_bufsize_equal_one(universal_newlines=False) + def test_leaking_fds_on_error(self): # see bug #5179: Popen leaks file descriptors to PIPEs if # the child fails to execute; this will eventually exhaust