Fix subprocess and Update subprocess from Python 3.13.11 (#6583)

* fix test_subprocess

* fix posixsubprocess

* fix io warning

* Update subprocess from Python 3.13.11

* fix

* Auto-format: cargo fmt --all

* fix macos

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
This commit is contained in:
Jeong, YunWonandgithub-actions[bot] authored and GitHub committed 2025-12-30 12:28:47 +09:00
1 parent f274164dcd
commit 489289f54a
7 files changed
+398 -101

No files matched your search

+85 -43
View File
@@ -74,8 +74,8 @@ except ModuleNotFoundError:
else:
_mswindows = True
# wasm32-emscripten and wasm32-wasi do not support processes
_can_fork_exec = sys.platform not in {"emscripten", "wasi"}
# some platforms do not support subprocesses
_can_fork_exec = sys.platform not in {"emscripten", "wasi", "ios", "tvos", "watchos"}
if _mswindows:
import _winapi
@@ -83,6 +83,7 @@ if _mswindows:
STD_INPUT_HANDLE, STD_OUTPUT_HANDLE,
STD_ERROR_HANDLE, SW_HIDE,
STARTF_USESTDHANDLES, STARTF_USESHOWWINDOW,
STARTF_FORCEONFEEDBACK, STARTF_FORCEOFFFEEDBACK,
ABOVE_NORMAL_PRIORITY_CLASS, BELOW_NORMAL_PRIORITY_CLASS,
HIGH_PRIORITY_CLASS, IDLE_PRIORITY_CLASS,
NORMAL_PRIORITY_CLASS, REALTIME_PRIORITY_CLASS,
@@ -93,6 +94,7 @@ if _mswindows:
"STD_INPUT_HANDLE", "STD_OUTPUT_HANDLE",
"STD_ERROR_HANDLE", "SW_HIDE",
"STARTF_USESTDHANDLES", "STARTF_USESHOWWINDOW",
"STARTF_FORCEONFEEDBACK", "STARTF_FORCEOFFFEEDBACK",
"STARTUPINFO",
"ABOVE_NORMAL_PRIORITY_CLASS", "BELOW_NORMAL_PRIORITY_CLASS",
"HIGH_PRIORITY_CLASS", "IDLE_PRIORITY_CLASS",
@@ -103,18 +105,22 @@ else:
if _can_fork_exec:
from _posixsubprocess import fork_exec as _fork_exec
# used in methods that are called by __del__
_waitpid = os.waitpid
_waitstatus_to_exitcode = os.waitstatus_to_exitcode
_WIFSTOPPED = os.WIFSTOPPED
_WSTOPSIG = os.WSTOPSIG
_WNOHANG = os.WNOHANG
class _del_safe:
waitpid = os.waitpid
waitstatus_to_exitcode = os.waitstatus_to_exitcode
WIFSTOPPED = os.WIFSTOPPED
WSTOPSIG = os.WSTOPSIG
WNOHANG = os.WNOHANG
ECHILD = errno.ECHILD
else:
_fork_exec = None
_waitpid = None
_waitstatus_to_exitcode = None
_WIFSTOPPED = None
_WSTOPSIG = None
_WNOHANG = None
class _del_safe:
waitpid = None
waitstatus_to_exitcode = None
WIFSTOPPED = None
WSTOPSIG = None
WNOHANG = None
ECHILD = errno.ECHILD
import select
import selectors
@@ -346,7 +352,7 @@ def _args_from_interpreter_flags():
if dev_mode:
args.extend(('-X', 'dev'))
for opt in ('faulthandler', 'tracemalloc', 'importtime',
'frozen_modules', 'showrefcount', 'utf8'):
'frozen_modules', 'showrefcount', 'utf8', 'gil'):
if opt in xoptions:
value = xoptions[opt]
if value is True:
@@ -380,7 +386,7 @@ def _text_encoding():
def call(*popenargs, timeout=None, **kwargs):
"""Run command with arguments. Wait for command to complete or
timeout, then return the returncode attribute.
for timeout seconds, then return the returncode attribute.
The arguments are the same as for the Popen constructor. Example:
@@ -517,8 +523,8 @@ def run(*popenargs,
in the returncode attribute, and output & stderr attributes if those streams
were captured.
If timeout is given, and the process takes too long, a TimeoutExpired
exception will be raised.
If timeout (seconds) is given and the process takes too long,
a TimeoutExpired exception will be raised.
There is an optional argument "input", allowing you to
pass bytes or a string to the subprocess's stdin. If you use this argument
@@ -709,6 +715,9 @@ def _use_posix_spawn():
# os.posix_spawn() is not available
return False
if ((_env := os.environ.get('_PYTHON_SUBPROCESS_USE_POSIX_SPAWN')) in ('0', '1')):
return bool(int(_env))
if sys.platform in ('darwin', 'sunos5'):
# posix_spawn() is a syscall on both macOS and Solaris,
# and properly reports errors
@@ -744,6 +753,7 @@ def _use_posix_spawn():
# guarantee the given libc/syscall API will be used.
_USE_POSIX_SPAWN = _use_posix_spawn()
_USE_VFORK = True
_HAVE_POSIX_SPAWN_CLOSEFROM = hasattr(os, 'POSIX_SPAWN_CLOSEFROM')
class Popen:
@@ -834,6 +844,9 @@ class Popen:
if not isinstance(bufsize, int):
raise TypeError("bufsize must be an integer")
if stdout is STDOUT:
raise ValueError("STDOUT can only be used for stderr")
if pipesize is None:
pipesize = -1 # Restore default
if not isinstance(pipesize, int):
@@ -1224,8 +1237,11 @@ class Popen:
finally:
self._communication_started = True
sts = self.wait(timeout=self._remaining_time(endtime))
try:
sts = self.wait(timeout=self._remaining_time(endtime))
except TimeoutExpired as exc:
exc.timeout = timeout
raise
return (stdout, stderr)
@@ -1600,6 +1616,10 @@ class Popen:
fh.close()
def _writerthread(self, input):
self._stdin_write(input)
def _communicate(self, input, endtime, orig_timeout):
# Start reader threads feeding into a list hanging off of this
# object, unless they've already been started.
@@ -1618,8 +1638,23 @@ class Popen:
self.stderr_thread.daemon = True
self.stderr_thread.start()
if self.stdin:
self._stdin_write(input)
# Start writer thread to send input to stdin, unless already
# started. The thread writes input and closes stdin when done,
# or continues in the background on timeout.
if self.stdin and not hasattr(self, "_stdin_thread"):
self._stdin_thread = \
threading.Thread(target=self._writerthread,
args=(input,))
self._stdin_thread.daemon = True
self._stdin_thread.start()
# Wait for the writer thread, or time out. If we time out, the
# thread remains writing and the fd left open in case the user
# calls communicate again.
if hasattr(self, "_stdin_thread"):
self._stdin_thread.join(self._remaining_time(endtime))
if self._stdin_thread.is_alive():
raise TimeoutExpired(self.args, orig_timeout)
# Wait for the reader threads, or time out. If we time out, the
# threads remain reading and the fds left open in case the user
@@ -1749,14 +1784,11 @@ class Popen:
errread, errwrite)
def _posix_spawn(self, args, executable, env, restore_signals,
def _posix_spawn(self, args, executable, env, restore_signals, close_fds,
p2cread, p2cwrite,
c2pread, c2pwrite,
errread, errwrite):
"""Execute program using os.posix_spawn()."""
if env is None:
env = os.environ
kwargs = {}
if restore_signals:
# See _Py_RestoreSignals() in Python/pylifecycle.c
@@ -1778,6 +1810,10 @@ class Popen:
):
if fd != -1:
file_actions.append((os.POSIX_SPAWN_DUP2, fd, fd2))
if close_fds:
file_actions.append((os.POSIX_SPAWN_CLOSEFROM, 3))
if file_actions:
kwargs['file_actions'] = file_actions
@@ -1825,7 +1861,7 @@ class Popen:
if (_USE_POSIX_SPAWN
and os.path.dirname(executable)
and preexec_fn is None
and not close_fds
and (not close_fds or _HAVE_POSIX_SPAWN_CLOSEFROM)
and not pass_fds
and cwd is None
and (p2cread == -1 or p2cread > 2)
@@ -1837,7 +1873,7 @@ class Popen:
and gids is None
and uid is None
and umask < 0):
self._posix_spawn(args, executable, env, restore_signals,
self._posix_spawn(args, executable, env, restore_signals, close_fds,
p2cread, p2cwrite,
c2pread, c2pwrite,
errread, errwrite)
@@ -1958,20 +1994,16 @@ class Popen:
raise child_exception_type(err_msg)
def _handle_exitstatus(self, sts,
_waitstatus_to_exitcode=_waitstatus_to_exitcode,
_WIFSTOPPED=_WIFSTOPPED,
_WSTOPSIG=_WSTOPSIG):
def _handle_exitstatus(self, sts, _del_safe=_del_safe):
"""All callers to this function MUST hold self._waitpid_lock."""
# This method is called (indirectly) by __del__, so it cannot
# refer to anything outside of its local scope.
if _WIFSTOPPED(sts):
self.returncode = -_WSTOPSIG(sts)
if _del_safe.WIFSTOPPED(sts):
self.returncode = -_del_safe.WSTOPSIG(sts)
else:
self.returncode = _waitstatus_to_exitcode(sts)
self.returncode = _del_safe.waitstatus_to_exitcode(sts)
def _internal_poll(self, _deadstate=None, _waitpid=_waitpid,
_WNOHANG=_WNOHANG, _ECHILD=errno.ECHILD):
def _internal_poll(self, _deadstate=None, _del_safe=_del_safe):
"""Check if child process has terminated. Returns returncode
attribute.
@@ -1987,13 +2019,13 @@ class Popen:
try:
if self.returncode is not None:
return self.returncode # Another thread waited.
pid, sts = _waitpid(self.pid, _WNOHANG)
pid, sts = _del_safe.waitpid(self.pid, _del_safe.WNOHANG)
if pid == self.pid:
self._handle_exitstatus(sts)
except OSError as e:
if _deadstate is not None:
self.returncode = _deadstate
elif e.errno == _ECHILD:
elif e.errno == _del_safe.ECHILD:
# This happens if SIGCLD is set to be ignored or
# waiting for child processes has otherwise been
# disabled for our process. This child is dead, we
@@ -2067,6 +2099,10 @@ class Popen:
self.stdin.flush()
except BrokenPipeError:
pass # communicate() must ignore BrokenPipeError.
except ValueError:
# ignore ValueError: I/O operation on closed file.
if not self.stdin.closed:
raise
if not input:
try:
self.stdin.close()
@@ -2092,10 +2128,13 @@ class Popen:
self._save_input(input)
if self._input:
input_view = memoryview(self._input)
if not isinstance(self._input, memoryview):
input_view = memoryview(self._input)
else:
input_view = self._input.cast("b") # byte input required
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)
@@ -2128,7 +2167,7 @@ class Popen:
selector.unregister(key.fileobj)
key.fileobj.close()
else:
if self._input_offset >= len(self._input):
if self._input_offset >= len(input_view):
selector.unregister(key.fileobj)
key.fileobj.close()
elif key.fileobj in (self.stdout, self.stderr):
@@ -2137,8 +2176,11 @@ class Popen:
selector.unregister(key.fileobj)
key.fileobj.close()
self._fileobj2output[key.fileobj].append(data)
self.wait(timeout=self._remaining_time(endtime))
try:
self.wait(timeout=self._remaining_time(endtime))
except TimeoutExpired as exc:
exc.timeout = orig_timeout
raise
# All data exchanged. Translate lists into strings.
if stdout is not None:
-3
View File
@@ -344,9 +344,6 @@ class OtherFileTests:
class COtherFileTests(OtherFileTests, unittest.TestCase):
open = io.open
@unittest.expectedFailure # TODO: RUSTPYTHON
def testSetBufferSize(self):
return super().testSetBufferSize()
class PyOtherFileTests(OtherFileTests, unittest.TestCase):
open = staticmethod(pyio.open)
+279 -50
View File
@@ -25,7 +25,6 @@ import threading
import gc
import textwrap
import json
import pathlib
from test.support.os_helper import FakePath
try:
@@ -41,6 +40,10 @@ try:
import grp
except ImportError:
grp = None
try:
import resource
except ImportError:
resource = None
try:
import fcntl
@@ -158,6 +161,20 @@ class ProcessTestCase(BaseTestCase):
[sys.executable, "-c", "while True: pass"],
timeout=0.1)
def test_timeout_exception(self):
try:
subprocess.run([sys.executable, '-c', 'import time;time.sleep(9)'], timeout = -1)
except subprocess.TimeoutExpired as e:
self.assertIn("-1 seconds", str(e))
else:
self.fail("Expected TimeoutExpired exception not raised")
try:
subprocess.run([sys.executable, '-c', 'import time;time.sleep(9)'], timeout = 0)
except subprocess.TimeoutExpired as e:
self.assertIn("0 seconds", str(e))
else:
self.fail("Expected TimeoutExpired exception not raised")
def test_check_call_zero(self):
# check_call() function with zero return code
rc = subprocess.check_call(ZERO_RETURN_CMD)
@@ -269,21 +286,13 @@ class ProcessTestCase(BaseTestCase):
self.assertIn('stdin', c.exception.args[0])
self.assertIn('input', c.exception.args[0])
@support.requires_resource('walltime')
def test_check_output_timeout(self):
# check_output() function with timeout arg
with self.assertRaises(subprocess.TimeoutExpired) as c:
output = subprocess.check_output(
[sys.executable, "-c",
"import sys, time\n"
"sys.stdout.write('BDFL')\n"
"sys.stdout.flush()\n"
"time.sleep(3600)"],
# Some heavily loaded buildbots (sparc Debian 3.x) require
# this much time to start and print.
timeout=3)
self.fail("Expected TimeoutExpired.")
self.assertEqual(c.exception.output, b'BDFL')
"import time; time.sleep(3600)"],
timeout=0.1)
def test_call_kwargs(self):
# call() function with keyword args
@@ -949,6 +958,48 @@ class ProcessTestCase(BaseTestCase):
self.assertEqual(stdout, b"banana")
self.assertEqual(stderr, b"pineapple")
def test_communicate_memoryview_input(self):
# Test memoryview input with byte elements
test_data = b"Hello, memoryview!"
mv = memoryview(test_data)
p = subprocess.Popen([sys.executable, "-c",
'import sys; sys.stdout.write(sys.stdin.read())'],
stdin=subprocess.PIPE,
stdout=subprocess.PIPE)
self.addCleanup(p.stdout.close)
self.addCleanup(p.stdin.close)
(stdout, stderr) = p.communicate(mv)
self.assertEqual(stdout, test_data)
self.assertIsNone(stderr)
def test_communicate_memoryview_input_nonbyte(self):
# Test memoryview input with non-byte elements (e.g., int32)
# This tests the fix for gh-134453 where non-byte memoryviews
# had incorrect length tracking on POSIX
import array
# Create an array of 32-bit integers that's large enough to trigger
# the chunked writing behavior (> PIPE_BUF)
pipe_buf = getattr(select, 'PIPE_BUF', 512)
# Each 'i' element is 4 bytes, so we need more than pipe_buf/4 elements
# Add some extra to ensure we exceed the buffer size
num_elements = pipe_buf + 1
test_array = array.array('i', [0x64306f66 for _ in range(num_elements)])
expected_bytes = test_array.tobytes()
mv = memoryview(test_array)
p = subprocess.Popen([sys.executable, "-c",
'import sys; '
'data = sys.stdin.buffer.read(); '
'sys.stdout.buffer.write(data)'],
stdin=subprocess.PIPE,
stdout=subprocess.PIPE)
self.addCleanup(p.stdout.close)
self.addCleanup(p.stdin.close)
(stdout, stderr) = p.communicate(mv)
self.assertEqual(stdout, expected_bytes,
msg=f"{len(stdout)=} =? {len(expected_bytes)=}")
self.assertIsNone(stderr)
def test_communicate_timeout(self):
p = subprocess.Popen([sys.executable, "-c",
'import sys,os,time;'
@@ -984,6 +1035,62 @@ class ProcessTestCase(BaseTestCase):
(stdout, _) = p.communicate()
self.assertEqual(len(stdout), 4 * 64 * 1024)
def test_communicate_timeout_large_input(self):
# Test that timeout is enforced when writing large input to a
# slow-to-read subprocess, and that partial input is preserved
# for continuation after timeout (gh-141473).
#
# This is a regression test for Windows matching POSIX behavior.
# On POSIX, select() is used to multiplex I/O with timeout checking.
# On Windows, stdin writing must also honor the timeout rather than
# blocking indefinitely when the pipe buffer fills.
# Input larger than typical pipe buffer (4-64KB on Windows)
input_data = b"x" * (128 * 1024)
p = subprocess.Popen(
[sys.executable, "-c",
"import sys, time; "
"time.sleep(30); " # Don't read stdin for a long time
"sys.stdout.buffer.write(sys.stdin.buffer.read())"],
stdin=subprocess.PIPE,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE)
try:
timeout = 0.2
start = time.monotonic()
try:
p.communicate(input_data, timeout=timeout)
# If we get here without TimeoutExpired, the timeout was ignored
elapsed = time.monotonic() - start
self.fail(
f"TimeoutExpired not raised. communicate() completed in "
f"{elapsed:.2f}s, but subprocess sleeps for 30s. "
"Stdin writing blocked without enforcing timeout.")
except subprocess.TimeoutExpired:
elapsed = time.monotonic() - start
# Timeout should occur close to the specified timeout value,
# not after waiting for the subprocess to finish sleeping.
# Allow generous margin for slow CI, but must be well under
# the subprocess sleep time.
self.assertLess(elapsed, 5.0,
f"TimeoutExpired raised after {elapsed:.2f}s; expected ~{timeout}s. "
"Stdin writing blocked without checking timeout.")
# After timeout, continue communication. The remaining input
# should be sent and we should receive all data back.
stdout, stderr = p.communicate()
# Verify all input was eventually received by the subprocess
self.assertEqual(len(stdout), len(input_data),
f"Expected {len(input_data)} bytes output but got {len(stdout)}")
self.assertEqual(stdout, input_data)
finally:
p.kill()
p.wait()
# Test for the fd leak reported in http://bugs.python.org/issue2791.
def test_communicate_pipe_fd_leak(self):
for stdin_pipe in (False, True):
@@ -1054,6 +1161,19 @@ class ProcessTestCase(BaseTestCase):
self.assertEqual(stdout, b"bananasplit")
self.assertEqual(stderr, b"")
def test_communicate_stdin_closed_before_call(self):
# gh-70560, gh-74389: stdin.close() before communicate()
# should not raise ValueError from stdin.flush()
with subprocess.Popen([sys.executable, "-c",
'import sys; sys.exit(0)'],
stdin=subprocess.PIPE,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE) as p:
p.stdin.close() # Close stdin before communicate
# This should not raise ValueError
(stdout, stderr) = p.communicate()
self.assertEqual(p.returncode, 0)
def test_universal_newlines_and_text(self):
args = [
sys.executable, "-c",
@@ -1223,6 +1343,16 @@ class ProcessTestCase(BaseTestCase):
max_handles = 1026 # too much for most UNIX systems
else:
max_handles = 2050 # too much for (at least some) Windows setups
if resource:
# And if it is not too much, try to make it too much.
try:
soft, hard = resource.getrlimit(resource.RLIMIT_NOFILE)
if soft > 1024:
resource.setrlimit(resource.RLIMIT_NOFILE, (1024, hard))
self.addCleanup(resource.setrlimit, resource.RLIMIT_NOFILE,
(soft, hard))
except (OSError, ValueError):
pass
handles = []
tmpdir = tempfile.mkdtemp()
try:
@@ -1237,7 +1367,9 @@ class ProcessTestCase(BaseTestCase):
else:
self.skipTest("failed to reach the file descriptor limit "
"(tried %d)" % max_handles)
# Close a couple of them (should be enough for a subprocess)
# Close a couple of them (should be enough for a subprocess).
# Close lower file descriptors, so select() will work.
handles.reverse()
for i in range(10):
os.close(handles.pop())
# Loop creating some subprocesses. If one of them leaks some fds,
@@ -1341,8 +1473,6 @@ class ProcessTestCase(BaseTestCase):
line = "line\n"
self._test_bufsize_equal_one(line, line, universal_newlines=True)
# TODO: RUSTPYTHON
@unittest.expectedFailure
def test_bufsize_equal_one_binary_mode(self):
# line is not flushed in binary mode with bufsize=1.
# we should get empty response
@@ -1414,7 +1544,7 @@ class ProcessTestCase(BaseTestCase):
t = threading.Thread(target=open_fds)
t.start()
try:
with self.assertRaises(EnvironmentError):
with self.assertRaises(OSError):
subprocess.Popen(NONEXISTING_CMD,
stdin=subprocess.PIPE,
stdout=subprocess.PIPE,
@@ -1528,9 +1658,6 @@ class ProcessTestCase(BaseTestCase):
p.communicate(b"x" * 2**20)
def test_repr(self):
path_cmd = pathlib.Path("my-tool.py")
pathlib_cls = path_cmd.__class__.__name__
cases = [
("ls", True, 123, "<Popen: returncode: 123 args: 'ls'>"),
('a' * 100, True, 0,
@@ -1538,7 +1665,8 @@ class ProcessTestCase(BaseTestCase):
(["ls"], False, None, "<Popen: returncode: None args: ['ls']>"),
(["ls", '--my-opts', 'a' * 100], False, None,
"<Popen: returncode: None args: ['ls', '--my-opts', 'aaaaaaaaaaaaaaaaaaaaaaaa...>"),
(path_cmd, False, 7, f"<Popen: returncode: 7 args: {pathlib_cls}('my-tool.py')>")
(os_helper.FakePath("my-tool.py"), False, 7,
"<Popen: returncode: 7 args: <FakePath 'my-tool.py'>>")
]
with unittest.mock.patch.object(subprocess.Popen, '_execute_child'):
for cmd, shell, code, sx in cases:
@@ -1613,21 +1741,6 @@ class ProcessTestCase(BaseTestCase):
self.assertIsInstance(subprocess.Popen[bytes], types.GenericAlias)
self.assertIsInstance(subprocess.CompletedProcess[str], types.GenericAlias)
@unittest.skipIf(not sysconfig.get_config_var("HAVE_VFORK"),
"vfork() not enabled by configure.")
@mock.patch("subprocess._fork_exec")
def test__use_vfork(self, mock_fork_exec):
self.assertTrue(subprocess._USE_VFORK) # The default value regardless.
mock_fork_exec.side_effect = RuntimeError("just testing args")
with self.assertRaises(RuntimeError):
subprocess.run([sys.executable, "-c", "pass"])
mock_fork_exec.assert_called_once()
self.assertTrue(mock_fork_exec.call_args.args[-1])
with mock.patch.object(subprocess, '_USE_VFORK', False):
with self.assertRaises(RuntimeError):
subprocess.run([sys.executable, "-c", "pass"])
self.assertFalse(mock_fork_exec.call_args_list[-1].args[-1])
@unittest.skipUnless(hasattr(subprocess, '_winapi'),
'need subprocess._winapi')
def test_wait_negative_timeout(self):
@@ -1644,6 +1757,40 @@ class ProcessTestCase(BaseTestCase):
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.assertTrue(stdout.startswith("spam"), msg=stdout)
self.assertIn("beans", stdout)
class RunFuncTestCase(BaseTestCase):
def run_python(self, code, **kwargs):
@@ -1717,20 +1864,11 @@ class RunFuncTestCase(BaseTestCase):
self.assertIn('stdin', c.exception.args[0])
self.assertIn('input', c.exception.args[0])
@support.requires_resource('walltime')
def test_check_output_timeout(self):
with self.assertRaises(subprocess.TimeoutExpired) as c:
cp = self.run_python((
"import sys, time\n"
"sys.stdout.write('BDFL')\n"
"sys.stdout.flush()\n"
"time.sleep(3600)"),
# Some heavily loaded buildbots (sparc Debian 3.x) require
# this much time to start and print.
timeout=3, stdout=subprocess.PIPE)
self.assertEqual(c.exception.output, b'BDFL')
# output is aliased to stdout
self.assertEqual(c.exception.stdout, b'BDFL')
cp = self.run_python(
"import time; time.sleep(3600)",
timeout=0.1, stdout=subprocess.PIPE)
def test_run_kwargs(self):
newenv = os.environ.copy()
@@ -1785,6 +1923,13 @@ class RunFuncTestCase(BaseTestCase):
self.assertIn(b'BDFL', cp.stdout)
self.assertIn(b'FLUFL', cp.stderr)
def test_stdout_stdout(self):
# run() refuses to accept stdout=STDOUT
with self.assertRaises(ValueError,
msg=("STDOUT can only be used for stderr")):
self.run_python("print('will not be run')",
stdout=subprocess.STDOUT)
def test_stdout_with_capture_output_arg(self):
# run() refuses to accept 'stdout' with 'capture_output'
tf = tempfile.TemporaryFile()
@@ -1969,8 +2114,6 @@ class POSIXProcessTestCase(BaseTestCase):
self.assertIn(repr(error_data), str(e.exception))
# TODO: RUSTPYTHON
@unittest.expectedFailure
@unittest.skipIf(not os.path.exists('/proc/self/status'),
"need /proc/self/status")
def test_restore_signals(self):
@@ -2189,8 +2332,6 @@ class POSIXProcessTestCase(BaseTestCase):
cwd=os.curdir, env=os.environ,
extra_groups=[2**64])
# TODO: RUSTPYTHON
@unittest.expectedFailure
@unittest.skipIf(mswindows or not hasattr(os, 'umask'),
'POSIX umask() is not available.')
def test_umask(self):
@@ -3446,6 +3587,94 @@ class POSIXProcessTestCase(BaseTestCase):
self.assertEqual(out.strip(), b"OK")
self.assertIn(b"preexec_fn not supported at interpreter shutdown", err)
@unittest.skipIf(not sysconfig.get_config_var("HAVE_VFORK"),
"vfork() not enabled by configure.")
@mock.patch("subprocess._fork_exec")
@mock.patch("subprocess._USE_POSIX_SPAWN", new=False)
def test__use_vfork(self, mock_fork_exec):
self.assertTrue(subprocess._USE_VFORK) # The default value regardless.
mock_fork_exec.side_effect = RuntimeError("just testing args")
with self.assertRaises(RuntimeError):
subprocess.run([sys.executable, "-c", "pass"])
mock_fork_exec.assert_called_once()
# NOTE: These assertions are *ugly* as they require the last arg
# to remain the have_vfork boolean. We really need to refactor away
# from the giant "wall of args" internal C extension API.
self.assertTrue(mock_fork_exec.call_args.args[-1])
with mock.patch.object(subprocess, '_USE_VFORK', False):
with self.assertRaises(RuntimeError):
subprocess.run([sys.executable, "-c", "pass"])
self.assertFalse(mock_fork_exec.call_args_list[-1].args[-1])
@unittest.skipIf(not sysconfig.get_config_var("HAVE_VFORK"),
"vfork() not enabled by configure.")
@unittest.skipIf(sys.platform != "linux", "Linux only, requires strace.")
@mock.patch("subprocess._USE_POSIX_SPAWN", new=False)
def test_vfork_used_when_expected(self):
# This is a performance regression test to ensure we default to using
# vfork() when possible.
# Technically this test could pass when posix_spawn is used as well
# because libc tends to implement that internally using vfork. But
# that'd just be testing a libc+kernel implementation detail.
strace_binary = "/usr/bin/strace"
# The only system calls we are interested in.
strace_filter = "--trace=clone,clone2,clone3,fork,vfork,exit,exit_group"
true_binary = "/bin/true"
strace_command = [strace_binary, strace_filter]
try:
does_strace_work_process = subprocess.run(
strace_command + [true_binary],
stderr=subprocess.PIPE,
stdout=subprocess.DEVNULL,
)
rc = does_strace_work_process.returncode
stderr = does_strace_work_process.stderr
except OSError:
rc = -1
stderr = ""
if rc or (b"+++ exited with 0 +++" not in stderr):
self.skipTest("strace not found or not working as expected.")
with self.subTest(name="default_is_vfork"):
vfork_result = assert_python_ok(
"-c",
textwrap.dedent(f"""\
import subprocess
subprocess.check_call([{true_binary!r}])"""),
__run_using_command=strace_command,
)
# Match both vfork() and clone(..., flags=...|CLONE_VFORK|...)
self.assertRegex(vfork_result.err, br"(?i)vfork")
# Do NOT check that fork() or other clones did not happen.
# If the OS denys the vfork it'll fallback to plain fork().
# Test that each individual thing that would disable the use of vfork
# actually disables it.
for sub_name, preamble, sp_kwarg, expect_permission_error in (
("!use_vfork", "subprocess._USE_VFORK = False", "", False),
("preexec", "", "preexec_fn=lambda: None", False),
("setgid", "", f"group={os.getgid()}", True),
("setuid", "", f"user={os.getuid()}", True),
("setgroups", "", "extra_groups=[]", True),
):
with self.subTest(name=sub_name):
non_vfork_result = assert_python_ok(
"-c",
textwrap.dedent(f"""\
import subprocess
{preamble}
try:
subprocess.check_call(
[{true_binary!r}], **dict({sp_kwarg}))
except PermissionError:
if not {expect_permission_error}:
raise"""),
__run_using_command=strace_command,
)
# Ensure neither vfork() or clone(..., flags=...|CLONE_VFORK|...).
self.assertNotRegex(non_vfork_result.err, br"(?i)vfork")
@unittest.skipUnless(mswindows, "Windows specific tests")
class Win32ProcessTestCase(BaseTestCase):
+5 -2
View File
@@ -321,11 +321,14 @@ fn exec_inner(
}
if args.child_umask >= 0 {
// TODO: umask(child_umask);
unsafe { libc::umask(args.child_umask as libc::mode_t) };
}
if args.restore_signals {
// TODO: restore signals SIGPIPE, SIGXFZ, SIGXFSZ to SIG_DFL
unsafe {
libc::signal(libc::SIGPIPE, libc::SIG_DFL);
libc::signal(libc::SIGXFSZ, libc::SIG_DFL);
}
}
if args.call_setsid {
+10
View File
@@ -4489,6 +4489,16 @@ mod _io {
bool::try_from_object(vm, atty)?
};
// Warn if line buffering is requested in binary mode
if opts.buffering == 1 && matches!(mode.encode, EncodeMode::Bytes) {
crate::stdlib::warnings::warn(
vm.ctx.exceptions.runtime_warning,
"line buffering (buffering=1) isn't supported in binary mode, the default buffer size will be used".to_owned(),
1,
vm,
)?;
}
let line_buffering = opts.buffering == 1 || isatty;
let buffering = if opts.buffering < 0 || opts.buffering == 1 {
+17 -2
View File
@@ -1515,7 +1515,7 @@ pub mod module {
#[pyarg(positional)]
args: crate::function::ArgIterable<OsPath>,
#[pyarg(positional)]
env: crate::function::ArgMapping,
env: Option<crate::function::ArgMapping>,
#[pyarg(named, default)]
file_actions: Option<crate::function::ArgIterable<PyTupleRef>>,
#[pyarg(named, default)]
@@ -1678,7 +1678,22 @@ pub mod module {
.map_err(|_| vm.new_value_error("path should not have nul bytes"))
})
.collect::<Result<_, _>>()?;
let env = envp_from_dict(self.env, vm)?;
let env = if let Some(env_dict) = self.env {
envp_from_dict(env_dict, vm)?
} else {
// env=None means use the current environment
use rustpython_common::os::ffi::OsStringExt;
env::vars_os()
.map(|(k, v)| {
let mut entry = k.into_vec();
entry.push(b'=');
entry.extend(v.into_vec());
CString::new(entry).map_err(|_| {
vm.new_value_error("environment string contains null byte")
})
})
.collect::<PyResult<Vec<_>>>()?
};
let ret = if spawnp {
nix::spawn::posix_spawnp(&path, &file_actions, &attrp, &args, &env)
+2 -1
View File
@@ -63,7 +63,8 @@ mod _winapi {
CREATE_BREAKAWAY_FROM_JOB, CREATE_DEFAULT_ERROR_MODE, CREATE_NEW_CONSOLE,
CREATE_NEW_PROCESS_GROUP, CREATE_NO_WINDOW, DETACHED_PROCESS, HIGH_PRIORITY_CLASS,
IDLE_PRIORITY_CLASS, INFINITE, NORMAL_PRIORITY_CLASS, PROCESS_DUP_HANDLE,
REALTIME_PRIORITY_CLASS, STARTF_USESHOWWINDOW, STARTF_USESTDHANDLES,
REALTIME_PRIORITY_CLASS, STARTF_FORCEOFFFEEDBACK, STARTF_FORCEONFEEDBACK,
STARTF_USESHOWWINDOW, STARTF_USESTDHANDLES,
},
},
UI::WindowsAndMessaging::SW_HIDE,