Skip to content

Commit d4c0c61

Browse files
authored
Merge branch '3.13' into backport-5b1862b-3.13
2 parents 63c23ad + 2f3024f commit d4c0c61

File tree

5 files changed

+93
-4
lines changed

5 files changed

+93
-4
lines changed

Doc/library/subprocess.rst

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -831,7 +831,9 @@ Instances of the :class:`Popen` class have the following methods:
831831

832832
If the process does not terminate after *timeout* seconds, a
833833
:exc:`TimeoutExpired` exception will be raised. Catching this exception and
834-
retrying communication will not lose any output.
834+
retrying communication will not lose any output. Supplying *input* to a
835+
subsequent post-timeout :meth:`communicate` call is in undefined behavior
836+
and may become an error in the future.
835837

836838
The child process is not killed if the timeout expires, so in order to
837839
cleanup properly a well-behaved application should kill the child process and

Lib/subprocess.py

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2124,10 +2124,13 @@ def _communicate(self, input, endtime, orig_timeout):
21242124
self._save_input(input)
21252125

21262126
if self._input:
2127-
input_view = memoryview(self._input)
2127+
if not isinstance(self._input, memoryview):
2128+
input_view = memoryview(self._input)
2129+
else:
2130+
input_view = self._input.cast("b") # byte input required
21282131

21292132
with _PopenSelector() as selector:
2130-
if self.stdin and input:
2133+
if self.stdin and not self.stdin.closed and self._input:
21312134
selector.register(self.stdin, selectors.EVENT_WRITE)
21322135
if self.stdout and not self.stdout.closed:
21332136
selector.register(self.stdout, selectors.EVENT_READ)
@@ -2160,7 +2163,7 @@ def _communicate(self, input, endtime, orig_timeout):
21602163
selector.unregister(key.fileobj)
21612164
key.fileobj.close()
21622165
else:
2163-
if self._input_offset >= len(self._input):
2166+
if self._input_offset >= len(input_view):
21642167
selector.unregister(key.fileobj)
21652168
key.fileobj.close()
21662169
elif key.fileobj in (self.stdout, self.stderr):

Lib/test/test_subprocess.py

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -956,6 +956,48 @@ def test_communicate(self):
956956
self.assertEqual(stdout, b"banana")
957957
self.assertEqual(stderr, b"pineapple")
958958

959+
def test_communicate_memoryview_input(self):
960+
# Test memoryview input with byte elements
961+
test_data = b"Hello, memoryview!"
962+
mv = memoryview(test_data)
963+
p = subprocess.Popen([sys.executable, "-c",
964+
'import sys; sys.stdout.write(sys.stdin.read())'],
965+
stdin=subprocess.PIPE,
966+
stdout=subprocess.PIPE)
967+
self.addCleanup(p.stdout.close)
968+
self.addCleanup(p.stdin.close)
969+
(stdout, stderr) = p.communicate(mv)
970+
self.assertEqual(stdout, test_data)
971+
self.assertIsNone(stderr)
972+
973+
def test_communicate_memoryview_input_nonbyte(self):
974+
# Test memoryview input with non-byte elements (e.g., int32)
975+
# This tests the fix for gh-134453 where non-byte memoryviews
976+
# had incorrect length tracking on POSIX
977+
import array
978+
# Create an array of 32-bit integers that's large enough to trigger
979+
# the chunked writing behavior (> PIPE_BUF)
980+
pipe_buf = getattr(select, 'PIPE_BUF', 512)
981+
# Each 'i' element is 4 bytes, so we need more than pipe_buf/4 elements
982+
# Add some extra to ensure we exceed the buffer size
983+
num_elements = pipe_buf + 1
984+
test_array = array.array('i', [0x64306f66 for _ in range(num_elements)])
985+
expected_bytes = test_array.tobytes()
986+
mv = memoryview(test_array)
987+
988+
p = subprocess.Popen([sys.executable, "-c",
989+
'import sys; '
990+
'data = sys.stdin.buffer.read(); '
991+
'sys.stdout.buffer.write(data)'],
992+
stdin=subprocess.PIPE,
993+
stdout=subprocess.PIPE)
994+
self.addCleanup(p.stdout.close)
995+
self.addCleanup(p.stdin.close)
996+
(stdout, stderr) = p.communicate(mv)
997+
self.assertEqual(stdout, expected_bytes,
998+
msg=f"{len(stdout)=} =? {len(expected_bytes)=}")
999+
self.assertIsNone(stderr)
1000+
9591001
def test_communicate_timeout(self):
9601002
p = subprocess.Popen([sys.executable, "-c",
9611003
'import sys,os,time;'
@@ -1698,6 +1740,40 @@ def test_wait_negative_timeout(self):
16981740

16991741
self.assertEqual(proc.wait(), 0)
17001742

1743+
def test_post_timeout_communicate_sends_input(self):
1744+
"""GH-141473 regression test; the stdin pipe must close"""
1745+
with subprocess.Popen(
1746+
[sys.executable, "-uc", """\
1747+
import sys
1748+
while c := sys.stdin.read(512):
1749+
sys.stdout.write(c)
1750+
print()
1751+
"""],
1752+
stdin=subprocess.PIPE,
1753+
stdout=subprocess.PIPE,
1754+
stderr=subprocess.PIPE,
1755+
text=True,
1756+
) as proc:
1757+
try:
1758+
data = f"spam{'#'*4096}beans"
1759+
proc.communicate(
1760+
input=data,
1761+
timeout=0,
1762+
)
1763+
except subprocess.TimeoutExpired as exc:
1764+
pass
1765+
# Prior to the bugfix, this would hang as the stdin
1766+
# pipe to the child had not been closed.
1767+
try:
1768+
stdout, stderr = proc.communicate(timeout=15)
1769+
except subprocess.TimeoutExpired as exc:
1770+
self.fail("communicate() hung waiting on child process that should have seen its stdin pipe close and exit")
1771+
self.assertEqual(
1772+
proc.returncode, 0,
1773+
msg=f"STDERR:\n{stderr}\nSTDOUT:\n{stdout}")
1774+
self.assertTrue(stdout.startswith("spam"), msg=stdout)
1775+
self.assertIn("beans", stdout)
1776+
17011777

17021778
class RunFuncTestCase(BaseTestCase):
17031779
def run_python(self, code, **kwargs):
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
Fixed :func:`subprocess.Popen.communicate` ``input=`` handling of :class:`memoryview`
2+
instances that were non-byte shaped on POSIX platforms. Those are now properly
3+
cast to a byte shaped view instead of truncating the input. Windows platforms
4+
did not have this bug.
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
When :meth:`subprocess.Popen.communicate` was called with *input* and a
2+
*timeout* and is called for a second time after a
3+
:exc:`~subprocess.TimeoutExpired` exception before the process has died, it
4+
should no longer hang.

0 commit comments

Comments
 (0)