| import unittest | |
| from unittest import mock | |
| from test import support | |
| from test.support import check_sanitizer | |
| from test.support import import_helper | |
| from test.support import os_helper | |
| from test.support import strace_helper | |
| from test.support import warnings_helper | |
| from test.support.script_helper import assert_python_ok | |
| import subprocess | |
| import sys | |
| import signal | |
| import io | |
| import itertools | |
| import os | |
| import errno | |
| import tempfile | |
| import time | |
| import traceback | |
| import types | |
| import selectors | |
| import sysconfig | |
| import select | |
| import shutil | |
| import socket | |
| import threading | |
| import gc | |
| import textwrap | |
| import json | |
| from test.support.os_helper import FakePath | |
| try: | |
| import _testcapi | |
| except ImportError: | |
| _testcapi = None | |
| try: | |
| import pwd | |
| except ImportError: | |
| pwd = None | |
| try: | |
| import grp | |
| except ImportError: | |
| grp = None | |
| try: | |
| import resource | |
| except ImportError: | |
| resource = None | |
| try: | |
| import fcntl | |
| except: | |
| fcntl = None | |
| if support.PGO: | |
| raise unittest.SkipTest("test is not helpful for PGO") | |
| if not support.has_subprocess_support: | |
| raise unittest.SkipTest("test module requires subprocess") | |
| mswindows = (sys.platform == "win32") | |
| # | |
| # Depends on the following external programs: Python | |
| # | |
| if mswindows: | |
| SETBINARY = ('import msvcrt; msvcrt.setmode(sys.stdout.fileno(), ' | |
| 'os.O_BINARY);') | |
| else: | |
| SETBINARY = '' | |
| NONEXISTING_CMD = ('nonexisting_i_hope',) | |
| # Ignore errors that indicate the command was not found | |
| NONEXISTING_ERRORS = (FileNotFoundError, NotADirectoryError, PermissionError) | |
| ZERO_RETURN_CMD = (sys.executable, '-c', 'pass') | |
| def setUpModule(): | |
| shell_true = shutil.which('true') | |
| if shell_true is None: | |
| return | |
| if (os.access(shell_true, os.X_OK) and | |
| subprocess.run([shell_true]).returncode == 0): | |
| global ZERO_RETURN_CMD | |
| ZERO_RETURN_CMD = (shell_true,) # Faster than Python startup. | |
| class BaseTestCase(unittest.TestCase): | |
| def setUp(self): | |
| # Try to minimize the number of children we have so this test | |
| # doesn't crash on some buildbots (Alphas in particular). | |
| support.reap_children() | |
| def tearDown(self): | |
| if not mswindows: | |
| # subprocess._active is not used on Windows and is set to None. | |
| for inst in subprocess._active: | |
| inst.wait() | |
| subprocess._cleanup() | |
| self.assertFalse( | |
| subprocess._active, "subprocess._active not empty" | |
| ) | |
| self.doCleanups() | |
| support.reap_children() | |
| class PopenTestException(Exception): | |
| pass | |
| class PopenExecuteChildRaises(subprocess.Popen): | |
| """Popen subclass for testing cleanup of subprocess.PIPE filehandles when | |
| _execute_child fails. | |
| """ | |
| def _execute_child(self, *args, **kwargs): | |
| raise PopenTestException("Forced Exception for Test") | |
| class ProcessTestCase(BaseTestCase): | |
| def test_io_buffered_by_default(self): | |
| p = subprocess.Popen(ZERO_RETURN_CMD, | |
| stdin=subprocess.PIPE, stdout=subprocess.PIPE, | |
| stderr=subprocess.PIPE) | |
| try: | |
| self.assertIsInstance(p.stdin, io.BufferedIOBase) | |
| self.assertIsInstance(p.stdout, io.BufferedIOBase) | |
| self.assertIsInstance(p.stderr, io.BufferedIOBase) | |
| finally: | |
| p.stdin.close() | |
| p.stdout.close() | |
| p.stderr.close() | |
| p.wait() | |
| def test_io_unbuffered_works(self): | |
| p = subprocess.Popen(ZERO_RETURN_CMD, | |
| stdin=subprocess.PIPE, stdout=subprocess.PIPE, | |
| stderr=subprocess.PIPE, bufsize=0) | |
| try: | |
| self.assertIsInstance(p.stdin, io.RawIOBase) | |
| self.assertIsInstance(p.stdout, io.RawIOBase) | |
| self.assertIsInstance(p.stderr, io.RawIOBase) | |
| finally: | |
| p.stdin.close() | |
| p.stdout.close() | |
| p.stderr.close() | |
| p.wait() | |
| def test_call_seq(self): | |
| # call() function with sequence argument | |
| rc = subprocess.call([sys.executable, "-c", | |
| "import sys; sys.exit(47)"]) | |
| self.assertEqual(rc, 47) | |
| def test_call_timeout(self): | |
| # call() function with timeout argument; we want to test that the child | |
| # process gets killed when the timeout expires. If the child isn't | |
| # killed, this call will deadlock since subprocess.call waits for the | |
| # child. | |
| self.assertRaises(subprocess.TimeoutExpired, subprocess.call, | |
| [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) | |
| self.assertEqual(rc, 0) | |
| def test_check_call_nonzero(self): | |
| # check_call() function with non-zero return code | |
| with self.assertRaises(subprocess.CalledProcessError) as c: | |
| subprocess.check_call([sys.executable, "-c", | |
| "import sys; sys.exit(47)"]) | |
| self.assertEqual(c.exception.returncode, 47) | |
| def test_check_output(self): | |
| # check_output() function with zero return code | |
| output = subprocess.check_output( | |
| [sys.executable, "-c", "print('BDFL')"]) | |
| self.assertIn(b'BDFL', output) | |
| with self.assertRaisesRegex(ValueError, | |
| "stdout argument not allowed, it will be overridden"): | |
| subprocess.check_output([], stdout=None) | |
| with self.assertRaisesRegex(ValueError, | |
| "check argument not allowed, it will be overridden"): | |
| subprocess.check_output([], check=False) | |
| def test_check_output_nonzero(self): | |
| # check_call() function with non-zero return code | |
| with self.assertRaises(subprocess.CalledProcessError) as c: | |
| subprocess.check_output( | |
| [sys.executable, "-c", "import sys; sys.exit(5)"]) | |
| self.assertEqual(c.exception.returncode, 5) | |
| def test_check_output_stderr(self): | |
| # check_output() function stderr redirected to stdout | |
| output = subprocess.check_output( | |
| [sys.executable, "-c", "import sys; sys.stderr.write('BDFL')"], | |
| stderr=subprocess.STDOUT) | |
| self.assertIn(b'BDFL', output) | |
| def test_check_output_stdin_arg(self): | |
| # check_output() can be called with stdin set to a file | |
| tf = tempfile.TemporaryFile() | |
| self.addCleanup(tf.close) | |
| tf.write(b'pear') | |
| tf.seek(0) | |
| output = subprocess.check_output( | |
| [sys.executable, "-c", | |
| "import sys; sys.stdout.write(sys.stdin.read().upper())"], | |
| stdin=tf) | |
| self.assertIn(b'PEAR', output) | |
| def test_check_output_input_arg(self): | |
| # check_output() can be called with input set to a string | |
| output = subprocess.check_output( | |
| [sys.executable, "-c", | |
| "import sys; sys.stdout.write(sys.stdin.read().upper())"], | |
| input=b'pear') | |
| self.assertIn(b'PEAR', output) | |
| def test_check_output_input_none(self): | |
| """input=None has a legacy meaning of input='' on check_output.""" | |
| output = subprocess.check_output( | |
| [sys.executable, "-c", | |
| "import sys; print('XX' if sys.stdin.read() else '')"], | |
| input=None) | |
| self.assertNotIn(b'XX', output) | |
| def test_check_output_input_none_text(self): | |
| output = subprocess.check_output( | |
| [sys.executable, "-c", | |
| "import sys; print('XX' if sys.stdin.read() else '')"], | |
| input=None, text=True) | |
| self.assertNotIn('XX', output) | |
| def test_check_output_input_none_universal_newlines(self): | |
| output = subprocess.check_output( | |
| [sys.executable, "-c", | |
| "import sys; print('XX' if sys.stdin.read() else '')"], | |
| input=None, universal_newlines=True) | |
| self.assertNotIn('XX', output) | |
| def test_check_output_input_none_encoding_errors(self): | |
| output = subprocess.check_output( | |
| [sys.executable, "-c", "print('foo')"], | |
| input=None, encoding='utf-8', errors='ignore') | |
| self.assertIn('foo', output) | |
| def test_check_output_stdout_arg(self): | |
| # check_output() refuses to accept 'stdout' argument | |
| with self.assertRaises(ValueError) as c: | |
| output = subprocess.check_output( | |
| [sys.executable, "-c", "print('will not be run')"], | |
| stdout=sys.stdout) | |
| self.fail("Expected ValueError when stdout arg supplied.") | |
| self.assertIn('stdout', c.exception.args[0]) | |
| def test_check_output_stdin_with_input_arg(self): | |
| # check_output() refuses to accept 'stdin' with 'input' | |
| tf = tempfile.TemporaryFile() | |
| self.addCleanup(tf.close) | |
| tf.write(b'pear') | |
| tf.seek(0) | |
| with self.assertRaises(ValueError) as c: | |
| output = subprocess.check_output( | |
| [sys.executable, "-c", "print('will not be run')"], | |
| stdin=tf, input=b'hare') | |
| self.fail("Expected ValueError when stdin and input args supplied.") | |
| self.assertIn('stdin', c.exception.args[0]) | |
| self.assertIn('input', c.exception.args[0]) | |
| 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 time; time.sleep(3600)"], | |
| timeout=0.1) | |
| def test_call_kwargs(self): | |
| # call() function with keyword args | |
| newenv = os.environ.copy() | |
| newenv["FRUIT"] = "banana" | |
| rc = subprocess.call([sys.executable, "-c", | |
| 'import sys, os;' | |
| 'sys.exit(os.getenv("FRUIT")=="banana")'], | |
| env=newenv) | |
| self.assertEqual(rc, 1) | |
| def test_invalid_args(self): | |
| # Popen() called with invalid arguments should raise TypeError | |
| # but Popen.__del__ should not complain (issue #12085) | |
| with support.captured_stderr() as s: | |
| self.assertRaises(TypeError, subprocess.Popen, invalid_arg_name=1) | |
| argcount = subprocess.Popen.__init__.__code__.co_argcount | |
| too_many_args = [0] * (argcount + 1) | |
| self.assertRaises(TypeError, subprocess.Popen, *too_many_args) | |
| self.assertEqual(s.getvalue(), '') | |
| def test_stdin_none(self): | |
| # .stdin is None when not redirected | |
| p = subprocess.Popen([sys.executable, "-c", 'print("banana")'], | |
| stdout=subprocess.PIPE, stderr=subprocess.PIPE) | |
| self.addCleanup(p.stdout.close) | |
| self.addCleanup(p.stderr.close) | |
| p.wait() | |
| self.assertEqual(p.stdin, None) | |
| def test_stdout_none(self): | |
| # .stdout is None when not redirected, and the child's stdout will | |
| # be inherited from the parent. In order to test this we run a | |
| # subprocess in a subprocess: | |
| # this_test | |
| # \-- subprocess created by this test (parent) | |
| # \-- subprocess created by the parent subprocess (child) | |
| # The parent doesn't specify stdout, so the child will use the | |
| # parent's stdout. This test checks that the message printed by the | |
| # child goes to the parent stdout. The parent also checks that the | |
| # child's stdout is None. See #11963. | |
| code = ('import sys; from subprocess import Popen, PIPE;' | |
| 'p = Popen([sys.executable, "-c", "print(\'test_stdout_none\')"],' | |
| ' stdin=PIPE, stderr=PIPE);' | |
| 'p.wait(); assert p.stdout is None;') | |
| p = subprocess.Popen([sys.executable, "-c", code], | |
| stdout=subprocess.PIPE, stderr=subprocess.PIPE) | |
| self.addCleanup(p.stdout.close) | |
| self.addCleanup(p.stderr.close) | |
| out, err = p.communicate() | |
| self.assertEqual(p.returncode, 0, err) | |
| self.assertEqual(out.rstrip(), b'test_stdout_none') | |
| def test_stderr_none(self): | |
| # .stderr is None when not redirected | |
| p = subprocess.Popen([sys.executable, "-c", 'print("banana")'], | |
| stdin=subprocess.PIPE, stdout=subprocess.PIPE) | |
| self.addCleanup(p.stdout.close) | |
| self.addCleanup(p.stdin.close) | |
| p.wait() | |
| self.assertEqual(p.stderr, None) | |
| def _assert_python(self, pre_args, **kwargs): | |
| # We include sys.exit() to prevent the test runner from hanging | |
| # whenever python is found. | |
| args = pre_args + ["import sys; sys.exit(47)"] | |
| p = subprocess.Popen(args, **kwargs) | |
| p.wait() | |
| self.assertEqual(47, p.returncode) | |
| def test_executable(self): | |
| # Check that the executable argument works. | |
| # | |
| # On Unix (non-Mac and non-Windows), Python looks at args[0] to | |
| # determine where its standard library is, so we need the directory | |
| # of args[0] to be valid for the Popen() call to Python to succeed. | |
| # See also issue #16170 and issue #7774. | |
| doesnotexist = os.path.join(os.path.dirname(sys.executable), | |
| "doesnotexist") | |
| self._assert_python([doesnotexist, "-c"], executable=sys.executable) | |
| def test_bytes_executable(self): | |
| doesnotexist = os.path.join(os.path.dirname(sys.executable), | |
| "doesnotexist") | |
| self._assert_python([doesnotexist, "-c"], | |
| executable=os.fsencode(sys.executable)) | |
| def test_pathlike_executable(self): | |
| doesnotexist = os.path.join(os.path.dirname(sys.executable), | |
| "doesnotexist") | |
| self._assert_python([doesnotexist, "-c"], | |
| executable=FakePath(sys.executable)) | |
| def test_executable_takes_precedence(self): | |
| # Check that the executable argument takes precedence over args[0]. | |
| # | |
| # Verify first that the call succeeds without the executable arg. | |
| pre_args = [sys.executable, "-c"] | |
| self._assert_python(pre_args) | |
| self.assertRaises(NONEXISTING_ERRORS, | |
| self._assert_python, pre_args, | |
| executable=NONEXISTING_CMD[0]) | |
| @unittest.skipIf(mswindows, "executable argument replaces shell") | |
| def test_executable_replaces_shell(self): | |
| # Check that the executable argument replaces the default shell | |
| # when shell=True. | |
| self._assert_python([], executable=sys.executable, shell=True) | |
| @unittest.skipIf(mswindows, "executable argument replaces shell") | |
| def test_bytes_executable_replaces_shell(self): | |
| self._assert_python([], executable=os.fsencode(sys.executable), | |
| shell=True) | |
| @unittest.skipIf(mswindows, "executable argument replaces shell") | |
| def test_pathlike_executable_replaces_shell(self): | |
| self._assert_python([], executable=FakePath(sys.executable), | |
| shell=True) | |
| # For use in the test_cwd* tests below. | |
| def _normalize_cwd(self, cwd): | |
| # Normalize an expected cwd (for Tru64 support). | |
| # We can't use os.path.realpath since it doesn't expand Tru64 {memb} | |
| # strings. See bug #1063571. | |
| with os_helper.change_cwd(cwd): | |
| return os.getcwd() | |
| # For use in the test_cwd* tests below. | |
| def _split_python_path(self): | |
| # Return normalized (python_dir, python_base). | |
| python_path = os.path.realpath(sys.executable) | |
| return os.path.split(python_path) | |
| # For use in the test_cwd* tests below. | |
| def _assert_cwd(self, expected_cwd, python_arg, **kwargs): | |
| # Invoke Python via Popen, and assert that (1) the call succeeds, | |
| # and that (2) the current working directory of the child process | |
| # matches *expected_cwd*. | |
| p = subprocess.Popen([python_arg, "-c", | |
| "import os, sys; " | |
| "buf = sys.stdout.buffer; " | |
| "buf.write(os.getcwd().encode()); " | |
| "buf.flush(); " | |
| "sys.exit(47)"], | |
| stdout=subprocess.PIPE, | |
| **kwargs) | |
| self.addCleanup(p.stdout.close) | |
| p.wait() | |
| self.assertEqual(47, p.returncode) | |
| normcase = os.path.normcase | |
| self.assertEqual(normcase(expected_cwd), | |
| normcase(p.stdout.read().decode())) | |
| def test_cwd(self): | |
| # Check that cwd changes the cwd for the child process. | |
| temp_dir = tempfile.gettempdir() | |
| temp_dir = self._normalize_cwd(temp_dir) | |
| self._assert_cwd(temp_dir, sys.executable, cwd=temp_dir) | |
| def test_cwd_with_bytes(self): | |
| temp_dir = tempfile.gettempdir() | |
| temp_dir = self._normalize_cwd(temp_dir) | |
| self._assert_cwd(temp_dir, sys.executable, cwd=os.fsencode(temp_dir)) | |
| def test_cwd_with_pathlike(self): | |
| temp_dir = tempfile.gettempdir() | |
| temp_dir = self._normalize_cwd(temp_dir) | |
| self._assert_cwd(temp_dir, sys.executable, cwd=FakePath(temp_dir)) | |
| @unittest.skipIf(mswindows, "pending resolution of issue #15533") | |
| def test_cwd_with_relative_arg(self): | |
| # Check that Popen looks for args[0] relative to cwd if args[0] | |
| # is relative. | |
| python_dir, python_base = self._split_python_path() | |
| rel_python = os.path.join(os.curdir, python_base) | |
| with os_helper.temp_cwd() as wrong_dir: | |
| # Before calling with the correct cwd, confirm that the call fails | |
| # without cwd and with the wrong cwd. | |
| self.assertRaises(FileNotFoundError, subprocess.Popen, | |
| [rel_python]) | |
| self.assertRaises(FileNotFoundError, subprocess.Popen, | |
| [rel_python], cwd=wrong_dir) | |
| python_dir = self._normalize_cwd(python_dir) | |
| self._assert_cwd(python_dir, rel_python, cwd=python_dir) | |
| @unittest.skipIf(mswindows, "pending resolution of issue #15533") | |
| def test_cwd_with_relative_executable(self): | |
| # Check that Popen looks for executable relative to cwd if executable | |
| # is relative (and that executable takes precedence over args[0]). | |
| python_dir, python_base = self._split_python_path() | |
| rel_python = os.path.join(os.curdir, python_base) | |
| doesntexist = "somethingyoudonthave" | |
| with os_helper.temp_cwd() as wrong_dir: | |
| # Before calling with the correct cwd, confirm that the call fails | |
| # without cwd and with the wrong cwd. | |
| self.assertRaises(FileNotFoundError, subprocess.Popen, | |
| [doesntexist], executable=rel_python) | |
| self.assertRaises(FileNotFoundError, subprocess.Popen, | |
| [doesntexist], executable=rel_python, | |
| cwd=wrong_dir) | |
| python_dir = self._normalize_cwd(python_dir) | |
| self._assert_cwd(python_dir, doesntexist, executable=rel_python, | |
| cwd=python_dir) | |
| def test_cwd_with_absolute_arg(self): | |
| # Check that Popen can find the executable when the cwd is wrong | |
| # if args[0] is an absolute path. | |
| python_dir, python_base = self._split_python_path() | |
| abs_python = os.path.join(python_dir, python_base) | |
| rel_python = os.path.join(os.curdir, python_base) | |
| with os_helper.temp_dir() as wrong_dir: | |
| # Before calling with an absolute path, confirm that using a | |
| # relative path fails. | |
| self.assertRaises(FileNotFoundError, subprocess.Popen, | |
| [rel_python], cwd=wrong_dir) | |
| wrong_dir = self._normalize_cwd(wrong_dir) | |
| self._assert_cwd(wrong_dir, abs_python, cwd=wrong_dir) | |
| @unittest.skipIf(sys.base_prefix != sys.prefix, | |
| 'Test is not venv-compatible') | |
| def test_executable_with_cwd(self): | |
| python_dir, python_base = self._split_python_path() | |
| python_dir = self._normalize_cwd(python_dir) | |
| self._assert_cwd(python_dir, "somethingyoudonthave", | |
| executable=sys.executable, cwd=python_dir) | |
| @unittest.skipIf(sys.base_prefix != sys.prefix, | |
| 'Test is not venv-compatible') | |
| @unittest.skipIf(sysconfig.is_python_build(), | |
| "need an installed Python. See #7774") | |
| def test_executable_without_cwd(self): | |
| # For a normal installation, it should work without 'cwd' | |
| # argument. For test runs in the build directory, see #7774. | |
| self._assert_cwd(os.getcwd(), "somethingyoudonthave", | |
| executable=sys.executable) | |
| def test_stdin_pipe(self): | |
| # stdin redirection | |
| p = subprocess.Popen([sys.executable, "-c", | |
| 'import sys; sys.exit(sys.stdin.read() == "pear")'], | |
| stdin=subprocess.PIPE) | |
| p.stdin.write(b"pear") | |
| p.stdin.close() | |
| p.wait() | |
| self.assertEqual(p.returncode, 1) | |
| def test_stdin_filedes(self): | |
| # stdin is set to open file descriptor | |
| tf = tempfile.TemporaryFile() | |
| self.addCleanup(tf.close) | |
| d = tf.fileno() | |
| os.write(d, b"pear") | |
| os.lseek(d, 0, 0) | |
| p = subprocess.Popen([sys.executable, "-c", | |
| 'import sys; sys.exit(sys.stdin.read() == "pear")'], | |
| stdin=d) | |
| p.wait() | |
| self.assertEqual(p.returncode, 1) | |
| def test_stdin_fileobj(self): | |
| # stdin is set to open file object | |
| tf = tempfile.TemporaryFile() | |
| self.addCleanup(tf.close) | |
| tf.write(b"pear") | |
| tf.seek(0) | |
| p = subprocess.Popen([sys.executable, "-c", | |
| 'import sys; sys.exit(sys.stdin.read() == "pear")'], | |
| stdin=tf) | |
| p.wait() | |
| self.assertEqual(p.returncode, 1) | |
| def test_stdout_pipe(self): | |
| # stdout redirection | |
| p = subprocess.Popen([sys.executable, "-c", | |
| 'import sys; sys.stdout.write("orange")'], | |
| stdout=subprocess.PIPE) | |
| with p: | |
| self.assertEqual(p.stdout.read(), b"orange") | |
| def test_stdout_filedes(self): | |
| # stdout is set to open file descriptor | |
| tf = tempfile.TemporaryFile() | |
| self.addCleanup(tf.close) | |
| d = tf.fileno() | |
| p = subprocess.Popen([sys.executable, "-c", | |
| 'import sys; sys.stdout.write("orange")'], | |
| stdout=d) | |
| p.wait() | |
| os.lseek(d, 0, 0) | |
| self.assertEqual(os.read(d, 1024), b"orange") | |
| def test_stdout_fileobj(self): | |
| # stdout is set to open file object | |
| tf = tempfile.TemporaryFile() | |
| self.addCleanup(tf.close) | |
| p = subprocess.Popen([sys.executable, "-c", | |
| 'import sys; sys.stdout.write("orange")'], | |
| stdout=tf) | |
| p.wait() | |
| tf.seek(0) | |
| self.assertEqual(tf.read(), b"orange") | |
| def test_stderr_pipe(self): | |
| # stderr redirection | |
| p = subprocess.Popen([sys.executable, "-c", | |
| 'import sys; sys.stderr.write("strawberry")'], | |
| stderr=subprocess.PIPE) | |
| with p: | |
| self.assertEqual(p.stderr.read(), b"strawberry") | |
| def test_stderr_filedes(self): | |
| # stderr is set to open file descriptor | |
| tf = tempfile.TemporaryFile() | |
| self.addCleanup(tf.close) | |
| d = tf.fileno() | |
| p = subprocess.Popen([sys.executable, "-c", | |
| 'import sys; sys.stderr.write("strawberry")'], | |
| stderr=d) | |
| p.wait() | |
| os.lseek(d, 0, 0) | |
| self.assertEqual(os.read(d, 1024), b"strawberry") | |
| def test_stderr_fileobj(self): | |
| # stderr is set to open file object | |
| tf = tempfile.TemporaryFile() | |
| self.addCleanup(tf.close) | |
| p = subprocess.Popen([sys.executable, "-c", | |
| 'import sys; sys.stderr.write("strawberry")'], | |
| stderr=tf) | |
| p.wait() | |
| tf.seek(0) | |
| self.assertEqual(tf.read(), b"strawberry") | |
| def test_stderr_redirect_with_no_stdout_redirect(self): | |
| # test stderr=STDOUT while stdout=None (not set) | |
| # - grandchild prints to stderr | |
| # - child redirects grandchild's stderr to its stdout | |
| # - the parent should get grandchild's stderr in child's stdout | |
| p = subprocess.Popen([sys.executable, "-c", | |
| 'import sys, subprocess;' | |
| 'rc = subprocess.call([sys.executable, "-c",' | |
| ' "import sys;"' | |
| ' "sys.stderr.write(\'42\')"],' | |
| ' stderr=subprocess.STDOUT);' | |
| 'sys.exit(rc)'], | |
| stdout=subprocess.PIPE, | |
| stderr=subprocess.PIPE) | |
| stdout, stderr = p.communicate() | |
| #NOTE: stdout should get stderr from grandchild | |
| self.assertEqual(stdout, b'42') | |
| self.assertEqual(stderr, b'') # should be empty | |
| self.assertEqual(p.returncode, 0) | |
| def test_stdout_stderr_pipe(self): | |
| # capture stdout and stderr to the same pipe | |
| p = subprocess.Popen([sys.executable, "-c", | |
| 'import sys;' | |
| 'sys.stdout.write("apple");' | |
| 'sys.stdout.flush();' | |
| 'sys.stderr.write("orange")'], | |
| stdout=subprocess.PIPE, | |
| stderr=subprocess.STDOUT) | |
| with p: | |
| self.assertEqual(p.stdout.read(), b"appleorange") | |
| def test_stdout_stderr_file(self): | |
| # capture stdout and stderr to the same open file | |
| tf = tempfile.TemporaryFile() | |
| self.addCleanup(tf.close) | |
| p = subprocess.Popen([sys.executable, "-c", | |
| 'import sys;' | |
| 'sys.stdout.write("apple");' | |
| 'sys.stdout.flush();' | |
| 'sys.stderr.write("orange")'], | |
| stdout=tf, | |
| stderr=tf) | |
| p.wait() | |
| tf.seek(0) | |
| self.assertEqual(tf.read(), b"appleorange") | |
| def test_stdout_filedes_of_stdout(self): | |
| # stdout is set to 1 (#1531862). | |
| # To avoid printing the text on stdout, we do something similar to | |
| # test_stdout_none (see above). The parent subprocess calls the child | |
| # subprocess passing stdout=1, and this test uses stdout=PIPE in | |
| # order to capture and check the output of the parent. See #11963. | |
| code = ('import sys, subprocess; ' | |
| 'rc = subprocess.call([sys.executable, "-c", ' | |
| ' "import os, sys; sys.exit(os.write(sys.stdout.fileno(), ' | |
| 'b\'test with stdout=1\'))"], stdout=1); ' | |
| 'assert rc == 18') | |
| p = subprocess.Popen([sys.executable, "-c", code], | |
| stdout=subprocess.PIPE, stderr=subprocess.PIPE) | |
| self.addCleanup(p.stdout.close) | |
| self.addCleanup(p.stderr.close) | |
| out, err = p.communicate() | |
| self.assertEqual(p.returncode, 0, err) | |
| self.assertEqual(out.rstrip(), b'test with stdout=1') | |
| def test_stdout_devnull(self): | |
| p = subprocess.Popen([sys.executable, "-c", | |
| 'for i in range(10240):' | |
| 'print("x" * 1024)'], | |
| stdout=subprocess.DEVNULL) | |
| p.wait() | |
| self.assertEqual(p.stdout, None) | |
| def test_stderr_devnull(self): | |
| p = subprocess.Popen([sys.executable, "-c", | |
| 'import sys\n' | |
| 'for i in range(10240):' | |
| 'sys.stderr.write("x" * 1024)'], | |
| stderr=subprocess.DEVNULL) | |
| p.wait() | |
| self.assertEqual(p.stderr, None) | |
| def test_stdin_devnull(self): | |
| p = subprocess.Popen([sys.executable, "-c", | |
| 'import sys;' | |
| 'sys.stdin.read(1)'], | |
| stdin=subprocess.DEVNULL) | |
| p.wait() | |
| self.assertEqual(p.stdin, None) | |
| @unittest.skipUnless(fcntl and hasattr(fcntl, 'F_GETPIPE_SZ'), | |
| 'fcntl.F_GETPIPE_SZ required for test.') | |
| def test_pipesizes(self): | |
| test_pipe_r, test_pipe_w = os.pipe() | |
| try: | |
| # Get the default pipesize with F_GETPIPE_SZ | |
| pipesize_default = fcntl.fcntl(test_pipe_w, fcntl.F_GETPIPE_SZ) | |
| finally: | |
| os.close(test_pipe_r) | |
| os.close(test_pipe_w) | |
| pipesize = pipesize_default // 2 | |
| pagesize_default = support.get_pagesize() | |
| if pipesize < pagesize_default: # the POSIX minimum | |
| raise unittest.SkipTest( | |
| 'default pipesize too small to perform test.') | |
| p = subprocess.Popen( | |
| [sys.executable, "-c", | |
| 'import sys; sys.stdin.read(); sys.stdout.write("out"); ' | |
| 'sys.stderr.write("error!")'], | |
| stdin=subprocess.PIPE, stdout=subprocess.PIPE, | |
| stderr=subprocess.PIPE, pipesize=pipesize) | |
| try: | |
| for fifo in [p.stdin, p.stdout, p.stderr]: | |
| self.assertEqual( | |
| fcntl.fcntl(fifo.fileno(), fcntl.F_GETPIPE_SZ), | |
| pipesize) | |
| # Windows pipe size can be acquired via GetNamedPipeInfoFunction | |
| # https://docs.microsoft.com/en-us/windows/win32/api/namedpipeapi/nf-namedpipeapi-getnamedpipeinfo | |
| # However, this function is not yet in _winapi. | |
| p.stdin.write(b"pear") | |
| p.stdin.close() | |
| p.stdout.close() | |
| p.stderr.close() | |
| finally: | |
| p.kill() | |
| p.wait() | |
| @unittest.skipUnless(fcntl and hasattr(fcntl, 'F_GETPIPE_SZ'), | |
| 'fcntl.F_GETPIPE_SZ required for test.') | |
| def test_pipesize_default(self): | |
| proc = subprocess.Popen( | |
| [sys.executable, "-c", | |
| 'import sys; sys.stdin.read(); sys.stdout.write("out"); ' | |
| 'sys.stderr.write("error!")'], | |
| stdin=subprocess.PIPE, stdout=subprocess.PIPE, | |
| stderr=subprocess.PIPE, pipesize=-1) | |
| with proc: | |
| try: | |
| fp_r, fp_w = os.pipe() | |
| try: | |
| default_read_pipesize = fcntl.fcntl(fp_r, fcntl.F_GETPIPE_SZ) | |
| default_write_pipesize = fcntl.fcntl(fp_w, fcntl.F_GETPIPE_SZ) | |
| finally: | |
| os.close(fp_r) | |
| os.close(fp_w) | |
| self.assertEqual( | |
| fcntl.fcntl(proc.stdin.fileno(), fcntl.F_GETPIPE_SZ), | |
| default_read_pipesize) | |
| self.assertEqual( | |
| fcntl.fcntl(proc.stdout.fileno(), fcntl.F_GETPIPE_SZ), | |
| default_write_pipesize) | |
| self.assertEqual( | |
| fcntl.fcntl(proc.stderr.fileno(), fcntl.F_GETPIPE_SZ), | |
| default_write_pipesize) | |
| # On other platforms we cannot test the pipe size (yet). But above | |
| # code using pipesize=-1 should not crash. | |
| finally: | |
| proc.kill() | |
| def test_env(self): | |
| newenv = os.environ.copy() | |
| newenv["FRUIT"] = "orange" | |
| with subprocess.Popen([sys.executable, "-c", | |
| 'import sys,os;' | |
| 'sys.stdout.write(os.getenv("FRUIT"))'], | |
| stdout=subprocess.PIPE, | |
| env=newenv) as p: | |
| stdout, stderr = p.communicate() | |
| self.assertEqual(stdout, b"orange") | |
| @unittest.skipUnless(sys.platform == "win32", "Windows only issue") | |
| def test_win32_duplicate_envs(self): | |
| newenv = os.environ.copy() | |
| newenv["fRUit"] = "cherry" | |
| newenv["fruit"] = "lemon" | |
| newenv["FRUIT"] = "orange" | |
| newenv["frUit"] = "banana" | |
| with subprocess.Popen(["CMD", "/c", "SET", "fruit"], | |
| stdout=subprocess.PIPE, | |
| env=newenv) as p: | |
| stdout, _ = p.communicate() | |
| self.assertEqual(stdout.strip(), b"frUit=banana") | |
| # Windows requires at least the SYSTEMROOT environment variable to start | |
| # Python | |
| @unittest.skipIf(sys.platform == 'win32', | |
| 'cannot test an empty env on Windows') | |
| @unittest.skipIf(sysconfig.get_config_var('Py_ENABLE_SHARED') == 1, | |
| 'The Python shared library cannot be loaded ' | |
| 'with an empty environment.') | |
| @unittest.skipIf(check_sanitizer(address=True), | |
| 'AddressSanitizer adds to the environment.') | |
| def test_empty_env(self): | |
| """Verify that env={} is as empty as possible.""" | |
| def is_env_var_to_ignore(n): | |
| """Determine if an environment variable is under our control.""" | |
| # This excludes some __CF_* and VERSIONER_* keys MacOS insists | |
| # on adding even when the environment in exec is empty. | |
| # Gentoo sandboxes also force LD_PRELOAD and SANDBOX_* to exist. | |
| return ('VERSIONER' in n or '__CF' in n or # MacOS | |
| n == 'LD_PRELOAD' or n.startswith('SANDBOX') or # Gentoo | |
| n == 'LC_CTYPE') # Locale coercion triggered | |
| with subprocess.Popen([sys.executable, "-c", | |
| 'import os; print(list(os.environ.keys()))'], | |
| stdout=subprocess.PIPE, env={}) as p: | |
| stdout, stderr = p.communicate() | |
| child_env_names = eval(stdout.strip()) | |
| self.assertIsInstance(child_env_names, list) | |
| child_env_names = [k for k in child_env_names | |
| if not is_env_var_to_ignore(k)] | |
| self.assertEqual(child_env_names, []) | |
| @unittest.skipIf(sysconfig.get_config_var('Py_ENABLE_SHARED') == 1, | |
| 'The Python shared library cannot be loaded ' | |
| 'without some system environments.') | |
| @unittest.skipIf(check_sanitizer(address=True), | |
| 'AddressSanitizer adds to the environment.') | |
| def test_one_environment_variable(self): | |
| newenv = {'fruit': 'orange'} | |
| cmd = [sys.executable, '-c', | |
| 'import sys,os;' | |
| 'sys.stdout.write("fruit="+os.getenv("fruit"))'] | |
| if sys.platform == "win32": | |
| cmd = ["CMD", "/c", "SET", "fruit"] | |
| with subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, env=newenv) as p: | |
| stdout, stderr = p.communicate() | |
| if p.returncode and support.verbose: | |
| print("STDOUT:", stdout.decode("ascii", "replace")) | |
| print("STDERR:", stderr.decode("ascii", "replace")) | |
| self.assertEqual(p.returncode, 0) | |
| self.assertEqual(stdout.strip(), b"fruit=orange") | |
| def test_invalid_cmd(self): | |
| # null character in the command name | |
| cmd = sys.executable + '\0' | |
| with self.assertRaises(ValueError): | |
| subprocess.Popen([cmd, "-c", "pass"]) | |
| # null character in the command argument | |
| with self.assertRaises(ValueError): | |
| subprocess.Popen([sys.executable, "-c", "pass#\0"]) | |
| def test_invalid_env(self): | |
| # null character in the environment variable name | |
| newenv = os.environ.copy() | |
| newenv["FRUIT\0VEGETABLE"] = "cabbage" | |
| with self.assertRaises(ValueError): | |
| subprocess.Popen(ZERO_RETURN_CMD, env=newenv) | |
| # null character in the environment variable value | |
| newenv = os.environ.copy() | |
| newenv["FRUIT"] = "orange\0VEGETABLE=cabbage" | |
| with self.assertRaises(ValueError): | |
| subprocess.Popen(ZERO_RETURN_CMD, env=newenv) | |
| # equal character in the environment variable name | |
| newenv = os.environ.copy() | |
| newenv["FRUIT=ORANGE"] = "lemon" | |
| with self.assertRaises(ValueError): | |
| subprocess.Popen(ZERO_RETURN_CMD, env=newenv) | |
| # equal character in the environment variable value | |
| newenv = os.environ.copy() | |
| newenv["FRUIT"] = "orange=lemon" | |
| with subprocess.Popen([sys.executable, "-c", | |
| 'import sys, os;' | |
| 'sys.stdout.write(os.getenv("FRUIT"))'], | |
| stdout=subprocess.PIPE, | |
| env=newenv) as p: | |
| stdout, stderr = p.communicate() | |
| self.assertEqual(stdout, b"orange=lemon") | |
| @unittest.skipUnless(sys.platform == "win32", "Windows only issue") | |
| def test_win32_invalid_env(self): | |
| # '=' in the environment variable name | |
| newenv = os.environ.copy() | |
| newenv["FRUIT=VEGETABLE"] = "cabbage" | |
| with self.assertRaises(ValueError): | |
| subprocess.Popen(ZERO_RETURN_CMD, env=newenv) | |
| newenv = os.environ.copy() | |
| newenv["==FRUIT"] = "cabbage" | |
| with self.assertRaises(ValueError): | |
| subprocess.Popen(ZERO_RETURN_CMD, env=newenv) | |
| def test_communicate_stdin(self): | |
| p = subprocess.Popen([sys.executable, "-c", | |
| 'import sys;' | |
| 'sys.exit(sys.stdin.read() == "pear")'], | |
| stdin=subprocess.PIPE) | |
| p.communicate(b"pear") | |
| self.assertEqual(p.returncode, 1) | |
| def test_communicate_stdout(self): | |
| p = subprocess.Popen([sys.executable, "-c", | |
| 'import sys; sys.stdout.write("pineapple")'], | |
| stdout=subprocess.PIPE) | |
| (stdout, stderr) = p.communicate() | |
| self.assertEqual(stdout, b"pineapple") | |
| self.assertEqual(stderr, None) | |
| def test_communicate_stderr(self): | |
| p = subprocess.Popen([sys.executable, "-c", | |
| 'import sys; sys.stderr.write("pineapple")'], | |
| stderr=subprocess.PIPE) | |
| (stdout, stderr) = p.communicate() | |
| self.assertEqual(stdout, None) | |
| self.assertEqual(stderr, b"pineapple") | |
| def test_communicate(self): | |
| p = subprocess.Popen([sys.executable, "-c", | |
| 'import sys,os;' | |
| 'sys.stderr.write("pineapple");' | |
| 'sys.stdout.write(sys.stdin.read())'], | |
| stdin=subprocess.PIPE, | |
| stdout=subprocess.PIPE, | |
| stderr=subprocess.PIPE) | |
| self.addCleanup(p.stdout.close) | |
| self.addCleanup(p.stderr.close) | |
| self.addCleanup(p.stdin.close) | |
| (stdout, stderr) = p.communicate(b"banana") | |
| 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;' | |
| 'sys.stderr.write("pineapple\\n");' | |
| 'time.sleep(1);' | |
| 'sys.stderr.write("pear\\n");' | |
| 'sys.stdout.write(sys.stdin.read())'], | |
| universal_newlines=True, | |
| stdin=subprocess.PIPE, | |
| stdout=subprocess.PIPE, | |
| stderr=subprocess.PIPE) | |
| self.assertRaises(subprocess.TimeoutExpired, p.communicate, "banana", | |
| timeout=0.3) | |
| # Make sure we can keep waiting for it, and that we get the whole output | |
| # after it completes. | |
| (stdout, stderr) = p.communicate() | |
| self.assertEqual(stdout, "banana") | |
| self.assertEqual(stderr.encode(), b"pineapple\npear\n") | |
| def test_communicate_timeout_large_output(self): | |
| # Test an expiring timeout while the child is outputting lots of data. | |
| p = subprocess.Popen([sys.executable, "-c", | |
| 'import sys,os,time;' | |
| 'sys.stdout.write("a" * (64 * 1024));' | |
| 'time.sleep(0.2);' | |
| 'sys.stdout.write("a" * (64 * 1024));' | |
| 'time.sleep(0.2);' | |
| 'sys.stdout.write("a" * (64 * 1024));' | |
| 'time.sleep(0.2);' | |
| 'sys.stdout.write("a" * (64 * 1024));'], | |
| stdout=subprocess.PIPE) | |
| self.assertRaises(subprocess.TimeoutExpired, p.communicate, timeout=0.4) | |
| (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_data = b"x" * (128 * 1024) # > typical pipe buffer | |
| # Cross-platform wake mechanism: the slow reader connects to a | |
| # loopback TCP socket and blocks in select() on it (capped at 9s | |
| # as a safety net we don't expect to hit). After phase 1 raises | |
| # TimeoutExpired, the parent sends a byte to release the child so | |
| # it drains stdin. A socket (rather than a raw pipe) is required | |
| # because Windows select() only supports sockets, not arbitrary | |
| # file descriptors. | |
| server = socket.create_server(('127.0.0.1', 0), backlog=1) | |
| server.settimeout(10) # bound the accept() if the child fails to start | |
| port = server.getsockname()[1] | |
| # The child sends one byte (low byte of its PID) first so the parent | |
| # can detect the rare case of an unrelated process on the same host | |
| # connecting to our ephemeral port before our child does. A single | |
| # byte gives 1/256 collision odds, which is plenty for flake-prevention. | |
| slow_reader = ( | |
| "import os, socket, sys, select; " | |
| f"s = socket.create_connection(('127.0.0.1', {port}), timeout=9); " | |
| "s.sendall(bytes([os.getpid() & 0xff])); " | |
| "select.select([s], [], [], 9); " | |
| "sys.stdout.buffer.write(sys.stdin.buffer.read())" | |
| ) | |
| p = subprocess.Popen( | |
| [sys.executable, "-c", slow_reader], | |
| stdin=subprocess.PIPE, | |
| stdout=subprocess.PIPE, | |
| stderr=subprocess.PIPE) | |
| conn = None | |
| try: | |
| conn, _ = server.accept() | |
| server.close() | |
| server = None | |
| conn.settimeout(5) | |
| peer_byte = conn.recv(1) | |
| conn.settimeout(None) | |
| self.assertEqual(peer_byte, bytes([p.pid & 0xff]), | |
| f"loopback handshake byte {peer_byte!r} != " | |
| f"low byte of child PID {p.pid} ({p.pid & 0xff:#x})") | |
| 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 slow reader stalls for up to 9s. " | |
| "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 slow-reader's stall cap. | |
| self.assertLess(elapsed, 5.0, | |
| f"TimeoutExpired raised after {elapsed:.2f}s; expected ~{timeout}s. " | |
| "Stdin writing blocked without checking timeout.") | |
| # Release the slow reader so it stops blocking and drains stdin. | |
| conn.sendall(b'go') | |
| conn.close() | |
| conn = None | |
| # 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: | |
| if conn is not None: | |
| conn.close() | |
| if server is not None: | |
| server.close() | |
| p.kill() | |
| p.wait() | |
| def test_communicate_timeout_resume_partial_write(self): | |
| """Resume writing input after a partial-write TimeoutExpired. | |
| Exercises the _input_offset bookkeeping across the | |
| _communicate_io_posix factoring: a first communicate() must time out | |
| mid-write, and a subsequent communicate() must finish delivering the | |
| remaining bytes so the child receives the full input intact. | |
| """ | |
| # 1 MiB easily exceeds typical pipe buffers (~64 KiB) so writing | |
| # blocks once the buffer fills before the child starts reading. | |
| input_data = bytes(range(256)) * 4096 # 1 MiB, distinctive pattern | |
| self.assertEqual(len(input_data), 1024 * 1024) | |
| p = subprocess.Popen( | |
| [sys.executable, "-c", | |
| "import sys, time; " | |
| "time.sleep(0.5); " | |
| "sys.stdout.buffer.write(sys.stdin.buffer.read())"], | |
| stdin=subprocess.PIPE, | |
| stdout=subprocess.PIPE, | |
| stderr=subprocess.PIPE) | |
| try: | |
| with self.assertRaises(subprocess.TimeoutExpired): | |
| p.communicate(input_data, timeout=0.05) | |
| # Resume: no new input, generous timeout to avoid CI flakes. | |
| stdout, stderr = p.communicate(timeout=support.LONG_TIMEOUT) | |
| self.assertEqual(len(stdout), len(input_data)) | |
| 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): | |
| for stdout_pipe in (False, True): | |
| for stderr_pipe in (False, True): | |
| options = {} | |
| if stdin_pipe: | |
| options['stdin'] = subprocess.PIPE | |
| if stdout_pipe: | |
| options['stdout'] = subprocess.PIPE | |
| if stderr_pipe: | |
| options['stderr'] = subprocess.PIPE | |
| if not options: | |
| continue | |
| p = subprocess.Popen(ZERO_RETURN_CMD, **options) | |
| p.communicate() | |
| if p.stdin is not None: | |
| self.assertTrue(p.stdin.closed) | |
| if p.stdout is not None: | |
| self.assertTrue(p.stdout.closed) | |
| if p.stderr is not None: | |
| self.assertTrue(p.stderr.closed) | |
| def test_communicate_returns(self): | |
| # communicate() should return None if no redirection is active | |
| p = subprocess.Popen([sys.executable, "-c", | |
| "import sys; sys.exit(47)"]) | |
| (stdout, stderr) = p.communicate() | |
| self.assertEqual(stdout, None) | |
| self.assertEqual(stderr, None) | |
| def test_communicate_pipe_buf(self): | |
| # communicate() with writes larger than pipe_buf | |
| # This test will probably deadlock rather than fail, if | |
| # communicate() does not work properly. | |
| x, y = os.pipe() | |
| os.close(x) | |
| os.close(y) | |
| p = subprocess.Popen([sys.executable, "-c", | |
| 'import sys,os;' | |
| 'sys.stdout.write(sys.stdin.read(47));' | |
| 'sys.stderr.write("x" * %d);' | |
| 'sys.stdout.write(sys.stdin.read())' % | |
| support.PIPE_MAX_SIZE], | |
| stdin=subprocess.PIPE, | |
| stdout=subprocess.PIPE, | |
| stderr=subprocess.PIPE) | |
| self.addCleanup(p.stdout.close) | |
| self.addCleanup(p.stderr.close) | |
| self.addCleanup(p.stdin.close) | |
| string_to_write = b"a" * support.PIPE_MAX_SIZE | |
| (stdout, stderr) = p.communicate(string_to_write) | |
| self.assertEqual(stdout, string_to_write) | |
| def test_writes_before_communicate(self): | |
| # stdin.write before communicate() | |
| p = subprocess.Popen([sys.executable, "-c", | |
| 'import sys,os;' | |
| 'sys.stdout.write(sys.stdin.read())'], | |
| stdin=subprocess.PIPE, | |
| stdout=subprocess.PIPE, | |
| stderr=subprocess.PIPE) | |
| self.addCleanup(p.stdout.close) | |
| self.addCleanup(p.stderr.close) | |
| self.addCleanup(p.stdin.close) | |
| p.stdin.write(b"banana") | |
| (stdout, stderr) = p.communicate(b"split") | |
| 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", | |
| 'import sys,os;' + SETBINARY + | |
| 'buf = sys.stdout.buffer;' | |
| 'buf.write(sys.stdin.readline().encode());' | |
| 'buf.flush();' | |
| 'buf.write(b"line2\\n");' | |
| 'buf.flush();' | |
| 'buf.write(sys.stdin.read().encode());' | |
| 'buf.flush();' | |
| 'buf.write(b"line4\\n");' | |
| 'buf.flush();' | |
| 'buf.write(b"line5\\r\\n");' | |
| 'buf.flush();' | |
| 'buf.write(b"line6\\r");' | |
| 'buf.flush();' | |
| 'buf.write(b"\\nline7");' | |
| 'buf.flush();' | |
| 'buf.write(b"\\nline8");'] | |
| for extra_kwarg in ('universal_newlines', 'text'): | |
| p = subprocess.Popen(args, **{'stdin': subprocess.PIPE, | |
| 'stdout': subprocess.PIPE, | |
| extra_kwarg: True}) | |
| with p: | |
| p.stdin.write("line1\n") | |
| p.stdin.flush() | |
| self.assertEqual(p.stdout.readline(), "line1\n") | |
| p.stdin.write("line3\n") | |
| p.stdin.close() | |
| self.addCleanup(p.stdout.close) | |
| self.assertEqual(p.stdout.readline(), | |
| "line2\n") | |
| self.assertEqual(p.stdout.read(6), | |
| "line3\n") | |
| self.assertEqual(p.stdout.read(), | |
| "line4\nline5\nline6\nline7\nline8") | |
| def test_universal_newlines_communicate(self): | |
| # universal newlines through communicate() | |
| p = subprocess.Popen([sys.executable, "-c", | |
| 'import sys,os;' + SETBINARY + | |
| 'buf = sys.stdout.buffer;' | |
| 'buf.write(b"line2\\n");' | |
| 'buf.flush();' | |
| 'buf.write(b"line4\\n");' | |
| 'buf.flush();' | |
| 'buf.write(b"line5\\r\\n");' | |
| 'buf.flush();' | |
| 'buf.write(b"line6\\r");' | |
| 'buf.flush();' | |
| 'buf.write(b"\\nline7");' | |
| 'buf.flush();' | |
| 'buf.write(b"\\nline8");'], | |
| stderr=subprocess.PIPE, | |
| stdout=subprocess.PIPE, | |
| universal_newlines=1) | |
| self.addCleanup(p.stdout.close) | |
| self.addCleanup(p.stderr.close) | |
| (stdout, stderr) = p.communicate() | |
| self.assertEqual(stdout, | |
| "line2\nline4\nline5\nline6\nline7\nline8") | |
| def test_universal_newlines_communicate_stdin(self): | |
| # universal newlines through communicate(), with only stdin | |
| p = subprocess.Popen([sys.executable, "-c", | |
| 'import sys,os;' + SETBINARY + textwrap.dedent(''' | |
| s = sys.stdin.readline() | |
| assert s == "line1\\n", repr(s) | |
| s = sys.stdin.read() | |
| assert s == "line3\\n", repr(s) | |
| ''')], | |
| stdin=subprocess.PIPE, | |
| universal_newlines=1) | |
| (stdout, stderr) = p.communicate("line1\nline3\n") | |
| self.assertEqual(p.returncode, 0) | |
| def test_universal_newlines_communicate_input_none(self): | |
| # Test communicate(input=None) with universal newlines. | |
| # | |
| # We set stdout to PIPE because, as of this writing, a different | |
| # code path is tested when the number of pipes is zero or one. | |
| p = subprocess.Popen(ZERO_RETURN_CMD, | |
| stdin=subprocess.PIPE, | |
| stdout=subprocess.PIPE, | |
| universal_newlines=True) | |
| p.communicate() | |
| self.assertEqual(p.returncode, 0) | |
| def test_universal_newlines_communicate_stdin_stdout_stderr(self): | |
| # universal newlines through communicate(), with stdin, stdout, stderr | |
| p = subprocess.Popen([sys.executable, "-c", | |
| 'import sys,os;' + SETBINARY + textwrap.dedent(''' | |
| s = sys.stdin.buffer.readline() | |
| sys.stdout.buffer.write(s) | |
| sys.stdout.buffer.write(b"line2\\r") | |
| sys.stderr.buffer.write(b"eline2\\n") | |
| s = sys.stdin.buffer.read() | |
| sys.stdout.buffer.write(s) | |
| sys.stdout.buffer.write(b"line4\\n") | |
| sys.stdout.buffer.write(b"line5\\r\\n") | |
| sys.stderr.buffer.write(b"eline6\\r") | |
| sys.stderr.buffer.write(b"eline7\\r\\nz") | |
| ''')], | |
| stdin=subprocess.PIPE, | |
| stderr=subprocess.PIPE, | |
| stdout=subprocess.PIPE, | |
| universal_newlines=True) | |
| self.addCleanup(p.stdout.close) | |
| self.addCleanup(p.stderr.close) | |
| (stdout, stderr) = p.communicate("line1\nline3\n") | |
| self.assertEqual(p.returncode, 0) | |
| self.assertEqual("line1\nline2\nline3\nline4\nline5\n", stdout) | |
| # Python debug build push something like "[42442 refs]\n" | |
| # to stderr at exit of subprocess. | |
| self.assertStartsWith(stderr, "eline2\neline6\neline7\n") | |
| def test_universal_newlines_communicate_encodings(self): | |
| # Check that universal newlines mode works for various encodings, | |
| # in particular for encodings in the UTF-16 and UTF-32 families. | |
| # See issue #15595. | |
| # | |
| # UTF-16 and UTF-32-BE are sufficient to check both with BOM and | |
| # without, and UTF-16 and UTF-32. | |
| for encoding in ['utf-16', 'utf-32-be']: | |
| code = ("import sys; " | |
| r"sys.stdout.buffer.write('1\r\n2\r3\n4'.encode('%s'))" % | |
| encoding) | |
| args = [sys.executable, '-c', code] | |
| # We set stdin to be non-None because, as of this writing, | |
| # a different code path is used when the number of pipes is | |
| # zero or one. | |
| popen = subprocess.Popen(args, | |
| stdin=subprocess.PIPE, | |
| stdout=subprocess.PIPE, | |
| encoding=encoding) | |
| stdout, stderr = popen.communicate(input='') | |
| self.assertEqual(stdout, '1\n2\n3\n4') | |
| def test_communicate_errors(self): | |
| for errors, expected in [ | |
| ('ignore', ''), | |
| ('replace', '\ufffd\ufffd'), | |
| ('surrogateescape', '\udc80\udc80'), | |
| ('backslashreplace', '\\x80\\x80'), | |
| ]: | |
| code = ("import sys; " | |
| r"sys.stdout.buffer.write(b'[\x80\x80]')") | |
| args = [sys.executable, '-c', code] | |
| # We set stdin to be non-None because, as of this writing, | |
| # a different code path is used when the number of pipes is | |
| # zero or one. | |
| popen = subprocess.Popen(args, | |
| stdin=subprocess.PIPE, | |
| stdout=subprocess.PIPE, | |
| encoding='utf-8', | |
| errors=errors) | |
| stdout, stderr = popen.communicate(input='') | |
| self.assertEqual(stdout, '[{}]'.format(expected)) | |
| def test_no_leaking(self): | |
| # Make sure we leak no resources | |
| if not mswindows: | |
| 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: | |
| for i in range(max_handles): | |
| try: | |
| tmpfile = os.path.join(tmpdir, os_helper.TESTFN) | |
| handles.append(os.open(tmpfile, os.O_WRONLY|os.O_CREAT)) | |
| except OSError as e: | |
| if e.errno != errno.EMFILE: | |
| raise | |
| break | |
| 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 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, | |
| # the next loop iteration will fail by reaching the max fd limit. | |
| for i in range(15): | |
| p = subprocess.Popen([sys.executable, "-c", | |
| "import sys;" | |
| "sys.stdout.write(sys.stdin.read())"], | |
| stdin=subprocess.PIPE, | |
| stdout=subprocess.PIPE, | |
| stderr=subprocess.PIPE) | |
| data = p.communicate(b"lime")[0] | |
| self.assertEqual(data, b"lime") | |
| finally: | |
| for h in handles: | |
| os.close(h) | |
| shutil.rmtree(tmpdir) | |
| def test_list2cmdline(self): | |
| self.assertEqual(subprocess.list2cmdline(['a b c', 'd', 'e']), | |
| '"a b c" d e') | |
| self.assertEqual(subprocess.list2cmdline(['ab"c', '\\', 'd']), | |
| 'ab\\"c \\ d') | |
| self.assertEqual(subprocess.list2cmdline(['ab"c', ' \\', 'd']), | |
| 'ab\\"c " \\\\" d') | |
| self.assertEqual(subprocess.list2cmdline(['a\\\\\\b', 'de fg', 'h']), | |
| 'a\\\\\\b "de fg" h') | |
| self.assertEqual(subprocess.list2cmdline(['a\\"b', 'c', 'd']), | |
| 'a\\\\\\"b c d') | |
| self.assertEqual(subprocess.list2cmdline(['a\\\\b c', 'd', 'e']), | |
| '"a\\\\b c" d e') | |
| self.assertEqual(subprocess.list2cmdline(['a\\\\b\\ c', 'd', 'e']), | |
| '"a\\\\b\\ c" d e') | |
| self.assertEqual(subprocess.list2cmdline(['ab', '']), | |
| 'ab ""') | |
| def test_poll(self): | |
| p = subprocess.Popen([sys.executable, "-c", | |
| "import os; os.read(0, 1)"], | |
| stdin=subprocess.PIPE) | |
| self.addCleanup(p.stdin.close) | |
| self.assertIsNone(p.poll()) | |
| os.write(p.stdin.fileno(), b'A') | |
| p.wait() | |
| # Subsequent invocations should just return the returncode | |
| self.assertEqual(p.poll(), 0) | |
| def test_wait(self): | |
| p = subprocess.Popen(ZERO_RETURN_CMD) | |
| self.assertEqual(p.wait(), 0) | |
| # Subsequent invocations should just return the returncode | |
| self.assertEqual(p.wait(), 0) | |
| def test_wait_timeout(self): | |
| p = subprocess.Popen([sys.executable, | |
| "-c", "import time; time.sleep(0.3)"]) | |
| with self.assertRaises(subprocess.TimeoutExpired) as c: | |
| p.wait(timeout=0) | |
| with self.assertRaises(subprocess.TimeoutExpired) as c: | |
| p.wait(timeout=0.0001) | |
| self.assertIn("0.0001", str(c.exception)) # For coverage of __str__. | |
| self.assertEqual(p.wait(timeout=support.SHORT_TIMEOUT), 0) | |
| def test_invalid_bufsize(self): | |
| # an invalid type of the bufsize argument should raise | |
| # TypeError. | |
| with self.assertRaises(TypeError): | |
| subprocess.Popen(ZERO_RETURN_CMD, "orange") | |
| def test_bufsize_is_none(self): | |
| # bufsize=None should be the same as bufsize=0. | |
| p = subprocess.Popen(ZERO_RETURN_CMD, None) | |
| self.assertEqual(p.wait(), 0) | |
| # Again with keyword arg | |
| p = subprocess.Popen(ZERO_RETURN_CMD, bufsize=None) | |
| self.assertEqual(p.wait(), 0) | |
| def _test_bufsize_equal_one(self, line, expected, universal_newlines): | |
| # subprocess may deadlock with bufsize=1, see issue #21332 | |
| 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: | |
| p.stdin.write(line) # expect that it flushes the line in text mode | |
| os.close(p.stdin.fileno()) # close it without flushing the buffer | |
| read_line = p.stdout.readline() | |
| with support.SuppressCrashReport(): | |
| try: | |
| p.stdin.close() | |
| except OSError: | |
| pass | |
| p.stdin = None | |
| self.assertEqual(p.returncode, 0) | |
| self.assertEqual(read_line, expected) | |
| def test_bufsize_equal_one_text_mode(self): | |
| # line is flushed in text mode with bufsize=1. | |
| # we should get the full line in return | |
| line = "line\n" | |
| self._test_bufsize_equal_one(line, line, universal_newlines=True) | |
| def test_bufsize_equal_one_binary_mode(self): | |
| # line is not flushed in binary mode with bufsize=1. | |
| # we should get empty response | |
| line = b'line' + os.linesep.encode() # assume ascii-based locale | |
| with self.assertWarnsRegex(RuntimeWarning, 'line buffering'): | |
| self._test_bufsize_equal_one(line, b'', universal_newlines=False) | |
| @support.requires_resource('cpu') | |
| 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 | |
| # the maximum number of open fds. 1024 seems a very common | |
| # value for that limit, but Windows has 2048, so we loop | |
| # 1024 times (each call leaked two fds). | |
| for i in range(1024): | |
| with self.assertRaises(NONEXISTING_ERRORS): | |
| subprocess.Popen(NONEXISTING_CMD, | |
| stdout=subprocess.PIPE, | |
| stderr=subprocess.PIPE) | |
| def test_nonexisting_with_pipes(self): | |
| # bpo-30121: Popen with pipes must close properly pipes on error. | |
| # Previously, os.close() was called with a Windows handle which is not | |
| # a valid file descriptor. | |
| # | |
| # Run the test in a subprocess to control how the CRT reports errors | |
| # and to get stderr content. | |
| try: | |
| import msvcrt | |
| msvcrt.CrtSetReportMode | |
| except (AttributeError, ImportError): | |
| self.skipTest("need msvcrt.CrtSetReportMode") | |
| code = textwrap.dedent(f""" | |
| import msvcrt | |
| import subprocess | |
| cmd = {NONEXISTING_CMD!r} | |
| for report_type in [msvcrt.CRT_WARN, | |
| msvcrt.CRT_ERROR, | |
| msvcrt.CRT_ASSERT]: | |
| msvcrt.CrtSetReportMode(report_type, msvcrt.CRTDBG_MODE_FILE) | |
| msvcrt.CrtSetReportFile(report_type, msvcrt.CRTDBG_FILE_STDERR) | |
| try: | |
| subprocess.Popen(cmd, | |
| stdout=subprocess.PIPE, | |
| stderr=subprocess.PIPE) | |
| except OSError: | |
| pass | |
| """) | |
| cmd = [sys.executable, "-c", code] | |
| proc = subprocess.Popen(cmd, | |
| stderr=subprocess.PIPE, | |
| universal_newlines=True) | |
| with proc: | |
| stderr = proc.communicate()[1] | |
| self.assertEqual(stderr, "") | |
| self.assertEqual(proc.returncode, 0) | |
| def test_double_close_on_error(self): | |
| # Issue #18851 | |
| fds = [] | |
| def open_fds(): | |
| for i in range(20): | |
| fds.extend(os.pipe()) | |
| time.sleep(0.001) | |
| t = threading.Thread(target=open_fds) | |
| t.start() | |
| try: | |
| with self.assertRaises(OSError): | |
| subprocess.Popen(NONEXISTING_CMD, | |
| stdin=subprocess.PIPE, | |
| stdout=subprocess.PIPE, | |
| stderr=subprocess.PIPE) | |
| finally: | |
| t.join() | |
| exc = None | |
| for fd in fds: | |
| # If a double close occurred, some of those fds will | |
| # already have been closed by mistake, and os.close() | |
| # here will raise. | |
| try: | |
| os.close(fd) | |
| except OSError as e: | |
| exc = e | |
| if exc is not None: | |
| raise exc | |
| def test_threadsafe_wait(self): | |
| """Issue21291: Popen.wait() needs to be threadsafe for returncode.""" | |
| proc = subprocess.Popen([sys.executable, '-c', | |
| 'import time; time.sleep(12)']) | |
| self.assertEqual(proc.returncode, None) | |
| results = [] | |
| def kill_proc_timer_thread(): | |
| results.append(('thread-start-poll-result', proc.poll())) | |
| # terminate it from the thread and wait for the result. | |
| proc.kill() | |
| proc.wait() | |
| results.append(('thread-after-kill-and-wait', proc.returncode)) | |
| # this wait should be a no-op given the above. | |
| proc.wait() | |
| results.append(('thread-after-second-wait', proc.returncode)) | |
| # This is a timing sensitive test, the failure mode is | |
| # triggered when both the main thread and this thread are in | |
| # the wait() call at once. The delay here is to allow the | |
| # main thread to most likely be blocked in its wait() call. | |
| t = threading.Timer(0.2, kill_proc_timer_thread) | |
| t.start() | |
| if mswindows: | |
| expected_errorcode = 1 | |
| else: | |
| # Should be -9 because of the proc.kill() from the thread. | |
| expected_errorcode = -9 | |
| # Wait for the process to finish; the thread should kill it | |
| # long before it finishes on its own. Supplying a timeout | |
| # triggers a different code path for better coverage. | |
| proc.wait(timeout=support.SHORT_TIMEOUT) | |
| self.assertEqual(proc.returncode, expected_errorcode, | |
| msg="unexpected result in wait from main thread") | |
| # This should be a no-op with no change in returncode. | |
| proc.wait() | |
| self.assertEqual(proc.returncode, expected_errorcode, | |
| msg="unexpected result in second main wait.") | |
| t.join() | |
| # Ensure that all of the thread results are as expected. | |
| # When a race condition occurs in wait(), the returncode could | |
| # be set by the wrong thread that doesn't actually have it | |
| # leading to an incorrect value. | |
| self.assertEqual([('thread-start-poll-result', None), | |
| ('thread-after-kill-and-wait', expected_errorcode), | |
| ('thread-after-second-wait', expected_errorcode)], | |
| results) | |
| def test_issue8780(self): | |
| # Ensure that stdout is inherited from the parent | |
| # if stdout=PIPE is not used | |
| code = ';'.join(( | |
| 'import subprocess, sys', | |
| 'retcode = subprocess.call(' | |
| "[sys.executable, '-c', 'print(\"Hello World!\")'])", | |
| 'assert retcode == 0')) | |
| output = subprocess.check_output([sys.executable, '-c', code]) | |
| self.assertStartsWith(output, b'Hello World!') | |
| def test_handles_closed_on_exception(self): | |
| # If CreateProcess exits with an error, ensure the | |
| # duplicate output handles are released | |
| ifhandle, ifname = tempfile.mkstemp() | |
| ofhandle, ofname = tempfile.mkstemp() | |
| efhandle, efname = tempfile.mkstemp() | |
| try: | |
| subprocess.Popen (["*"], stdin=ifhandle, stdout=ofhandle, | |
| stderr=efhandle) | |
| except OSError: | |
| os.close(ifhandle) | |
| os.remove(ifname) | |
| os.close(ofhandle) | |
| os.remove(ofname) | |
| os.close(efhandle) | |
| os.remove(efname) | |
| self.assertFalse(os.path.exists(ifname)) | |
| self.assertFalse(os.path.exists(ofname)) | |
| self.assertFalse(os.path.exists(efname)) | |
| def test_communicate_epipe(self): | |
| # Issue 10963: communicate() should hide EPIPE | |
| p = subprocess.Popen(ZERO_RETURN_CMD, | |
| stdin=subprocess.PIPE, | |
| stdout=subprocess.PIPE, | |
| stderr=subprocess.PIPE) | |
| self.addCleanup(p.stdout.close) | |
| self.addCleanup(p.stderr.close) | |
| self.addCleanup(p.stdin.close) | |
| p.communicate(b"x" * 2**20) | |
| def test_repr(self): | |
| cases = [ | |
| ("ls", True, 123, "<Popen: returncode: 123 args: 'ls'>"), | |
| ('a' * 100, True, 0, | |
| "<Popen: returncode: 0 args: 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa...>"), | |
| (["ls"], False, None, "<Popen: returncode: None args: ['ls']>"), | |
| (["ls", '--my-opts', 'a' * 100], False, None, | |
| "<Popen: returncode: None args: ['ls', '--my-opts', 'aaaaaaaaaaaaaaaaaaaaaaaa...>"), | |
| (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: | |
| p = subprocess.Popen(cmd, shell=shell) | |
| p.returncode = code | |
| self.assertEqual(repr(p), sx) | |
| def test_communicate_epipe_only_stdin(self): | |
| # Issue 10963: communicate() should hide EPIPE | |
| p = subprocess.Popen(ZERO_RETURN_CMD, | |
| stdin=subprocess.PIPE) | |
| self.addCleanup(p.stdin.close) | |
| p.wait() | |
| p.communicate(b"x" * 2**20) | |
| @unittest.skipUnless(hasattr(signal, 'SIGUSR1'), | |
| "Requires signal.SIGUSR1") | |
| @unittest.skipUnless(hasattr(os, 'kill'), | |
| "Requires os.kill") | |
| @unittest.skipUnless(hasattr(os, 'getppid'), | |
| "Requires os.getppid") | |
| def test_communicate_eintr(self): | |
| # Issue #12493: communicate() should handle EINTR | |
| def handler(signum, frame): | |
| pass | |
| old_handler = signal.signal(signal.SIGUSR1, handler) | |
| self.addCleanup(signal.signal, signal.SIGUSR1, old_handler) | |
| args = [sys.executable, "-c", | |
| 'import os, signal;' | |
| 'os.kill(os.getppid(), signal.SIGUSR1)'] | |
| for stream in ('stdout', 'stderr'): | |
| kw = {stream: subprocess.PIPE} | |
| with subprocess.Popen(args, **kw) as process: | |
| # communicate() will be interrupted by SIGUSR1 | |
| process.communicate() | |
| # This test is Linux-ish specific for simplicity to at least have | |
| # some coverage. It is not a platform specific bug. | |
| @unittest.skipUnless(os.path.isdir('/proc/%d/fd' % os.getpid()), | |
| "Linux specific") | |
| def test_failed_child_execute_fd_leak(self): | |
| """Test for the fork() failure fd leak reported in issue16327.""" | |
| fd_directory = '/proc/%d/fd' % os.getpid() | |
| fds_before_popen = os.listdir(fd_directory) | |
| with self.assertRaises(PopenTestException): | |
| PopenExecuteChildRaises( | |
| ZERO_RETURN_CMD, stdin=subprocess.PIPE, | |
| stdout=subprocess.PIPE, stderr=subprocess.PIPE) | |
| # NOTE: This test doesn't verify that the real _execute_child | |
| # does not close the file descriptors itself on the way out | |
| # during an exception. Code inspection has confirmed that. | |
| fds_after_exception = os.listdir(fd_directory) | |
| self.assertEqual(fds_before_popen, fds_after_exception) | |
| def test_file_not_found_includes_filename(self): | |
| missing = (r'C:\opt\nonexistent_binary' if mswindows | |
| else '/opt/nonexistent_binary') | |
| with self.assertRaises(FileNotFoundError) as c: | |
| subprocess.call([missing, 'with', 'some', 'args']) | |
| self.assertEqual(c.exception.filename, missing) | |
| def test_args_filter_iterable(self): | |
| # gh-119646: Windows used to index args[0] before list2cmdline. | |
| # test_faulthandler.test_sys_xoptions passes a filter() object. | |
| args = filter(None, (sys.executable, "-c", "import sys; sys.exit(17)")) | |
| self.assertEqual(subprocess.call(args), 17) | |
| def test_file_not_found_includes_filename_from_iterable(self): | |
| missing = (r'C:\opt\nonexistent_binary' if mswindows | |
| else '/opt/nonexistent_binary') | |
| args = filter(None, (missing, "with", "some", "args")) | |
| with self.assertRaises(FileNotFoundError) as c: | |
| subprocess.call(args) | |
| self.assertEqual(c.exception.filename, missing) | |
| @unittest.skipIf(mswindows, "Windows reports NotADirectoryError (WinError 267)") | |
| def test_file_not_found_with_bad_cwd(self): | |
| with self.assertRaises(FileNotFoundError) as c: | |
| subprocess.Popen(['exit', '0'], cwd='/some/nonexistent/directory') | |
| self.assertEqual(c.exception.filename, '/some/nonexistent/directory') | |
| def test_class_getitems(self): | |
| self.assertIsInstance(subprocess.Popen[bytes], types.GenericAlias) | |
| self.assertIsInstance(subprocess.CompletedProcess[str], types.GenericAlias) | |
| @unittest.skipUnless(hasattr(subprocess, '_winapi'), | |
| 'need subprocess._winapi') | |
| def test_wait_negative_timeout(self): | |
| proc = subprocess.Popen(ZERO_RETURN_CMD) | |
| with proc: | |
| patch = mock.patch.object( | |
| subprocess._winapi, | |
| 'WaitForSingleObject', | |
| return_value=subprocess._winapi.WAIT_OBJECT_0) | |
| with patch as mock_wait: | |
| proc.wait(-1) # negative timeout | |
| mock_wait.assert_called_once_with(proc._handle, 0) | |
| proc.returncode = None | |
| 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): | |
| """Run Python code in a subprocess using subprocess.run""" | |
| argv = [sys.executable, "-c", code] | |
| return subprocess.run(argv, **kwargs) | |
| def test_returncode(self): | |
| # call() function with sequence argument | |
| cp = self.run_python("import sys; sys.exit(47)") | |
| self.assertEqual(cp.returncode, 47) | |
| with self.assertRaises(subprocess.CalledProcessError): | |
| cp.check_returncode() | |
| def test_check(self): | |
| with self.assertRaises(subprocess.CalledProcessError) as c: | |
| self.run_python("import sys; sys.exit(47)", check=True) | |
| self.assertEqual(c.exception.returncode, 47) | |
| def test_check_zero(self): | |
| # check_returncode shouldn't raise when returncode is zero | |
| cp = subprocess.run(ZERO_RETURN_CMD, check=True) | |
| self.assertEqual(cp.returncode, 0) | |
| def test_timeout(self): | |
| # run() function with timeout argument; we want to test that the child | |
| # process gets killed when the timeout expires. If the child isn't | |
| # killed, this call will deadlock since subprocess.run waits for the | |
| # child. | |
| with self.assertRaises(subprocess.TimeoutExpired): | |
| self.run_python("while True: pass", timeout=0.0001) | |
| def test_capture_stdout(self): | |
| # capture stdout with zero return code | |
| cp = self.run_python("print('BDFL')", stdout=subprocess.PIPE) | |
| self.assertIn(b'BDFL', cp.stdout) | |
| def test_capture_stderr(self): | |
| cp = self.run_python("import sys; sys.stderr.write('BDFL')", | |
| stderr=subprocess.PIPE) | |
| self.assertIn(b'BDFL', cp.stderr) | |
| def test_check_output_stdin_arg(self): | |
| # run() can be called with stdin set to a file | |
| tf = tempfile.TemporaryFile() | |
| self.addCleanup(tf.close) | |
| tf.write(b'pear') | |
| tf.seek(0) | |
| cp = self.run_python( | |
| "import sys; sys.stdout.write(sys.stdin.read().upper())", | |
| stdin=tf, stdout=subprocess.PIPE) | |
| self.assertIn(b'PEAR', cp.stdout) | |
| def test_check_output_input_arg(self): | |
| # check_output() can be called with input set to a string | |
| cp = self.run_python( | |
| "import sys; sys.stdout.write(sys.stdin.read().upper())", | |
| input=b'pear', stdout=subprocess.PIPE) | |
| self.assertIn(b'PEAR', cp.stdout) | |
| def test_check_output_stdin_with_input_arg(self): | |
| # run() refuses to accept 'stdin' with 'input' | |
| tf = tempfile.TemporaryFile() | |
| self.addCleanup(tf.close) | |
| tf.write(b'pear') | |
| tf.seek(0) | |
| with self.assertRaises(ValueError, | |
| msg="Expected ValueError when stdin and input args supplied.") as c: | |
| output = self.run_python("print('will not be run')", | |
| stdin=tf, input=b'hare') | |
| self.assertIn('stdin', c.exception.args[0]) | |
| self.assertIn('input', c.exception.args[0]) | |
| def test_check_output_timeout(self): | |
| with self.assertRaises(subprocess.TimeoutExpired) as c: | |
| cp = self.run_python( | |
| "import time; time.sleep(3600)", | |
| timeout=0.1, stdout=subprocess.PIPE) | |
| def test_run_kwargs(self): | |
| newenv = os.environ.copy() | |
| newenv["FRUIT"] = "banana" | |
| cp = self.run_python(('import sys, os;' | |
| 'sys.exit(33 if os.getenv("FRUIT")=="banana" else 31)'), | |
| env=newenv) | |
| self.assertEqual(cp.returncode, 33) | |
| def test_run_with_pathlike_path(self): | |
| # bpo-31961: test run(pathlike_object) | |
| # the name of a command that can be run without | |
| # any arguments that exit fast | |
| prog = 'tree.com' if mswindows else 'ls' | |
| path = shutil.which(prog) | |
| if path is None: | |
| self.skipTest(f'{prog} required for this test') | |
| path = FakePath(path) | |
| res = subprocess.run(path, stdout=subprocess.DEVNULL) | |
| self.assertEqual(res.returncode, 0) | |
| with self.assertRaises(TypeError): | |
| subprocess.run(path, stdout=subprocess.DEVNULL, shell=True) | |
| def test_run_with_bytes_path_and_arguments(self): | |
| # bpo-31961: test run([bytes_object, b'additional arguments']) | |
| path = os.fsencode(sys.executable) | |
| args = [path, '-c', b'import sys; sys.exit(57)'] | |
| res = subprocess.run(args) | |
| self.assertEqual(res.returncode, 57) | |
| def test_run_with_pathlike_path_and_arguments(self): | |
| # bpo-31961: test run([pathlike_object, 'additional arguments']) | |
| path = FakePath(sys.executable) | |
| args = [path, '-c', 'import sys; sys.exit(57)'] | |
| res = subprocess.run(args) | |
| self.assertEqual(res.returncode, 57) | |
| @unittest.skipUnless(mswindows, "Maybe test trigger a leak on Ubuntu") | |
| def test_run_with_an_empty_env(self): | |
| # gh-105436: fix subprocess.run(..., env={}) broken on Windows | |
| args = [sys.executable, "-c", 'pass'] | |
| # Ignore subprocess errors - we only care that the API doesn't | |
| # raise an OSError | |
| subprocess.run(args, env={}) | |
| def test_capture_output(self): | |
| cp = self.run_python(("import sys;" | |
| "sys.stdout.write('BDFL'); " | |
| "sys.stderr.write('FLUFL')"), | |
| capture_output=True) | |
| 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() | |
| self.addCleanup(tf.close) | |
| with self.assertRaises(ValueError, | |
| msg=("Expected ValueError when stdout and capture_output " | |
| "args supplied.")) as c: | |
| output = self.run_python("print('will not be run')", | |
| capture_output=True, stdout=tf) | |
| self.assertIn('stdout', c.exception.args[0]) | |
| self.assertIn('capture_output', c.exception.args[0]) | |
| def test_stderr_with_capture_output_arg(self): | |
| # run() refuses to accept 'stderr' with 'capture_output' | |
| tf = tempfile.TemporaryFile() | |
| self.addCleanup(tf.close) | |
| with self.assertRaises(ValueError, | |
| msg=("Expected ValueError when stderr and capture_output " | |
| "args supplied.")) as c: | |
| output = self.run_python("print('will not be run')", | |
| capture_output=True, stderr=tf) | |
| self.assertIn('stderr', c.exception.args[0]) | |
| self.assertIn('capture_output', c.exception.args[0]) | |
| # This test _might_ wind up a bit fragile on loaded build+test machines | |
| # as it depends on the timing with wide enough margins for normal situations | |
| # but does assert that it happened "soon enough" to believe the right thing | |
| # happened. | |
| @unittest.skipIf(mswindows, "requires posix like 'sleep' shell command") | |
| def test_run_with_shell_timeout_and_capture_output(self): | |
| """Output capturing after a timeout mustn't hang forever on open filehandles.""" | |
| before_secs = time.monotonic() | |
| try: | |
| subprocess.run('sleep 3', shell=True, timeout=0.1, | |
| capture_output=True) # New session unspecified. | |
| except subprocess.TimeoutExpired as exc: | |
| after_secs = time.monotonic() | |
| stacks = traceback.format_exc() # assertRaises doesn't give this. | |
| else: | |
| self.fail("TimeoutExpired not raised.") | |
| self.assertLess(after_secs - before_secs, 1.5, | |
| msg="TimeoutExpired was delayed! Bad traceback:\n```\n" | |
| f"{stacks}```") | |
| def test_encoding_warning(self): | |
| code = textwrap.dedent("""\ | |
| from subprocess import * | |
| run("echo hello", shell=True, text=True) | |
| check_output("echo hello", shell=True, text=True) | |
| """) | |
| env = support.make_clean_env() | |
| cp = subprocess.run([sys.executable, "-Xwarn_default_encoding", "-c", code], | |
| capture_output=True, env=env) | |
| lines = cp.stderr.splitlines() | |
| self.assertEqual(len(lines), 2, lines) | |
| self.assertStartsWith(lines[0], b"<string>:2: EncodingWarning: ") | |
| self.assertStartsWith(lines[1], b"<string>:3: EncodingWarning: ") | |
| def _get_test_grp_name(): | |
| for name_group in ('staff', 'nogroup', 'grp', 'nobody', 'nfsnobody'): | |
| if grp: | |
| try: | |
| grp.getgrnam(name_group) | |
| except KeyError: | |
| continue | |
| return name_group | |
| else: | |
| raise unittest.SkipTest('No identified group name to use for this test on this platform.') | |
| @unittest.skipIf(mswindows, "POSIX specific tests") | |
| class POSIXProcessTestCase(BaseTestCase): | |
| def setUp(self): | |
| super().setUp() | |
| self._nonexistent_dir = "/_this/pa.th/does/not/exist" | |
| def _get_chdir_exception(self): | |
| try: | |
| os.chdir(self._nonexistent_dir) | |
| except OSError as e: | |
| # This avoids hard coding the errno value or the OS perror() | |
| # string and instead capture the exception that we want to see | |
| # below for comparison. | |
| desired_exception = e | |
| else: | |
| self.fail("chdir to nonexistent directory %s succeeded." % | |
| self._nonexistent_dir) | |
| return desired_exception | |
| def test_exception_cwd(self): | |
| """Test error in the child raised in the parent for a bad cwd.""" | |
| desired_exception = self._get_chdir_exception() | |
| try: | |
| p = subprocess.Popen([sys.executable, "-c", ""], | |
| cwd=self._nonexistent_dir) | |
| except OSError as e: | |
| # Test that the child process chdir failure actually makes | |
| # it up to the parent process as the correct exception. | |
| self.assertEqual(desired_exception.errno, e.errno) | |
| self.assertEqual(desired_exception.strerror, e.strerror) | |
| self.assertEqual(desired_exception.filename, e.filename) | |
| else: | |
| self.fail("Expected OSError: %s" % desired_exception) | |
| def test_exception_bad_executable(self): | |
| """Test error in the child raised in the parent for a bad executable.""" | |
| desired_exception = self._get_chdir_exception() | |
| try: | |
| p = subprocess.Popen([sys.executable, "-c", ""], | |
| executable=self._nonexistent_dir) | |
| except OSError as e: | |
| # Test that the child process exec failure actually makes | |
| # it up to the parent process as the correct exception. | |
| self.assertEqual(desired_exception.errno, e.errno) | |
| self.assertEqual(desired_exception.strerror, e.strerror) | |
| self.assertEqual(desired_exception.filename, e.filename) | |
| else: | |
| self.fail("Expected OSError: %s" % desired_exception) | |
| def test_exception_bad_args_0(self): | |
| """Test error in the child raised in the parent for a bad args[0].""" | |
| desired_exception = self._get_chdir_exception() | |
| try: | |
| p = subprocess.Popen([self._nonexistent_dir, "-c", ""]) | |
| except OSError as e: | |
| # Test that the child process exec failure actually makes | |
| # it up to the parent process as the correct exception. | |
| self.assertEqual(desired_exception.errno, e.errno) | |
| self.assertEqual(desired_exception.strerror, e.strerror) | |
| self.assertEqual(desired_exception.filename, e.filename) | |
| else: | |
| self.fail("Expected OSError: %s" % desired_exception) | |
| # We mock the __del__ method for Popen in the next two tests | |
| # because it does cleanup based on the pid returned by fork_exec | |
| # along with issuing a resource warning if it still exists. Since | |
| # we don't actually spawn a process in these tests we can forego | |
| # the destructor. An alternative would be to set _child_created to | |
| # False before the destructor is called but there is no easy way | |
| # to do that | |
| class PopenNoDestructor(subprocess.Popen): | |
| def __del__(self): | |
| pass | |
| @mock.patch("subprocess._fork_exec") | |
| def test_exception_errpipe_normal(self, fork_exec): | |
| """Test error passing done through errpipe_write in the good case""" | |
| def proper_error(*args): | |
| errpipe_write = args[13] | |
| # Write the hex for the error code EISDIR: 'is a directory' | |
| err_code = '{:x}'.format(errno.EISDIR).encode() | |
| os.write(errpipe_write, b"OSError:" + err_code + b":") | |
| return 0 | |
| fork_exec.side_effect = proper_error | |
| with mock.patch("subprocess.os.waitpid", | |
| side_effect=ChildProcessError): | |
| with self.assertRaises(IsADirectoryError): | |
| self.PopenNoDestructor(["non_existent_command"]) | |
| @mock.patch("subprocess._fork_exec") | |
| def test_exception_errpipe_bad_data(self, fork_exec): | |
| """Test error passing done through errpipe_write where its not | |
| in the expected format""" | |
| error_data = b"\xFF\x00\xDE\xAD" | |
| def bad_error(*args): | |
| errpipe_write = args[13] | |
| # Anything can be in the pipe, no assumptions should | |
| # be made about its encoding, so we'll write some | |
| # arbitrary hex bytes to test it out | |
| os.write(errpipe_write, error_data) | |
| return 0 | |
| fork_exec.side_effect = bad_error | |
| with mock.patch("subprocess.os.waitpid", | |
| side_effect=ChildProcessError): | |
| with self.assertRaises(subprocess.SubprocessError) as e: | |
| self.PopenNoDestructor(["non_existent_command"]) | |
| self.assertIn(repr(error_data), str(e.exception)) | |
| @unittest.skipIf(not os.path.exists('/proc/self/status'), | |
| "need /proc/self/status") | |
| def test_restore_signals(self): | |
| # Blindly assume that cat exists on systems with /proc/self/status... | |
| default_proc_status = subprocess.check_output( | |
| ['cat', '/proc/self/status'], | |
| restore_signals=False) | |
| for line in default_proc_status.splitlines(): | |
| if line.startswith(b'SigIgn'): | |
| default_sig_ign_mask = line | |
| break | |
| else: | |
| self.skipTest("SigIgn not found in /proc/self/status.") | |
| restored_proc_status = subprocess.check_output( | |
| ['cat', '/proc/self/status'], | |
| restore_signals=True) | |
| for line in restored_proc_status.splitlines(): | |
| if line.startswith(b'SigIgn'): | |
| restored_sig_ign_mask = line | |
| break | |
| self.assertNotEqual(default_sig_ign_mask, restored_sig_ign_mask, | |
| msg="restore_signals=True should've unblocked " | |
| "SIGPIPE and friends.") | |
| def test_start_new_session(self): | |
| # For code coverage of calling setsid(). We don't care if we get an | |
| # EPERM error from it depending on the test execution environment, that | |
| # still indicates that it was called. | |
| try: | |
| output = subprocess.check_output( | |
| [sys.executable, "-c", "import os; print(os.getsid(0))"], | |
| start_new_session=True) | |
| except PermissionError as e: | |
| if e.errno != errno.EPERM: | |
| raise # EACCES? | |
| else: | |
| parent_sid = os.getsid(0) | |
| child_sid = int(output) | |
| self.assertNotEqual(parent_sid, child_sid) | |
| @unittest.skipUnless(hasattr(os, 'setpgid') and hasattr(os, 'getpgid'), | |
| 'no setpgid or getpgid on platform') | |
| def test_process_group_0(self): | |
| # For code coverage of calling setpgid(). We don't care if we get an | |
| # EPERM error from it depending on the test execution environment, that | |
| # still indicates that it was called. | |
| try: | |
| output = subprocess.check_output( | |
| [sys.executable, "-c", "import os; print(os.getpgid(0))"], | |
| process_group=0) | |
| except PermissionError as e: | |
| if e.errno != errno.EPERM: | |
| raise # EACCES? | |
| else: | |
| parent_pgid = os.getpgid(0) | |
| child_pgid = int(output) | |
| self.assertNotEqual(parent_pgid, child_pgid) | |
| @unittest.skipUnless(hasattr(os, 'setreuid'), 'no setreuid on platform') | |
| def test_user(self): | |
| # For code coverage of the user parameter. We don't care if we get a | |
| # permission error from it depending on the test execution environment, | |
| # that still indicates that it was called. | |
| uid = os.geteuid() | |
| test_users = [65534 if uid != 65534 else 65533, uid] | |
| name_uid = "nobody" if sys.platform != 'darwin' else "unknown" | |
| if pwd is not None: | |
| try: | |
| pwd.getpwnam(name_uid) | |
| test_users.append(name_uid) | |
| except KeyError: | |
| # unknown user name | |
| name_uid = None | |
| for user in test_users: | |
| # posix_spawn() may be used with close_fds=False | |
| for close_fds in (False, True): | |
| with self.subTest(user=user, close_fds=close_fds): | |
| try: | |
| output = subprocess.check_output( | |
| [sys.executable, "-c", | |
| "import os; print(os.getuid())"], | |
| user=user, | |
| close_fds=close_fds) | |
| except PermissionError as e: # (EACCES, EPERM) | |
| if e.errno == errno.EACCES: | |
| self.assertEqual(e.filename, sys.executable) | |
| else: | |
| self.assertIsNone(e.filename) | |
| else: | |
| if isinstance(user, str): | |
| user_uid = pwd.getpwnam(user).pw_uid | |
| else: | |
| user_uid = user | |
| child_user = int(output) | |
| self.assertEqual(child_user, user_uid) | |
| with self.assertRaises(ValueError): | |
| subprocess.check_call(ZERO_RETURN_CMD, user=-1) | |
| with self.assertRaises(OverflowError): | |
| subprocess.check_call(ZERO_RETURN_CMD, | |
| cwd=os.curdir, env=os.environ, user=2**64) | |
| if pwd is None and name_uid is not None: | |
| with self.assertRaises(ValueError): | |
| subprocess.check_call(ZERO_RETURN_CMD, user=name_uid) | |
| @unittest.skipIf(hasattr(os, 'setreuid'), 'setreuid() available on platform') | |
| def test_user_error(self): | |
| with self.assertRaises(ValueError): | |
| subprocess.check_call(ZERO_RETURN_CMD, user=65535) | |
| @unittest.skipUnless(hasattr(os, 'setregid'), 'no setregid() on platform') | |
| def test_group(self): | |
| gid = os.getegid() | |
| group_list = [65534 if gid != 65534 else 65533] | |
| name_group = _get_test_grp_name() | |
| if grp is not None: | |
| group_list.append(name_group) | |
| for group in group_list + [gid]: | |
| # posix_spawn() may be used with close_fds=False | |
| for close_fds in (False, True): | |
| with self.subTest(group=group, close_fds=close_fds): | |
| try: | |
| output = subprocess.check_output( | |
| [sys.executable, "-c", | |
| "import os; print(os.getgid())"], | |
| group=group, | |
| close_fds=close_fds) | |
| except PermissionError as e: # (EACCES, EPERM) | |
| self.assertIsNone(e.filename) | |
| else: | |
| if isinstance(group, str): | |
| group_gid = grp.getgrnam(group).gr_gid | |
| else: | |
| group_gid = group | |
| child_group = int(output) | |
| self.assertEqual(child_group, group_gid) | |
| # make sure we bomb on negative values | |
| with self.assertRaises(ValueError): | |
| subprocess.check_call(ZERO_RETURN_CMD, group=-1) | |
| with self.assertRaises(OverflowError): | |
| subprocess.check_call(ZERO_RETURN_CMD, | |
| cwd=os.curdir, env=os.environ, group=2**64) | |
| if grp is None: | |
| with self.assertRaises(ValueError): | |
| subprocess.check_call(ZERO_RETURN_CMD, group=name_group) | |
| @unittest.skipIf(hasattr(os, 'setregid'), 'setregid() available on platform') | |
| def test_group_error(self): | |
| with self.assertRaises(ValueError): | |
| subprocess.check_call(ZERO_RETURN_CMD, group=65535) | |
| @unittest.skipUnless(hasattr(os, 'setgroups'), 'no setgroups() on platform') | |
| def test_extra_groups(self): | |
| gid = os.getegid() | |
| group_list = [65534 if gid != 65534 else 65533] | |
| self._test_extra_groups_impl(gid=gid, group_list=group_list) | |
| @unittest.skipUnless(hasattr(os, 'setgroups'), 'no setgroups() on platform') | |
| def test_extra_groups_empty_list(self): | |
| self._test_extra_groups_impl(gid=os.getegid(), group_list=[]) | |
| def _test_extra_groups_impl(self, *, gid, group_list): | |
| name_group = _get_test_grp_name() | |
| if grp is not None: | |
| group_list.append(name_group) | |
| try: | |
| output = subprocess.check_output( | |
| [sys.executable, "-c", | |
| "import os, sys, json; json.dump(os.getgroups(), sys.stdout)"], | |
| extra_groups=group_list) | |
| except PermissionError as e: | |
| self.assertIsNone(e.filename) | |
| self.skipTest("setgroup() EPERM; this test may require root.") | |
| else: | |
| parent_groups = os.getgroups() | |
| child_groups = json.loads(output) | |
| if grp is not None: | |
| desired_gids = [grp.getgrnam(g).gr_gid if isinstance(g, str) else g | |
| for g in group_list] | |
| else: | |
| desired_gids = group_list | |
| self.assertEqual(set(desired_gids), set(child_groups)) | |
| if grp is None: | |
| with self.assertRaises(ValueError): | |
| subprocess.check_call(ZERO_RETURN_CMD, | |
| extra_groups=[name_group]) | |
| # No skip necessary, this test won't make it to a setgroup() call. | |
| def test_extra_groups_invalid_gid_t_values(self): | |
| with self.assertRaises(ValueError): | |
| subprocess.check_call(ZERO_RETURN_CMD, extra_groups=[-1]) | |
| with self.assertRaises(ValueError): | |
| subprocess.check_call(ZERO_RETURN_CMD, | |
| cwd=os.curdir, env=os.environ, | |
| extra_groups=[2**64]) | |
| @unittest.skipIf(mswindows or not hasattr(os, 'umask'), | |
| 'POSIX umask() is not available.') | |
| def test_umask(self): | |
| tmpdir = None | |
| try: | |
| tmpdir = tempfile.mkdtemp() | |
| name = os.path.join(tmpdir, "beans") | |
| # We set an unusual umask in the child so as a unique mode | |
| # for us to test the child's touched file for. | |
| subprocess.check_call( | |
| [sys.executable, "-c", f"open({name!r}, 'w').close()"], | |
| umask=0o053) | |
| # Ignore execute permissions entirely in our test, | |
| # filesystems could be mounted to ignore or force that. | |
| st_mode = os.stat(name).st_mode & 0o666 | |
| expected_mode = 0o624 | |
| self.assertEqual(expected_mode, st_mode, | |
| msg=f'{oct(expected_mode)} != {oct(st_mode)}') | |
| finally: | |
| if tmpdir is not None: | |
| shutil.rmtree(tmpdir) | |
| def test_run_abort(self): | |
| # returncode handles signal termination | |
| with support.SuppressCrashReport(): | |
| p = subprocess.Popen([sys.executable, "-c", | |
| 'import os; os.abort()']) | |
| p.wait() | |
| self.assertEqual(-p.returncode, signal.SIGABRT) | |
| def test_CalledProcessError_str(self): | |
| # command string | |
| err = subprocess.CalledProcessError(2, "fake cmd") | |
| self.assertEqual(str(err), "Command 'fake cmd' returned non-zero exit status 2.") | |
| # command string with a single-quote | |
| err = subprocess.CalledProcessError(2, "fake ' cmd") | |
| self.assertEqual(str(err), 'Command "fake \' cmd" returned non-zero exit status 2.') | |
| # command list | |
| err = subprocess.CalledProcessError(2, ["fake", "cmd"]) | |
| self.assertEqual(str(err), "Command ['fake', 'cmd'] returned non-zero exit status 2.") | |
| # signal | |
| err = subprocess.CalledProcessError(-int(signal.SIGABRT), "fake cmd") | |
| self.assertEqual(str(err), f"Command 'fake cmd' died with {signal.SIGABRT!r}.") | |
| # unknown signal | |
| err = subprocess.CalledProcessError(-9876543, "fake cmd") | |
| self.assertEqual(str(err), "Command 'fake cmd' died with unknown signal 9876543.") | |
| # returncode which is not an integer, which happens for example when | |
| # Popen is mocked: str() must not fail | |
| for returncode in (None, "2", 2.5, [2]): | |
| with self.subTest(returncode=returncode): | |
| err = subprocess.CalledProcessError(returncode, "fake cmd") | |
| self.assertEqual( | |
| str(err), | |
| f"Command 'fake cmd' returned non-zero " | |
| f"exit status {returncode}.") | |
| def test_preexec(self): | |
| # DISCLAIMER: Setting environment variables is *not* a good use | |
| # of a preexec_fn. This is merely a test. | |
| p = subprocess.Popen([sys.executable, "-c", | |
| 'import sys,os;' | |
| 'sys.stdout.write(os.getenv("FRUIT"))'], | |
| stdout=subprocess.PIPE, | |
| preexec_fn=lambda: os.putenv("FRUIT", "apple")) | |
| with p: | |
| self.assertEqual(p.stdout.read(), b"apple") | |
| def test_preexec_exception(self): | |
| def raise_it(): | |
| raise ValueError("What if two swallows carried a coconut?") | |
| try: | |
| p = subprocess.Popen([sys.executable, "-c", ""], | |
| preexec_fn=raise_it) | |
| except subprocess.SubprocessError as e: | |
| self.assertTrue( | |
| subprocess._fork_exec, | |
| "Expected a ValueError from the preexec_fn") | |
| except ValueError as e: | |
| self.assertIn("coconut", e.args[0]) | |
| else: | |
| self.fail("Exception raised by preexec_fn did not make it " | |
| "to the parent process.") | |
| class _TestExecuteChildPopen(subprocess.Popen): | |
| """Used to test behavior at the end of _execute_child.""" | |
| def __init__(self, testcase, *args, **kwargs): | |
| self._testcase = testcase | |
| subprocess.Popen.__init__(self, *args, **kwargs) | |
| def _execute_child(self, *args, **kwargs): | |
| try: | |
| subprocess.Popen._execute_child(self, *args, **kwargs) | |
| finally: | |
| # Open a bunch of file descriptors and verify that | |
| # none of them are the same as the ones the Popen | |
| # instance is using for stdin/stdout/stderr. | |
| devzero_fds = [os.open("/dev/zero", os.O_RDONLY) | |
| for _ in range(8)] | |
| try: | |
| for fd in devzero_fds: | |
| self._testcase.assertNotIn( | |
| fd, (self.stdin.fileno(), self.stdout.fileno(), | |
| self.stderr.fileno()), | |
| msg="At least one fd was closed early.") | |
| finally: | |
| for fd in devzero_fds: | |
| os.close(fd) | |
| @unittest.skipIf(not os.path.exists("/dev/zero"), "/dev/zero required.") | |
| def test_preexec_errpipe_does_not_double_close_pipes(self): | |
| """Issue16140: Don't double close pipes on preexec error.""" | |
| def raise_it(): | |
| raise subprocess.SubprocessError( | |
| "force the _execute_child() errpipe_data path.") | |
| with self.assertRaises(subprocess.SubprocessError): | |
| self._TestExecuteChildPopen( | |
| self, ZERO_RETURN_CMD, | |
| stdin=subprocess.PIPE, stdout=subprocess.PIPE, | |
| stderr=subprocess.PIPE, preexec_fn=raise_it) | |
| def test_preexec_gc_module_failure(self): | |
| # This tests the code that disables garbage collection if the child | |
| # process will execute any Python. | |
| enabled = gc.isenabled() | |
| try: | |
| gc.disable() | |
| self.assertFalse(gc.isenabled()) | |
| subprocess.call([sys.executable, '-c', ''], | |
| preexec_fn=lambda: None) | |
| self.assertFalse(gc.isenabled(), | |
| "Popen enabled gc when it shouldn't.") | |
| gc.enable() | |
| self.assertTrue(gc.isenabled()) | |
| subprocess.call([sys.executable, '-c', ''], | |
| preexec_fn=lambda: None) | |
| self.assertTrue(gc.isenabled(), "Popen left gc disabled.") | |
| finally: | |
| if not enabled: | |
| gc.disable() | |
| @unittest.skipIf( | |
| sys.platform == 'darwin', 'setrlimit() seems to fail on OS X') | |
| def test_preexec_fork_failure(self): | |
| # The internal code did not preserve the previous exception when | |
| # re-enabling garbage collection | |
| try: | |
| from resource import getrlimit, setrlimit, RLIMIT_NPROC | |
| except ImportError as err: | |
| self.skipTest(err) # RLIMIT_NPROC is specific to Linux and BSD | |
| limits = getrlimit(RLIMIT_NPROC) | |
| [_, hard] = limits | |
| setrlimit(RLIMIT_NPROC, (0, hard)) | |
| self.addCleanup(setrlimit, RLIMIT_NPROC, limits) | |
| try: | |
| subprocess.call([sys.executable, '-c', ''], | |
| preexec_fn=lambda: None) | |
| except BlockingIOError: | |
| # Forking should raise EAGAIN, translated to BlockingIOError | |
| pass | |
| else: | |
| self.skipTest('RLIMIT_NPROC had no effect; probably superuser') | |
| def test_args_string(self): | |
| # args is a string | |
| fd, fname = tempfile.mkstemp() | |
| # reopen in text mode | |
| with open(fd, "w", errors="surrogateescape") as fobj: | |
| fobj.write("#!%s\n" % support.unix_shell) | |
| fobj.write("exec '%s' -c 'import sys; sys.exit(47)'\n" % | |
| sys.executable) | |
| os.chmod(fname, 0o700) | |
| p = subprocess.Popen(fname) | |
| p.wait() | |
| os.remove(fname) | |
| self.assertEqual(p.returncode, 47) | |
| def test_invalid_args(self): | |
| # invalid arguments should raise ValueError | |
| self.assertRaises(ValueError, subprocess.call, | |
| [sys.executable, "-c", | |
| "import sys; sys.exit(47)"], | |
| startupinfo=47) | |
| self.assertRaises(ValueError, subprocess.call, | |
| [sys.executable, "-c", | |
| "import sys; sys.exit(47)"], | |
| creationflags=47) | |
| def test_shell_sequence(self): | |
| # Run command through the shell (sequence) | |
| newenv = os.environ.copy() | |
| newenv["FRUIT"] = "apple" | |
| p = subprocess.Popen(["echo $FRUIT"], shell=1, | |
| stdout=subprocess.PIPE, | |
| env=newenv) | |
| with p: | |
| self.assertEqual(p.stdout.read().strip(b" \t\r\n\f"), b"apple") | |
| def test_shell_string(self): | |
| # Run command through the shell (string) | |
| newenv = os.environ.copy() | |
| newenv["FRUIT"] = "apple" | |
| p = subprocess.Popen("echo $FRUIT", shell=1, | |
| stdout=subprocess.PIPE, | |
| env=newenv) | |
| with p: | |
| self.assertEqual(p.stdout.read().strip(b" \t\r\n\f"), b"apple") | |
| def test_call_string(self): | |
| # call() function with string argument on UNIX | |
| fd, fname = tempfile.mkstemp() | |
| # reopen in text mode | |
| with open(fd, "w", errors="surrogateescape") as fobj: | |
| fobj.write("#!%s\n" % support.unix_shell) | |
| fobj.write("exec '%s' -c 'import sys; sys.exit(47)'\n" % | |
| sys.executable) | |
| os.chmod(fname, 0o700) | |
| rc = subprocess.call(fname) | |
| os.remove(fname) | |
| self.assertEqual(rc, 47) | |
| def test_specific_shell(self): | |
| # Issue #9265: Incorrect name passed as arg[0]. | |
| shells = [] | |
| for prefix in ['/bin', '/usr/bin/', '/usr/local/bin']: | |
| for name in ['bash', 'ksh']: | |
| sh = os.path.join(prefix, name) | |
| if os.path.isfile(sh): | |
| shells.append(sh) | |
| if not shells: # Will probably work for any shell but csh. | |
| self.skipTest("bash or ksh required for this test") | |
| sh = '/bin/sh' | |
| if os.path.isfile(sh) and not os.path.islink(sh): | |
| # Test will fail if /bin/sh is a symlink to csh. | |
| shells.append(sh) | |
| for sh in shells: | |
| p = subprocess.Popen("echo $0", executable=sh, shell=True, | |
| stdout=subprocess.PIPE) | |
| with p: | |
| self.assertEqual(p.stdout.read().strip(), bytes(sh, 'ascii')) | |
| def _kill_process(self, method, *args): | |
| # Do not inherit file handles from the parent. | |
| # It should fix failures on some platforms. | |
| # Also set the SIGINT handler to the default to make sure it's not | |
| # being ignored (some tests rely on that.) | |
| old_handler = signal.signal(signal.SIGINT, signal.default_int_handler) | |
| try: | |
| p = subprocess.Popen([sys.executable, "-c", """if 1: | |
| import sys, time | |
| sys.stdout.write('x\\n') | |
| sys.stdout.flush() | |
| time.sleep(30) | |
| """], | |
| close_fds=True, | |
| stdin=subprocess.PIPE, | |
| stdout=subprocess.PIPE, | |
| stderr=subprocess.PIPE) | |
| finally: | |
| signal.signal(signal.SIGINT, old_handler) | |
| # Wait for the interpreter to be completely initialized before | |
| # sending any signal. | |
| p.stdout.read(1) | |
| getattr(p, method)(*args) | |
| return p | |
| @unittest.skipIf(sys.platform.startswith(('netbsd', 'openbsd')), | |
| "Due to known OS bug (issue #16762)") | |
| def _kill_dead_process(self, method, *args): | |
| # Do not inherit file handles from the parent. | |
| # It should fix failures on some platforms. | |
| p = subprocess.Popen([sys.executable, "-c", """if 1: | |
| import sys, time | |
| sys.stdout.write('x\\n') | |
| sys.stdout.flush() | |
| """], | |
| close_fds=True, | |
| stdin=subprocess.PIPE, | |
| stdout=subprocess.PIPE, | |
| stderr=subprocess.PIPE) | |
| # Wait for the interpreter to be completely initialized before | |
| # sending any signal. | |
| p.stdout.read(1) | |
| # The process should end after this | |
| time.sleep(1) | |
| # This shouldn't raise even though the child is now dead | |
| getattr(p, method)(*args) | |
| p.communicate() | |
| def test_send_signal(self): | |
| p = self._kill_process('send_signal', signal.SIGINT) | |
| _, stderr = p.communicate() | |
| self.assertIn(b'KeyboardInterrupt', stderr) | |
| self.assertNotEqual(p.wait(), 0) | |
| def test_kill(self): | |
| p = self._kill_process('kill') | |
| _, stderr = p.communicate() | |
| self.assertEqual(stderr, b'') | |
| self.assertEqual(p.wait(), -signal.SIGKILL) | |
| def test_terminate(self): | |
| p = self._kill_process('terminate') | |
| _, stderr = p.communicate() | |
| self.assertEqual(stderr, b'') | |
| self.assertEqual(p.wait(), -signal.SIGTERM) | |
| def test_send_signal_dead(self): | |
| # Sending a signal to a dead process | |
| self._kill_dead_process('send_signal', signal.SIGINT) | |
| def test_kill_dead(self): | |
| # Killing a dead process | |
| self._kill_dead_process('kill') | |
| def test_terminate_dead(self): | |
| # Terminating a dead process | |
| self._kill_dead_process('terminate') | |
| def _save_fds(self, save_fds): | |
| fds = [] | |
| for fd in save_fds: | |
| inheritable = os.get_inheritable(fd) | |
| saved = os.dup(fd) | |
| fds.append((fd, saved, inheritable)) | |
| return fds | |
| def _restore_fds(self, fds): | |
| for fd, saved, inheritable in fds: | |
| os.dup2(saved, fd, inheritable=inheritable) | |
| os.close(saved) | |
| def check_close_std_fds(self, fds): | |
| # Issue #9905: test that subprocess pipes still work properly with | |
| # some standard fds closed | |
| stdin = 0 | |
| saved_fds = self._save_fds(fds) | |
| for fd, saved, inheritable in saved_fds: | |
| if fd == 0: | |
| stdin = saved | |
| break | |
| try: | |
| for fd in fds: | |
| os.close(fd) | |
| out, err = subprocess.Popen([sys.executable, "-c", | |
| 'import sys;' | |
| 'sys.stdout.write("apple");' | |
| 'sys.stdout.flush();' | |
| 'sys.stderr.write("orange")'], | |
| stdin=stdin, | |
| stdout=subprocess.PIPE, | |
| stderr=subprocess.PIPE).communicate() | |
| self.assertEqual(out, b'apple') | |
| self.assertEqual(err, b'orange') | |
| finally: | |
| self._restore_fds(saved_fds) | |
| def test_close_fd_0(self): | |
| self.check_close_std_fds([0]) | |
| def test_close_fd_1(self): | |
| self.check_close_std_fds([1]) | |
| def test_close_fd_2(self): | |
| self.check_close_std_fds([2]) | |
| def test_close_fds_0_1(self): | |
| self.check_close_std_fds([0, 1]) | |
| def test_close_fds_0_2(self): | |
| self.check_close_std_fds([0, 2]) | |
| def test_close_fds_1_2(self): | |
| self.check_close_std_fds([1, 2]) | |
| def test_close_fds_0_1_2(self): | |
| # Issue #10806: test that subprocess pipes still work properly with | |
| # all standard fds closed. | |
| self.check_close_std_fds([0, 1, 2]) | |
| def test_small_errpipe_write_fd(self): | |
| """Issue #15798: Popen should work when stdio fds are available.""" | |
| new_stdin = os.dup(0) | |
| new_stdout = os.dup(1) | |
| try: | |
| os.close(0) | |
| os.close(1) | |
| # Side test: if errpipe_write fails to have its CLOEXEC | |
| # flag set this should cause the parent to think the exec | |
| # failed. Extremely unlikely: everyone supports CLOEXEC. | |
| subprocess.Popen([ | |
| sys.executable, "-c", | |
| "print('AssertionError:0:CLOEXEC failure.')"]).wait() | |
| finally: | |
| # Restore original stdin and stdout | |
| os.dup2(new_stdin, 0) | |
| os.dup2(new_stdout, 1) | |
| os.close(new_stdin) | |
| os.close(new_stdout) | |
| def test_remapping_std_fds(self): | |
| # open up some temporary files | |
| temps = [tempfile.mkstemp() for i in range(3)] | |
| try: | |
| temp_fds = [fd for fd, fname in temps] | |
| # unlink the files -- we won't need to reopen them | |
| for fd, fname in temps: | |
| os.unlink(fname) | |
| # write some data to what will become stdin, and rewind | |
| os.write(temp_fds[1], b"STDIN") | |
| os.lseek(temp_fds[1], 0, 0) | |
| # move the standard file descriptors out of the way | |
| saved_fds = self._save_fds(range(3)) | |
| try: | |
| # duplicate the file objects over the standard fd's | |
| for fd, temp_fd in enumerate(temp_fds): | |
| os.dup2(temp_fd, fd) | |
| # now use those files in the "wrong" order, so that subprocess | |
| # has to rearrange them in the child | |
| p = subprocess.Popen([sys.executable, "-c", | |
| 'import sys; got = sys.stdin.read();' | |
| 'sys.stdout.write("got %s"%got); sys.stderr.write("err")'], | |
| stdin=temp_fds[1], | |
| stdout=temp_fds[2], | |
| stderr=temp_fds[0]) | |
| p.wait() | |
| finally: | |
| self._restore_fds(saved_fds) | |
| for fd in temp_fds: | |
| os.lseek(fd, 0, 0) | |
| out = os.read(temp_fds[2], 1024) | |
| err = os.read(temp_fds[0], 1024).strip() | |
| self.assertEqual(out, b"got STDIN") | |
| self.assertEqual(err, b"err") | |
| finally: | |
| for fd in temp_fds: | |
| os.close(fd) | |
| def check_swap_fds(self, stdin_no, stdout_no, stderr_no): | |
| # open up some temporary files | |
| temps = [tempfile.mkstemp() for i in range(3)] | |
| temp_fds = [fd for fd, fname in temps] | |
| try: | |
| # unlink the files -- we won't need to reopen them | |
| for fd, fname in temps: | |
| os.unlink(fname) | |
| # save a copy of the standard file descriptors | |
| saved_fds = self._save_fds(range(3)) | |
| try: | |
| # duplicate the temp files over the standard fd's 0, 1, 2 | |
| for fd, temp_fd in enumerate(temp_fds): | |
| os.dup2(temp_fd, fd) | |
| # write some data to what will become stdin, and rewind | |
| os.write(stdin_no, b"STDIN") | |
| os.lseek(stdin_no, 0, 0) | |
| # now use those files in the given order, so that subprocess | |
| # has to rearrange them in the child | |
| p = subprocess.Popen([sys.executable, "-c", | |
| 'import sys; got = sys.stdin.read();' | |
| 'sys.stdout.write("got %s"%got); sys.stderr.write("err")'], | |
| stdin=stdin_no, | |
| stdout=stdout_no, | |
| stderr=stderr_no) | |
| p.wait() | |
| for fd in temp_fds: | |
| os.lseek(fd, 0, 0) | |
| out = os.read(stdout_no, 1024) | |
| err = os.read(stderr_no, 1024).strip() | |
| finally: | |
| self._restore_fds(saved_fds) | |
| self.assertEqual(out, b"got STDIN") | |
| self.assertEqual(err, b"err") | |
| finally: | |
| for fd in temp_fds: | |
| os.close(fd) | |
| # When duping fds, if there arises a situation where one of the fds is | |
| # either 0, 1 or 2, it is possible that it is overwritten (#12607). | |
| # This tests all combinations of this. | |
| def test_swap_fds(self): | |
| self.check_swap_fds(0, 1, 2) | |
| self.check_swap_fds(0, 2, 1) | |
| self.check_swap_fds(1, 0, 2) | |
| self.check_swap_fds(1, 2, 0) | |
| self.check_swap_fds(2, 0, 1) | |
| self.check_swap_fds(2, 1, 0) | |
| def _check_swap_std_fds_with_one_closed(self, from_fds, to_fds): | |
| saved_fds = self._save_fds(range(3)) | |
| try: | |
| for from_fd in from_fds: | |
| with tempfile.TemporaryFile() as f: | |
| os.dup2(f.fileno(), from_fd) | |
| fd_to_close = (set(range(3)) - set(from_fds)).pop() | |
| os.close(fd_to_close) | |
| arg_names = ['stdin', 'stdout', 'stderr'] | |
| kwargs = {} | |
| for from_fd, to_fd in zip(from_fds, to_fds): | |
| kwargs[arg_names[to_fd]] = from_fd | |
| code = textwrap.dedent(r''' | |
| import os, sys | |
| skipped_fd = int(sys.argv[1]) | |
| for fd in range(3): | |
| if fd != skipped_fd: | |
| os.write(fd, str(fd).encode('ascii')) | |
| ''') | |
| skipped_fd = (set(range(3)) - set(to_fds)).pop() | |
| rc = subprocess.call([sys.executable, '-c', code, str(skipped_fd)], | |
| **kwargs) | |
| self.assertEqual(rc, 0) | |
| for from_fd, to_fd in zip(from_fds, to_fds): | |
| os.lseek(from_fd, 0, os.SEEK_SET) | |
| read_bytes = os.read(from_fd, 1024) | |
| read_fds = list(map(int, read_bytes.decode('ascii'))) | |
| msg = textwrap.dedent(f""" | |
| When testing {from_fds} to {to_fds} redirection, | |
| parent descriptor {from_fd} got redirected | |
| to descriptor(s) {read_fds} instead of descriptor {to_fd}. | |
| """) | |
| self.assertEqual([to_fd], read_fds, msg) | |
| finally: | |
| self._restore_fds(saved_fds) | |
| # Check that subprocess can remap std fds correctly even | |
| # if one of them is closed (#32844). | |
| def test_swap_std_fds_with_one_closed(self): | |
| for from_fds in itertools.combinations(range(3), 2): | |
| for to_fds in itertools.permutations(range(3), 2): | |
| self._check_swap_std_fds_with_one_closed(from_fds, to_fds) | |
| def test_surrogates_error_message(self): | |
| def prepare(): | |
| raise ValueError("surrogate:\uDCff") | |
| try: | |
| subprocess.call( | |
| ZERO_RETURN_CMD, | |
| preexec_fn=prepare) | |
| except ValueError as err: | |
| # Pure Python implementations keeps the message | |
| self.assertIsNone(subprocess._fork_exec) | |
| self.assertEqual(str(err), "surrogate:\uDCff") | |
| except subprocess.SubprocessError as err: | |
| # _posixsubprocess uses a default message | |
| self.assertIsNotNone(subprocess._fork_exec) | |
| self.assertEqual(str(err), "Exception occurred in preexec_fn.") | |
| else: | |
| self.fail("Expected ValueError or subprocess.SubprocessError") | |
| def test_undecodable_env(self): | |
| for key, value in (('test', 'abc\uDCFF'), ('test\uDCFF', '42')): | |
| encoded_value = value.encode("ascii", "surrogateescape") | |
| # test str with surrogates | |
| script = "import os; print(ascii(os.getenv(%s)))" % repr(key) | |
| env = os.environ.copy() | |
| env[key] = value | |
| # Use C locale to get ASCII for the locale encoding to force | |
| # surrogate-escaping of \xFF in the child process | |
| env['LC_ALL'] = 'C' | |
| decoded_value = value | |
| stdout = subprocess.check_output( | |
| [sys.executable, "-c", script], | |
| env=env) | |
| stdout = stdout.rstrip(b'\n\r') | |
| self.assertEqual(stdout.decode('ascii'), ascii(decoded_value)) | |
| # test bytes | |
| key = key.encode("ascii", "surrogateescape") | |
| script = "import os; print(ascii(os.getenvb(%s)))" % repr(key) | |
| env = os.environ.copy() | |
| env[key] = encoded_value | |
| stdout = subprocess.check_output( | |
| [sys.executable, "-c", script], | |
| env=env) | |
| stdout = stdout.rstrip(b'\n\r') | |
| self.assertEqual(stdout.decode('ascii'), ascii(encoded_value)) | |
| def test_bytes_program(self): | |
| abs_program = os.fsencode(ZERO_RETURN_CMD[0]) | |
| args = list(ZERO_RETURN_CMD[1:]) | |
| path, program = os.path.split(ZERO_RETURN_CMD[0]) | |
| program = os.fsencode(program) | |
| # absolute bytes path | |
| exitcode = subprocess.call([abs_program]+args) | |
| self.assertEqual(exitcode, 0) | |
| # absolute bytes path as a string | |
| cmd = b"'%s' %s" % (abs_program, " ".join(args).encode("utf-8")) | |
| exitcode = subprocess.call(cmd, shell=True) | |
| self.assertEqual(exitcode, 0) | |
| # bytes program, unicode PATH | |
| env = os.environ.copy() | |
| env["PATH"] = path | |
| exitcode = subprocess.call([program]+args, env=env) | |
| self.assertEqual(exitcode, 0) | |
| # bytes program, bytes PATH | |
| envb = os.environb.copy() | |
| envb[b"PATH"] = os.fsencode(path) | |
| exitcode = subprocess.call([program]+args, env=envb) | |
| self.assertEqual(exitcode, 0) | |
| def test_pipe_cloexec(self): | |
| sleeper = support.findfile("input_reader.py", subdir="subprocessdata") | |
| fd_status = support.findfile("fd_status.py", subdir="subprocessdata") | |
| p1 = subprocess.Popen([sys.executable, sleeper], | |
| stdin=subprocess.PIPE, stdout=subprocess.PIPE, | |
| stderr=subprocess.PIPE, close_fds=False) | |
| self.addCleanup(p1.communicate, b'') | |
| p2 = subprocess.Popen([sys.executable, fd_status], | |
| stdout=subprocess.PIPE, close_fds=False) | |
| output, error = p2.communicate() | |
| result_fds = set(map(int, output.split(b','))) | |
| unwanted_fds = set([p1.stdin.fileno(), p1.stdout.fileno(), | |
| p1.stderr.fileno()]) | |
| self.assertFalse(result_fds & unwanted_fds, | |
| "Expected no fds from %r to be open in child, " | |
| "found %r" % | |
| (unwanted_fds, result_fds & unwanted_fds)) | |
| def test_pipe_cloexec_real_tools(self): | |
| qcat = support.findfile("qcat.py", subdir="subprocessdata") | |
| qgrep = support.findfile("qgrep.py", subdir="subprocessdata") | |
| subdata = b'zxcvbn' | |
| data = subdata * 4 + b'\n' | |
| p1 = subprocess.Popen([sys.executable, qcat], | |
| stdin=subprocess.PIPE, stdout=subprocess.PIPE, | |
| close_fds=False) | |
| p2 = subprocess.Popen([sys.executable, qgrep, subdata], | |
| stdin=p1.stdout, stdout=subprocess.PIPE, | |
| close_fds=False) | |
| self.addCleanup(p1.wait) | |
| self.addCleanup(p2.wait) | |
| def kill_p1(): | |
| try: | |
| p1.terminate() | |
| except ProcessLookupError: | |
| pass | |
| def kill_p2(): | |
| try: | |
| p2.terminate() | |
| except ProcessLookupError: | |
| pass | |
| self.addCleanup(kill_p1) | |
| self.addCleanup(kill_p2) | |
| p1.stdin.write(data) | |
| p1.stdin.close() | |
| readfiles, ignored1, ignored2 = select.select([p2.stdout], [], [], 10) | |
| self.assertTrue(readfiles, "The child hung") | |
| self.assertEqual(p2.stdout.read(), data) | |
| p1.stdout.close() | |
| p2.stdout.close() | |
| def test_close_fds(self): | |
| fd_status = support.findfile("fd_status.py", subdir="subprocessdata") | |
| fds = os.pipe() | |
| self.addCleanup(os.close, fds[0]) | |
| self.addCleanup(os.close, fds[1]) | |
| open_fds = set(fds) | |
| # add a bunch more fds | |
| for _ in range(9): | |
| fd = os.open(os.devnull, os.O_RDONLY) | |
| self.addCleanup(os.close, fd) | |
| open_fds.add(fd) | |
| for fd in open_fds: | |
| os.set_inheritable(fd, True) | |
| p = subprocess.Popen([sys.executable, fd_status], | |
| stdout=subprocess.PIPE, close_fds=False) | |
| output, ignored = p.communicate() | |
| remaining_fds = set(map(int, output.split(b','))) | |
| self.assertEqual(remaining_fds & open_fds, open_fds, | |
| "Some fds were closed") | |
| p = subprocess.Popen([sys.executable, fd_status], | |
| stdout=subprocess.PIPE, close_fds=True) | |
| output, ignored = p.communicate() | |
| remaining_fds = set(map(int, output.split(b','))) | |
| self.assertFalse(remaining_fds & open_fds, | |
| "Some fds were left open") | |
| self.assertIn(1, remaining_fds, "Subprocess failed") | |
| # Keep some of the fd's we opened open in the subprocess. | |
| # This tests _posixsubprocess.c's proper handling of fds_to_keep. | |
| fds_to_keep = set(open_fds.pop() for _ in range(8)) | |
| p = subprocess.Popen([sys.executable, fd_status], | |
| stdout=subprocess.PIPE, close_fds=True, | |
| pass_fds=fds_to_keep) | |
| output, ignored = p.communicate() | |
| remaining_fds = set(map(int, output.split(b','))) | |
| self.assertFalse((remaining_fds - fds_to_keep) & open_fds, | |
| "Some fds not in pass_fds were left open") | |
| self.assertIn(1, remaining_fds, "Subprocess failed") | |
| @unittest.skipIf(sys.platform.startswith("freebsd") and | |
| os.stat("/dev").st_dev == os.stat("/dev/fd").st_dev, | |
| "Requires fdescfs mounted on /dev/fd on FreeBSD") | |
| def test_close_fds_when_max_fd_is_lowered(self): | |
| """Confirm that issue21618 is fixed (may fail under valgrind).""" | |
| fd_status = support.findfile("fd_status.py", subdir="subprocessdata") | |
| # This launches the meat of the test in a child process to | |
| # avoid messing with the larger unittest processes maximum | |
| # number of file descriptors. | |
| # This process launches: | |
| # +--> Process that lowers its RLIMIT_NOFILE aftr setting up | |
| # a bunch of high open fds above the new lower rlimit. | |
| # Those are reported via stdout before launching a new | |
| # process with close_fds=False to run the actual test: | |
| # +--> The TEST: This one launches a fd_status.py | |
| # subprocess with close_fds=True so we can find out if | |
| # any of the fds above the lowered rlimit are still open. | |
| p = subprocess.Popen([sys.executable, '-c', textwrap.dedent( | |
| ''' | |
| import os, resource, subprocess, sys, textwrap | |
| open_fds = set() | |
| # Add a bunch more fds to pass down. | |
| for _ in range(40): | |
| fd = os.open(os.devnull, os.O_RDONLY) | |
| open_fds.add(fd) | |
| # Leave a two pairs of low ones available for use by the | |
| # internal child error pipe and the stdout pipe. | |
| # We also leave 10 more open as some Python buildbots run into | |
| # "too many open files" errors during the test if we do not. | |
| for fd in sorted(open_fds)[:14]: | |
| os.close(fd) | |
| open_fds.remove(fd) | |
| for fd in open_fds: | |
| #self.addCleanup(os.close, fd) | |
| os.set_inheritable(fd, True) | |
| max_fd_open = max(open_fds) | |
| # Communicate the open_fds to the parent unittest.TestCase process. | |
| print(','.join(map(str, sorted(open_fds)))) | |
| sys.stdout.flush() | |
| rlim_cur, rlim_max = resource.getrlimit(resource.RLIMIT_NOFILE) | |
| try: | |
| # 29 is lower than the highest fds we are leaving open. | |
| resource.setrlimit(resource.RLIMIT_NOFILE, (29, rlim_max)) | |
| # Launch a new Python interpreter with our low fd rlim_cur that | |
| # inherits open fds above that limit. It then uses subprocess | |
| # with close_fds=True to get a report of open fds in the child. | |
| # An explicit list of fds to check is passed to fd_status.py as | |
| # letting fd_status rely on its default logic would miss the | |
| # fds above rlim_cur as it normally only checks up to that limit. | |
| subprocess.Popen( | |
| [sys.executable, '-c', | |
| textwrap.dedent(""" | |
| import subprocess, sys | |
| subprocess.Popen([sys.executable, %r] + | |
| [str(x) for x in range({max_fd})], | |
| close_fds=True).wait() | |
| """.format(max_fd=max_fd_open+1))], | |
| close_fds=False).wait() | |
| finally: | |
| resource.setrlimit(resource.RLIMIT_NOFILE, (rlim_cur, rlim_max)) | |
| ''' % fd_status)], stdout=subprocess.PIPE) | |
| output, unused_stderr = p.communicate() | |
| output_lines = output.splitlines() | |
| self.assertEqual(len(output_lines), 2, | |
| msg="expected exactly two lines of output:\n%r" % output) | |
| opened_fds = set(map(int, output_lines[0].strip().split(b','))) | |
| remaining_fds = set(map(int, output_lines[1].strip().split(b','))) | |
| self.assertFalse(remaining_fds & opened_fds, | |
| msg="Some fds were left open.") | |
| # Mac OS X Tiger (10.4) has a kernel bug: sometimes, the file | |
| # descriptor of a pipe closed in the parent process is valid in the | |
| # child process according to fstat(), but the mode of the file | |
| # descriptor is invalid, and read or write raise an error. | |
| @support.requires_mac_ver(10, 5) | |
| def test_pass_fds(self): | |
| fd_status = support.findfile("fd_status.py", subdir="subprocessdata") | |
| open_fds = set() | |
| for x in range(5): | |
| fds = os.pipe() | |
| self.addCleanup(os.close, fds[0]) | |
| self.addCleanup(os.close, fds[1]) | |
| os.set_inheritable(fds[0], True) | |
| os.set_inheritable(fds[1], True) | |
| open_fds.update(fds) | |
| for fd in open_fds: | |
| p = subprocess.Popen([sys.executable, fd_status], | |
| stdout=subprocess.PIPE, close_fds=True, | |
| pass_fds=(fd, )) | |
| output, ignored = p.communicate() | |
| remaining_fds = set(map(int, output.split(b','))) | |
| to_be_closed = open_fds - {fd} | |
| self.assertIn(fd, remaining_fds, "fd to be passed not passed") | |
| self.assertFalse(remaining_fds & to_be_closed, | |
| "fd to be closed passed") | |
| # pass_fds overrides close_fds with a warning. | |
| with self.assertWarns(RuntimeWarning) as context: | |
| self.assertFalse(subprocess.call( | |
| ZERO_RETURN_CMD, | |
| close_fds=False, pass_fds=(fd, ))) | |
| self.assertIn('overriding close_fds', str(context.warning)) | |
| def test_pass_fds_inheritable(self): | |
| script = support.findfile("fd_status.py", subdir="subprocessdata") | |
| inheritable, non_inheritable = os.pipe() | |
| self.addCleanup(os.close, inheritable) | |
| self.addCleanup(os.close, non_inheritable) | |
| os.set_inheritable(inheritable, True) | |
| os.set_inheritable(non_inheritable, False) | |
| pass_fds = (inheritable, non_inheritable) | |
| args = [sys.executable, script] | |
| args += list(map(str, pass_fds)) | |
| p = subprocess.Popen(args, | |
| stdout=subprocess.PIPE, close_fds=True, | |
| pass_fds=pass_fds) | |
| output, ignored = p.communicate() | |
| fds = set(map(int, output.split(b','))) | |
| # the inheritable file descriptor must be inherited, so its inheritable | |
| # flag must be set in the child process after fork() and before exec() | |
| self.assertEqual(fds, set(pass_fds), "output=%a" % output) | |
| # inheritable flag must not be changed in the parent process | |
| self.assertEqual(os.get_inheritable(inheritable), True) | |
| self.assertEqual(os.get_inheritable(non_inheritable), False) | |
| # bpo-32270: Ensure that descriptors specified in pass_fds | |
| # are inherited even if they are used in redirections. | |
| # Contributed by @izbyshev. | |
| def test_pass_fds_redirected(self): | |
| """Regression test for https://bugs.python.org/issue32270.""" | |
| fd_status = support.findfile("fd_status.py", subdir="subprocessdata") | |
| pass_fds = [] | |
| for _ in range(2): | |
| fd = os.open(os.devnull, os.O_RDWR) | |
| self.addCleanup(os.close, fd) | |
| pass_fds.append(fd) | |
| stdout_r, stdout_w = os.pipe() | |
| self.addCleanup(os.close, stdout_r) | |
| self.addCleanup(os.close, stdout_w) | |
| pass_fds.insert(1, stdout_w) | |
| with subprocess.Popen([sys.executable, fd_status], | |
| stdin=pass_fds[0], | |
| stdout=pass_fds[1], | |
| stderr=pass_fds[2], | |
| close_fds=True, | |
| pass_fds=pass_fds): | |
| output = os.read(stdout_r, 1024) | |
| fds = {int(num) for num in output.split(b',')} | |
| self.assertEqual(fds, {0, 1, 2} | frozenset(pass_fds), f"output={output!a}") | |
| def test_stdout_stdin_are_single_inout_fd(self): | |
| with io.open(os.devnull, "r+") as inout: | |
| p = subprocess.Popen(ZERO_RETURN_CMD, | |
| stdout=inout, stdin=inout) | |
| p.wait() | |
| def test_stdout_stderr_are_single_inout_fd(self): | |
| with io.open(os.devnull, "r+") as inout: | |
| p = subprocess.Popen(ZERO_RETURN_CMD, | |
| stdout=inout, stderr=inout) | |
| p.wait() | |
| def test_stderr_stdin_are_single_inout_fd(self): | |
| with io.open(os.devnull, "r+") as inout: | |
| p = subprocess.Popen(ZERO_RETURN_CMD, | |
| stderr=inout, stdin=inout) | |
| p.wait() | |
| def test_wait_when_sigchild_ignored(self): | |
| # NOTE: sigchild_ignore.py may not be an effective test on all OSes. | |
| sigchild_ignore = support.findfile("sigchild_ignore.py", | |
| subdir="subprocessdata") | |
| p = subprocess.Popen([sys.executable, sigchild_ignore], | |
| stdout=subprocess.PIPE, stderr=subprocess.PIPE) | |
| stdout, stderr = p.communicate() | |
| self.assertEqual(0, p.returncode, "sigchild_ignore.py exited" | |
| " non-zero with this error:\n%s" % | |
| stderr.decode('utf-8')) | |
| def test_select_unbuffered(self): | |
| # Issue #11459: bufsize=0 should really set the pipes as | |
| # unbuffered (and therefore let select() work properly). | |
| select = import_helper.import_module("select") | |
| p = subprocess.Popen([sys.executable, "-c", | |
| 'import sys;' | |
| 'sys.stdout.write("apple")'], | |
| stdout=subprocess.PIPE, | |
| bufsize=0) | |
| f = p.stdout | |
| self.addCleanup(f.close) | |
| try: | |
| self.assertEqual(f.read(4), b"appl") | |
| self.assertIn(f, select.select([f], [], [], 0.0)[0]) | |
| finally: | |
| p.wait() | |
| def test_zombie_fast_process_del(self): | |
| # Issue #12650: on Unix, if Popen.__del__() was called before the | |
| # process exited, it wouldn't be added to subprocess._active, and would | |
| # remain a zombie. | |
| # spawn a Popen, and delete its reference before it exits | |
| p = subprocess.Popen([sys.executable, "-c", | |
| 'import sys, time;' | |
| 'time.sleep(0.2)'], | |
| stdout=subprocess.PIPE, | |
| stderr=subprocess.PIPE) | |
| self.addCleanup(p.stdout.close) | |
| self.addCleanup(p.stderr.close) | |
| ident = id(p) | |
| pid = p.pid | |
| with warnings_helper.check_warnings(('', ResourceWarning)): | |
| p = None | |
| if mswindows: | |
| # subprocess._active is not used on Windows and is set to None. | |
| self.assertIsNone(subprocess._active) | |
| else: | |
| # check that p is in the active processes list | |
| self.assertIn(ident, [id(o) for o in subprocess._active]) | |
| def test_leak_fast_process_del_killed(self): | |
| # Issue #12650: on Unix, if Popen.__del__() was called before the | |
| # process exited, and the process got killed by a signal, it would never | |
| # be removed from subprocess._active, which triggered a FD and memory | |
| # leak. | |
| # spawn a Popen, delete its reference and kill it | |
| p = subprocess.Popen([sys.executable, "-c", | |
| 'import time;' | |
| 'time.sleep(3)'], | |
| stdout=subprocess.PIPE, | |
| stderr=subprocess.PIPE) | |
| self.addCleanup(p.stdout.close) | |
| self.addCleanup(p.stderr.close) | |
| ident = id(p) | |
| pid = p.pid | |
| with warnings_helper.check_warnings(('', ResourceWarning)): | |
| p = None | |
| support.gc_collect() # For PyPy or other GCs. | |
| os.kill(pid, signal.SIGKILL) | |
| if mswindows: | |
| # subprocess._active is not used on Windows and is set to None. | |
| self.assertIsNone(subprocess._active) | |
| else: | |
| # check that p is in the active processes list | |
| self.assertIn(ident, [id(o) for o in subprocess._active]) | |
| # let some time for the process to exit, and create a new Popen: this | |
| # should trigger the wait() of p | |
| time.sleep(0.2) | |
| with self.assertRaises(OSError): | |
| with subprocess.Popen(NONEXISTING_CMD, | |
| stdout=subprocess.PIPE, | |
| stderr=subprocess.PIPE) as proc: | |
| pass | |
| # p should have been wait()ed on, and removed from the _active list | |
| self.assertRaises(OSError, os.waitpid, pid, 0) | |
| if mswindows: | |
| # subprocess._active is not used on Windows and is set to None. | |
| self.assertIsNone(subprocess._active) | |
| else: | |
| self.assertNotIn(ident, [id(o) for o in subprocess._active]) | |
| def test_close_fds_after_preexec(self): | |
| fd_status = support.findfile("fd_status.py", subdir="subprocessdata") | |
| # this FD is used as dup2() target by preexec_fn, and should be closed | |
| # in the child process | |
| fd = os.dup(1) | |
| self.addCleanup(os.close, fd) | |
| p = subprocess.Popen([sys.executable, fd_status], | |
| stdout=subprocess.PIPE, close_fds=True, | |
| preexec_fn=lambda: os.dup2(1, fd)) | |
| output, ignored = p.communicate() | |
| remaining_fds = set(map(int, output.split(b','))) | |
| self.assertNotIn(fd, remaining_fds) | |
| @support.cpython_only | |
| def test_fork_exec(self): | |
| # Issue #22290: fork_exec() must not crash on memory allocation failure | |
| # or other errors | |
| import _posixsubprocess | |
| gc_enabled = gc.isenabled() | |
| try: | |
| # Use a preexec function and enable the garbage collector | |
| # to force fork_exec() to re-enable the garbage collector | |
| # on error. | |
| func = lambda: None | |
| gc.enable() | |
| for args, exe_list, cwd, env_list in ( | |
| (123, [b"exe"], None, [b"env"]), | |
| ([b"arg"], 123, None, [b"env"]), | |
| ([b"arg"], [b"exe"], 123, [b"env"]), | |
| ([b"arg"], [b"exe"], None, 123), | |
| ): | |
| with self.assertRaises(TypeError) as err: | |
| _posixsubprocess.fork_exec( | |
| args, exe_list, | |
| True, (), cwd, env_list, | |
| -1, -1, -1, -1, | |
| 1, 2, 3, 4, | |
| True, True, 0, | |
| False, [], 0, -1, | |
| func, False) | |
| # Attempt to prevent | |
| # "TypeError: fork_exec() takes exactly N arguments (M given)" | |
| # from passing the test. More refactoring to have us start | |
| # with a valid *args list, confirm a good call with that works | |
| # before mutating it in various ways to ensure that bad calls | |
| # with individual arg type errors raise a typeerror would be | |
| # ideal. Saving that for a future PR... | |
| self.assertNotIn('takes exactly', str(err.exception)) | |
| finally: | |
| if not gc_enabled: | |
| gc.disable() | |
| @support.cpython_only | |
| def test_fork_exec_sorted_fd_sanity_check(self): | |
| # Issue #23564: sanity check the fork_exec() fds_to_keep sanity check. | |
| import _posixsubprocess | |
| class BadInt: | |
| first = True | |
| def __init__(self, value): | |
| self.value = value | |
| def __int__(self): | |
| if self.first: | |
| self.first = False | |
| return self.value | |
| raise ValueError | |
| gc_enabled = gc.isenabled() | |
| try: | |
| gc.enable() | |
| for fds_to_keep in ( | |
| (-1, 2, 3, 4, 5), # Negative number. | |
| ('str', 4), # Not an int. | |
| (18, 23, 42, 2**63), # Out of range. | |
| (5, 4), # Not sorted. | |
| (6, 7, 7, 8), # Duplicate. | |
| (BadInt(1), BadInt(2)), | |
| ): | |
| with self.assertRaises( | |
| ValueError, | |
| msg='fds_to_keep={}'.format(fds_to_keep)) as c: | |
| _posixsubprocess.fork_exec( | |
| [b"false"], [b"false"], | |
| True, fds_to_keep, None, [b"env"], | |
| -1, -1, -1, -1, | |
| 1, 2, 3, 4, | |
| True, True, 0, | |
| None, None, None, -1, | |
| None) | |
| self.assertIn('fds_to_keep', str(c.exception)) | |
| finally: | |
| if not gc_enabled: | |
| gc.disable() | |
| def test_communicate_BrokenPipeError_stdin_close(self): | |
| # By not setting stdout or stderr or a timeout we force the fast path | |
| # that just calls _stdin_write() internally due to our mock. | |
| proc = subprocess.Popen(ZERO_RETURN_CMD) | |
| with proc, mock.patch.object(proc, 'stdin') as mock_proc_stdin: | |
| mock_proc_stdin.close.side_effect = BrokenPipeError | |
| proc.communicate() # Should swallow BrokenPipeError from close. | |
| mock_proc_stdin.close.assert_called_with() | |
| def test_communicate_BrokenPipeError_stdin_write(self): | |
| # By not setting stdout or stderr or a timeout we force the fast path | |
| # that just calls _stdin_write() internally due to our mock. | |
| proc = subprocess.Popen(ZERO_RETURN_CMD) | |
| with proc, mock.patch.object(proc, 'stdin') as mock_proc_stdin: | |
| mock_proc_stdin.write.side_effect = BrokenPipeError | |
| proc.communicate(b'stuff') # Should swallow the BrokenPipeError. | |
| mock_proc_stdin.write.assert_called_once_with(b'stuff') | |
| mock_proc_stdin.close.assert_called_once_with() | |
| def test_communicate_BrokenPipeError_stdin_flush(self): | |
| # Setting stdin and stdout forces the ._communicate() code path. | |
| # python -h exits faster than python -c pass (but spams stdout). | |
| proc = subprocess.Popen([sys.executable, '-h'], | |
| stdin=subprocess.PIPE, | |
| stdout=subprocess.PIPE) | |
| with proc, mock.patch.object(proc, 'stdin') as mock_proc_stdin, \ | |
| open(os.devnull, 'wb') as dev_null: | |
| mock_proc_stdin.flush.side_effect = BrokenPipeError | |
| # because _communicate registers a selector using proc.stdin... | |
| mock_proc_stdin.fileno.return_value = dev_null.fileno() | |
| # _communicate() should swallow BrokenPipeError from flush. | |
| proc.communicate(b'stuff') | |
| mock_proc_stdin.flush.assert_called_once_with() | |
| def test_communicate_BrokenPipeError_stdin_close_with_timeout(self): | |
| # Setting stdin and stdout forces the ._communicate() code path. | |
| # python -h exits faster than python -c pass (but spams stdout). | |
| proc = subprocess.Popen([sys.executable, '-h'], | |
| stdin=subprocess.PIPE, | |
| stdout=subprocess.PIPE) | |
| with proc, mock.patch.object(proc, 'stdin') as mock_proc_stdin: | |
| mock_proc_stdin.close.side_effect = BrokenPipeError | |
| # _communicate() should swallow BrokenPipeError from close. | |
| proc.communicate(timeout=999) | |
| mock_proc_stdin.close.assert_called_once_with() | |
| @unittest.skipUnless(_testcapi is not None | |
| and hasattr(_testcapi, 'W_STOPCODE'), | |
| 'need _testcapi.W_STOPCODE') | |
| def test_stopped(self): | |
| """Test wait() behavior when waitpid returns WIFSTOPPED; issue29335.""" | |
| args = ZERO_RETURN_CMD | |
| proc = subprocess.Popen(args) | |
| # Wait until the real process completes to avoid zombie process | |
| support.wait_process(proc.pid, exitcode=0) | |
| status = _testcapi.W_STOPCODE(3) | |
| with mock.patch('subprocess.os.waitpid', return_value=(proc.pid, status)): | |
| returncode = proc.wait() | |
| self.assertEqual(returncode, -3) | |
| def test_send_signal_race(self): | |
| # bpo-38630: send_signal() must poll the process exit status to reduce | |
| # the risk of sending the signal to the wrong process. | |
| proc = subprocess.Popen(ZERO_RETURN_CMD) | |
| # wait until the process completes without using the Popen APIs. | |
| support.wait_process(proc.pid, exitcode=0) | |
| # returncode is still None but the process completed. | |
| self.assertIsNone(proc.returncode) | |
| with mock.patch("os.kill") as mock_kill: | |
| proc.send_signal(signal.SIGTERM) | |
| # send_signal() didn't call os.kill() since the process already | |
| # completed. | |
| mock_kill.assert_not_called() | |
| # Don't check the returncode value: the test reads the exit status, | |
| # so Popen failed to read it and uses a default returncode instead. | |
| self.assertIsNotNone(proc.returncode) | |
| def test_send_signal_race2(self): | |
| # bpo-40550: the process might exist between the returncode check and | |
| # the kill operation | |
| p = subprocess.Popen([sys.executable, '-c', 'exit(1)']) | |
| # wait for process to exit | |
| while not p.returncode: | |
| p.poll() | |
| with mock.patch.object(p, 'poll', new=lambda: None): | |
| p.returncode = None | |
| p.send_signal(signal.SIGTERM) | |
| p.kill() | |
| def test_communicate_repeated_call_after_stdout_close(self): | |
| proc = subprocess.Popen([sys.executable, '-c', | |
| 'import os, time; os.close(1), time.sleep(2)'], | |
| stdout=subprocess.PIPE) | |
| while True: | |
| try: | |
| proc.communicate(timeout=0.1) | |
| return | |
| except subprocess.TimeoutExpired: | |
| pass | |
| def test_preexec_at_exit(self): | |
| code = f"""if 1: | |
| import atexit | |
| import subprocess | |
| def dummy(): | |
| pass | |
| class AtFinalization: | |
| def __del__(self): | |
| print("OK") | |
| subprocess.Popen({ZERO_RETURN_CMD}, preexec_fn=dummy) | |
| print("shouldn't be printed") | |
| at_finalization = AtFinalization() | |
| """ | |
| _, out, err = assert_python_ok("-c", code) | |
| 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.") | |
| @strace_helper.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. | |
| # Are intersted in the system calls: | |
| # clone,clone2,clone3,fork,vfork,exit,exit_group | |
| # Unfortunately using `--trace` with that list to strace fails because | |
| # not all are supported on all platforms (ex. clone2 is ia64 only...) | |
| # So instead use `%process` which is recommended by strace, and contains | |
| # the above. | |
| true_binary = "/bin/true" | |
| strace_args = ["--trace=%process"] | |
| with self.subTest(name="default_is_vfork"): | |
| vfork_result = strace_helper.strace_python( | |
| f"""\ | |
| import subprocess | |
| subprocess.check_call([{true_binary!r}])""", | |
| strace_args | |
| ) | |
| # Match both vfork() and clone(..., flags=...|CLONE_VFORK|...) | |
| self.assertRegex(vfork_result.event_bytes, 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 ( | |
| ("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 = strace_helper.strace_python( | |
| f"""\ | |
| import subprocess | |
| {preamble} | |
| try: | |
| subprocess.check_call( | |
| [{true_binary!r}], **dict({sp_kwarg})) | |
| except PermissionError: | |
| if not {expect_permission_error}: | |
| raise""", | |
| strace_args | |
| ) | |
| # Ensure neither vfork() or clone(..., flags=...|CLONE_VFORK|...). | |
| self.assertNotRegex(non_vfork_result.event_bytes, br"(?i)vfork") | |
| @unittest.skipUnless(mswindows, "Windows specific tests") | |
| class Win32ProcessTestCase(BaseTestCase): | |
| def test_createprocess_bad_cwd_includes_filename(self): | |
| # gh-119646: invalid cwd should appear on OSError.filename. | |
| missing_cwd = r'C:\some\nonexistent\directory' | |
| with self.assertRaises(OSError) as c: | |
| subprocess.Popen([sys.executable, '-c', 'pass'], cwd=missing_cwd) | |
| self.assertEqual(c.exception.filename, missing_cwd) | |
| self.assertEqual(c.exception.winerror, 267) | |
| def test_command_string_filename_omits_later_args(self): | |
| # gh-119646: a command-line string must not put later arguments | |
| # on OSError.filename. Those arguments can hold secrets. | |
| missing = r'C:\opt\nonexistent_binary' | |
| secret = 'NOT-A-REAL-SECRET' | |
| quoted = r'C:\Program Files\nonexistent_binary' | |
| cases = [ | |
| (missing, missing), | |
| (f'{missing} --token {secret}', missing), | |
| (f'"{missing}" --token {secret}', missing), | |
| (f'"{quoted}" --token {secret}', quoted), | |
| ] | |
| for command, expected in cases: | |
| with self.subTest(command=command): | |
| with self.assertRaises(FileNotFoundError) as c: | |
| subprocess.call(command) | |
| self.assertEqual(c.exception.filename, expected) | |
| self.assertNotIn(secret, c.exception.filename or '') | |
| self.assertNotIn(secret, str(c.exception)) | |
| def test_startupinfo(self): | |
| # startupinfo argument | |
| # We uses hardcoded constants, because we do not want to | |
| # depend on win32all. | |
| STARTF_USESHOWWINDOW = 1 | |
| SW_MAXIMIZE = 3 | |
| startupinfo = subprocess.STARTUPINFO() | |
| startupinfo.dwFlags = STARTF_USESHOWWINDOW | |
| startupinfo.wShowWindow = SW_MAXIMIZE | |
| # Since Python is a console process, it won't be affected | |
| # by wShowWindow, but the argument should be silently | |
| # ignored | |
| rc = subprocess.call(ZERO_RETURN_CMD, | |
| startupinfo=startupinfo) | |
| self.assertEqual(rc, 0) | |
| def test_startupinfo_keywords(self): | |
| # startupinfo argument | |
| # We use hardcoded constants, because we do not want to | |
| # depend on win32all. | |
| STARTF_USERSHOWWINDOW = 1 | |
| SW_MAXIMIZE = 3 | |
| startupinfo = subprocess.STARTUPINFO( | |
| dwFlags=STARTF_USERSHOWWINDOW, | |
| wShowWindow=SW_MAXIMIZE | |
| ) | |
| # Since Python is a console process, it won't be affected | |
| # by wShowWindow, but the argument should be silently | |
| # ignored | |
| rc = subprocess.call(ZERO_RETURN_CMD, | |
| startupinfo=startupinfo) | |
| self.assertEqual(rc, 0) | |
| def test_startupinfo_copy(self): | |
| # bpo-34044: Popen must not modify input STARTUPINFO structure | |
| startupinfo = subprocess.STARTUPINFO() | |
| startupinfo.dwFlags = subprocess.STARTF_USESHOWWINDOW | |
| startupinfo.wShowWindow = subprocess.SW_HIDE | |
| # Call Popen() twice with the same startupinfo object to make sure | |
| # that it's not modified | |
| for _ in range(2): | |
| cmd = ZERO_RETURN_CMD | |
| with open(os.devnull, 'w') as null: | |
| proc = subprocess.Popen(cmd, | |
| stdout=null, | |
| stderr=subprocess.STDOUT, | |
| startupinfo=startupinfo) | |
| with proc: | |
| proc.communicate() | |
| self.assertEqual(proc.returncode, 0) | |
| self.assertEqual(startupinfo.dwFlags, | |
| subprocess.STARTF_USESHOWWINDOW) | |
| self.assertIsNone(startupinfo.hStdInput) | |
| self.assertIsNone(startupinfo.hStdOutput) | |
| self.assertIsNone(startupinfo.hStdError) | |
| self.assertEqual(startupinfo.wShowWindow, subprocess.SW_HIDE) | |
| self.assertEqual(startupinfo.lpAttributeList, {"handle_list": []}) | |
| def test_startupinfo_shell_show_window(self): | |
| # gh-85028: shell=True must not override wShowWindow set by the caller | |
| import _winapi | |
| SW_MAXIMIZE = 3 | |
| used = [] | |
| create_process = _winapi.CreateProcess | |
| def spy(*args): | |
| # The startup info is the last argument of CreateProcess() | |
| used.append(args[-1]) | |
| return create_process(*args) | |
| startupinfo = subprocess.STARTUPINFO( | |
| dwFlags=subprocess.STARTF_USESHOWWINDOW, | |
| wShowWindow=SW_MAXIMIZE) | |
| with mock.patch.object(_winapi, 'CreateProcess', spy): | |
| rc = subprocess.call(ZERO_RETURN_CMD, shell=True, | |
| startupinfo=startupinfo) | |
| self.assertEqual(rc, 0) | |
| rc = subprocess.call(ZERO_RETURN_CMD, shell=True) | |
| self.assertEqual(rc, 0) | |
| requested, default = used | |
| self.assertEqual(requested.wShowWindow, SW_MAXIMIZE) | |
| # Without STARTF_USESHOWWINDOW the shell window is still hidden. | |
| self.assertTrue(default.dwFlags & subprocess.STARTF_USESHOWWINDOW) | |
| self.assertEqual(default.wShowWindow, subprocess.SW_HIDE) | |
| # CREATE_NEW_CONSOLE creates a "popup" window. | |
| @support.requires_resource('gui') | |
| def test_creationflags(self): | |
| # creationflags argument | |
| CREATE_NEW_CONSOLE = 16 | |
| sys.stderr.write(" a DOS box should flash briefly ...\n") | |
| rc = subprocess.call(sys.executable + | |
| ' -c "import time; time.sleep(0.25)"', | |
| creationflags=CREATE_NEW_CONSOLE) | |
| support.skip_on_low_desktop_heap_memory_subprocess(rc) | |
| self.assertEqual(rc, 0) | |
| def test_invalid_args(self): | |
| # invalid arguments should raise ValueError | |
| self.assertRaises(ValueError, subprocess.call, | |
| [sys.executable, "-c", | |
| "import sys; sys.exit(47)"], | |
| preexec_fn=lambda: 1) | |
| @support.cpython_only | |
| def test_issue31471(self): | |
| # There shouldn't be an assertion failure in Popen() in case the env | |
| # argument has a bad keys() method. | |
| class BadEnv(dict): | |
| keys = None | |
| with self.assertRaises(TypeError): | |
| subprocess.Popen(ZERO_RETURN_CMD, env=BadEnv()) | |
| def test_close_fds(self): | |
| # close file descriptors | |
| rc = subprocess.call([sys.executable, "-c", | |
| "import sys; sys.exit(47)"], | |
| close_fds=True) | |
| self.assertEqual(rc, 47) | |
| def test_close_fds_with_stdio(self): | |
| import msvcrt | |
| fds = os.pipe() | |
| self.addCleanup(os.close, fds[0]) | |
| self.addCleanup(os.close, fds[1]) | |
| handles = [] | |
| for fd in fds: | |
| os.set_inheritable(fd, True) | |
| handles.append(msvcrt.get_osfhandle(fd)) | |
| p = subprocess.Popen([sys.executable, "-c", | |
| "import msvcrt; print(msvcrt.open_osfhandle({}, 0))".format(handles[0])], | |
| stdout=subprocess.PIPE, close_fds=False) | |
| stdout, stderr = p.communicate() | |
| self.assertEqual(p.returncode, 0) | |
| int(stdout.strip()) # Check that stdout is an integer | |
| p = subprocess.Popen([sys.executable, "-c", | |
| "import msvcrt; print(msvcrt.open_osfhandle({}, 0))".format(handles[0])], | |
| stdout=subprocess.PIPE, stderr=subprocess.PIPE, close_fds=True) | |
| stdout, stderr = p.communicate() | |
| self.assertEqual(p.returncode, 1) | |
| self.assertIn(b"OSError", stderr) | |
| # The same as the previous call, but with an empty handle_list | |
| handle_list = [] | |
| startupinfo = subprocess.STARTUPINFO() | |
| startupinfo.lpAttributeList = {"handle_list": handle_list} | |
| p = subprocess.Popen([sys.executable, "-c", | |
| "import msvcrt; print(msvcrt.open_osfhandle({}, 0))".format(handles[0])], | |
| stdout=subprocess.PIPE, stderr=subprocess.PIPE, | |
| startupinfo=startupinfo, close_fds=True) | |
| stdout, stderr = p.communicate() | |
| self.assertEqual(p.returncode, 1) | |
| self.assertIn(b"OSError", stderr) | |
| # Check for a warning due to using handle_list and close_fds=False | |
| with warnings_helper.check_warnings((".*overriding close_fds", | |
| RuntimeWarning)): | |
| startupinfo = subprocess.STARTUPINFO() | |
| startupinfo.lpAttributeList = {"handle_list": handles[:]} | |
| p = subprocess.Popen([sys.executable, "-c", | |
| "import msvcrt; print(msvcrt.open_osfhandle({}, 0))".format(handles[0])], | |
| stdout=subprocess.PIPE, stderr=subprocess.PIPE, | |
| startupinfo=startupinfo, close_fds=False) | |
| stdout, stderr = p.communicate() | |
| self.assertEqual(p.returncode, 0) | |
| def test_empty_attribute_list(self): | |
| startupinfo = subprocess.STARTUPINFO() | |
| startupinfo.lpAttributeList = {} | |
| rc = subprocess.call(ZERO_RETURN_CMD, | |
| startupinfo=startupinfo) | |
| self.assertEqual(rc, 0) | |
| def test_empty_handle_list(self): | |
| startupinfo = subprocess.STARTUPINFO() | |
| startupinfo.lpAttributeList = {"handle_list": []} | |
| rc = subprocess.call(ZERO_RETURN_CMD, | |
| startupinfo=startupinfo) | |
| self.assertEqual(rc, 0) | |
| def test_shell_sequence(self): | |
| # Run command through the shell (sequence) | |
| newenv = os.environ.copy() | |
| newenv["FRUIT"] = "physalis" | |
| p = subprocess.Popen(["set"], shell=1, | |
| stdout=subprocess.PIPE, | |
| env=newenv) | |
| with p: | |
| self.assertIn(b"physalis", p.stdout.read()) | |
| p.communicate() | |
| self.assertEqual(p.returncode, 0) | |
| def test_shell_string(self): | |
| # Run command through the shell (string) | |
| newenv = os.environ.copy() | |
| newenv["FRUIT"] = "physalis" | |
| p = subprocess.Popen("set", shell=1, | |
| stdout=subprocess.PIPE, | |
| env=newenv) | |
| with p: | |
| self.assertIn(b"physalis", p.stdout.read()) | |
| p.communicate() | |
| self.assertEqual(p.returncode, 0) | |
| def test_shell_encodings(self): | |
| # Run command through the shell (string) | |
| for enc in ['ansi', 'oem']: | |
| newenv = os.environ.copy() | |
| newenv["FRUIT"] = "physalis" | |
| p = subprocess.Popen("set", shell=1, | |
| stdout=subprocess.PIPE, | |
| env=newenv, | |
| encoding=enc) | |
| with p: | |
| self.assertIn("physalis", p.stdout.read(), enc) | |
| p.communicate() | |
| self.assertEqual(p.returncode, 0) | |
| def test_call_string(self): | |
| # call() function with string argument on Windows | |
| rc = subprocess.call(sys.executable + | |
| ' -c "import sys; sys.exit(47)"') | |
| self.assertEqual(rc, 47) | |
| def _kill_process(self, method, *args): | |
| # Some win32 buildbot raises EOFError if stdin is inherited | |
| p = subprocess.Popen([sys.executable, "-c", """if 1: | |
| import sys, time | |
| sys.stdout.write('x\\n') | |
| sys.stdout.flush() | |
| time.sleep(30) | |
| """], | |
| stdin=subprocess.PIPE, | |
| stdout=subprocess.PIPE, | |
| stderr=subprocess.PIPE) | |
| with p: | |
| # Wait for the interpreter to be completely initialized before | |
| # sending any signal. | |
| p.stdout.read(1) | |
| getattr(p, method)(*args) | |
| _, stderr = p.communicate() | |
| self.assertEqual(stderr, b'') | |
| returncode = p.wait() | |
| self.assertNotEqual(returncode, 0) | |
| def _kill_dead_process(self, method, *args): | |
| p = subprocess.Popen([sys.executable, "-c", """if 1: | |
| import sys, time | |
| sys.stdout.write('x\\n') | |
| sys.stdout.flush() | |
| sys.exit(42) | |
| """], | |
| stdin=subprocess.PIPE, | |
| stdout=subprocess.PIPE, | |
| stderr=subprocess.PIPE) | |
| with p: | |
| # Wait for the interpreter to be completely initialized before | |
| # sending any signal. | |
| p.stdout.read(1) | |
| # The process should end after this | |
| time.sleep(1) | |
| # This shouldn't raise even though the child is now dead | |
| getattr(p, method)(*args) | |
| _, stderr = p.communicate() | |
| self.assertEqual(stderr, b'') | |
| rc = p.wait() | |
| self.assertEqual(rc, 42) | |
| def test_send_signal(self): | |
| self._kill_process('send_signal', signal.SIGTERM) | |
| def test_kill(self): | |
| self._kill_process('kill') | |
| def test_terminate(self): | |
| self._kill_process('terminate') | |
| def test_send_signal_dead(self): | |
| self._kill_dead_process('send_signal', signal.SIGTERM) | |
| def test_kill_dead(self): | |
| self._kill_dead_process('kill') | |
| def test_terminate_dead(self): | |
| self._kill_dead_process('terminate') | |
| class MiscTests(unittest.TestCase): | |
| class RecordingPopen(subprocess.Popen): | |
| """A Popen that saves a reference to each instance for testing.""" | |
| instances_created = [] | |
| def __init__(self, *args, **kwargs): | |
| super().__init__(*args, **kwargs) | |
| self.instances_created.append(self) | |
| @mock.patch.object(subprocess.Popen, "_communicate") | |
| def _test_keyboardinterrupt_no_kill(self, popener, mock__communicate, | |
| **kwargs): | |
| """Fake a SIGINT happening during Popen._communicate() and ._wait(). | |
| This avoids the need to actually try and get test environments to send | |
| and receive signals reliably across platforms. The net effect of a ^C | |
| happening during a blocking subprocess execution which we want to clean | |
| up from is a KeyboardInterrupt coming out of communicate() or wait(). | |
| """ | |
| mock__communicate.side_effect = KeyboardInterrupt | |
| try: | |
| with mock.patch.object(subprocess.Popen, "_wait") as mock__wait: | |
| # We patch out _wait() as no signal was involved so the | |
| # child process isn't actually going to exit rapidly. | |
| mock__wait.side_effect = KeyboardInterrupt | |
| with mock.patch.object(subprocess, "Popen", | |
| self.RecordingPopen): | |
| with self.assertRaises(KeyboardInterrupt): | |
| popener([sys.executable, "-c", | |
| "import time\ntime.sleep(9)\nimport sys\n" | |
| "sys.stderr.write('\\n!runaway child!\\n')"], | |
| stdout=subprocess.DEVNULL, **kwargs) | |
| for call in mock__wait.call_args_list[1:]: | |
| self.assertNotEqual( | |
| call, mock.call(timeout=None), | |
| "no open-ended wait() after the first allowed: " | |
| f"{mock__wait.call_args_list}") | |
| sigint_calls = [] | |
| for call in mock__wait.call_args_list: | |
| if call == mock.call(timeout=0.25): # from Popen.__init__ | |
| sigint_calls.append(call) | |
| self.assertLessEqual(mock__wait.call_count, 2, | |
| msg=mock__wait.call_args_list) | |
| self.assertEqual(len(sigint_calls), 1, | |
| msg=mock__wait.call_args_list) | |
| finally: | |
| # cleanup the forgotten (due to our mocks) child process | |
| process = self.RecordingPopen.instances_created.pop() | |
| process.kill() | |
| process.wait() | |
| self.assertEqual([], self.RecordingPopen.instances_created) | |
| def test_call_keyboardinterrupt_no_kill(self): | |
| self._test_keyboardinterrupt_no_kill(subprocess.call, timeout=6.282) | |
| def test_run_keyboardinterrupt_no_kill(self): | |
| self._test_keyboardinterrupt_no_kill(subprocess.run, timeout=6.282) | |
| def test_context_manager_keyboardinterrupt_no_kill(self): | |
| def popen_via_context_manager(*args, **kwargs): | |
| with subprocess.Popen(*args, **kwargs) as unused_process: | |
| raise KeyboardInterrupt # Test how __exit__ handles ^C. | |
| self._test_keyboardinterrupt_no_kill(popen_via_context_manager) | |
| def test_getoutput(self): | |
| self.assertEqual(subprocess.getoutput('echo xyzzy'), 'xyzzy') | |
| self.assertEqual(subprocess.getstatusoutput('echo xyzzy'), | |
| (0, 'xyzzy')) | |
| # we use mkdtemp in the next line to create an empty directory | |
| # under our exclusive control; from that, we can invent a pathname | |
| # that we _know_ won't exist. This is guaranteed to fail. | |
| dir = None | |
| try: | |
| dir = tempfile.mkdtemp() | |
| name = os.path.join(dir, "foo") | |
| status, output = subprocess.getstatusoutput( | |
| ("type " if mswindows else "cat ") + name) | |
| self.assertNotEqual(status, 0) | |
| finally: | |
| if dir is not None: | |
| os.rmdir(dir) | |
| def test__all__(self): | |
| """Ensure that __all__ is populated properly.""" | |
| intentionally_excluded = {"list2cmdline", "Handle", "pwd", "grp", "fcntl"} | |
| exported = set(subprocess.__all__) | |
| possible_exports = set() | |
| import types | |
| for name, value in subprocess.__dict__.items(): | |
| if name.startswith('_'): | |
| continue | |
| if isinstance(value, (types.ModuleType,)): | |
| continue | |
| possible_exports.add(name) | |
| self.assertEqual(exported, possible_exports - intentionally_excluded) | |
| @unittest.skipUnless(hasattr(selectors, 'PollSelector'), | |
| "Test needs selectors.PollSelector") | |
| class ProcessTestCaseNoPoll(ProcessTestCase): | |
| def setUp(self): | |
| self.orig_selector = subprocess._PopenSelector | |
| subprocess._PopenSelector = selectors.SelectSelector | |
| ProcessTestCase.setUp(self) | |
| def tearDown(self): | |
| subprocess._PopenSelector = self.orig_selector | |
| ProcessTestCase.tearDown(self) | |
| @unittest.skipUnless(mswindows, "Windows-specific tests") | |
| class CommandsWithSpaces (BaseTestCase): | |
| def setUp(self): | |
| super().setUp() | |
| f, fname = tempfile.mkstemp(".py", "te st") | |
| self.fname = fname.lower () | |
| os.write(f, b"import sys;" | |
| b"sys.stdout.write('%d %s' % (len(sys.argv), [a.lower () for a in sys.argv]))" | |
| ) | |
| os.close(f) | |
| def tearDown(self): | |
| os.remove(self.fname) | |
| super().tearDown() | |
| def with_spaces(self, *args, **kwargs): | |
| kwargs['stdout'] = subprocess.PIPE | |
| p = subprocess.Popen(*args, **kwargs) | |
| with p: | |
| self.assertEqual( | |
| p.stdout.read ().decode("mbcs"), | |
| "2 [%r, 'ab cd']" % self.fname | |
| ) | |
| def test_shell_string_with_spaces(self): | |
| # call() function with string argument with spaces on Windows | |
| self.with_spaces('"%s" "%s" "%s"' % (sys.executable, self.fname, | |
| "ab cd"), shell=1) | |
| def test_shell_sequence_with_spaces(self): | |
| # call() function with sequence argument with spaces on Windows | |
| self.with_spaces([sys.executable, self.fname, "ab cd"], shell=1) | |
| def test_noshell_string_with_spaces(self): | |
| # call() function with string argument with spaces on Windows | |
| self.with_spaces('"%s" "%s" "%s"' % (sys.executable, self.fname, | |
| "ab cd")) | |
| def test_noshell_sequence_with_spaces(self): | |
| # call() function with sequence argument with spaces on Windows | |
| self.with_spaces([sys.executable, self.fname, "ab cd"]) | |
| class ContextManagerTests(BaseTestCase): | |
| def test_pipe(self): | |
| with subprocess.Popen([sys.executable, "-c", | |
| "import sys;" | |
| "sys.stdout.write('stdout');" | |
| "sys.stderr.write('stderr');"], | |
| stdout=subprocess.PIPE, | |
| stderr=subprocess.PIPE) as proc: | |
| self.assertEqual(proc.stdout.read(), b"stdout") | |
| self.assertEqual(proc.stderr.read(), b"stderr") | |
| self.assertTrue(proc.stdout.closed) | |
| self.assertTrue(proc.stderr.closed) | |
| def test_returncode(self): | |
| with subprocess.Popen([sys.executable, "-c", | |
| "import sys; sys.exit(100)"]) as proc: | |
| pass | |
| # __exit__ calls wait(), so the returncode should be set | |
| self.assertEqual(proc.returncode, 100) | |
| def test_communicate_stdin(self): | |
| with subprocess.Popen([sys.executable, "-c", | |
| "import sys;" | |
| "sys.exit(sys.stdin.read() == 'context')"], | |
| stdin=subprocess.PIPE) as proc: | |
| proc.communicate(b"context") | |
| self.assertEqual(proc.returncode, 1) | |
| def test_invalid_args(self): | |
| with self.assertRaises(NONEXISTING_ERRORS): | |
| with subprocess.Popen(NONEXISTING_CMD, | |
| stdout=subprocess.PIPE, | |
| stderr=subprocess.PIPE) as proc: | |
| pass | |
| def test_broken_pipe_cleanup(self): | |
| """Broken pipe error should not prevent wait() (Issue 21619)""" | |
| proc = subprocess.Popen(ZERO_RETURN_CMD, | |
| stdin=subprocess.PIPE, | |
| bufsize=support.PIPE_MAX_SIZE*2) | |
| proc = proc.__enter__() | |
| # Prepare to send enough data to overflow any OS pipe buffering and | |
| # guarantee a broken pipe error. Data is held in BufferedWriter | |
| # buffer until closed. | |
| proc.stdin.write(b'x' * support.PIPE_MAX_SIZE) | |
| self.assertIsNone(proc.returncode) | |
| # EPIPE expected under POSIX; EINVAL under Windows | |
| self.assertRaises(OSError, proc.__exit__, None, None, None) | |
| self.assertEqual(proc.returncode, 0) | |
| self.assertTrue(proc.stdin.closed) | |
| class FastWaitTestCase(BaseTestCase): | |
| """Tests for efficient (pidfd_open() + poll() / kqueue()) process | |
| waiting in subprocess.Popen.wait(). | |
| """ | |
| CAN_USE_PIDFD_OPEN = subprocess._CAN_USE_PIDFD_OPEN | |
| CAN_USE_KQUEUE = subprocess._CAN_USE_KQUEUE | |
| COMMAND = [sys.executable, "-c", "import time; time.sleep(0.3)"] | |
| WAIT_TIMEOUT = 0.0001 # 0.1 ms | |
| def assert_fast_waitpid_error(self, patch_point): | |
| # Emulate a case where pidfd_open() or kqueue() fails. | |
| # Busy-poll wait should be used as fallback. | |
| exc = OSError(errno.EMFILE, os.strerror(errno.EMFILE)) | |
| with mock.patch(patch_point, side_effect=exc) as m: | |
| p = subprocess.Popen(self.COMMAND) | |
| with self.assertRaises(subprocess.TimeoutExpired): | |
| p.wait(self.WAIT_TIMEOUT) | |
| self.assertEqual(p.wait(timeout=support.SHORT_TIMEOUT), 0) | |
| self.assertTrue(m.called) | |
| @unittest.skipIf(not CAN_USE_PIDFD_OPEN, reason="needs pidfd_open()") | |
| def test_wait_pidfd_open_error(self): | |
| self.assert_fast_waitpid_error("os.pidfd_open") | |
| @unittest.skipIf(not CAN_USE_KQUEUE, reason="needs kqueue() for proc") | |
| def test_wait_kqueue_error(self): | |
| self.assert_fast_waitpid_error("select.kqueue") | |
| @unittest.skipIf(not CAN_USE_KQUEUE, reason="needs kqueue() for proc") | |
| def test_kqueue_control_error(self): | |
| # Emulate a case where kqueue.control() fails. Busy-poll wait | |
| # should be used as fallback. | |
| p = subprocess.Popen(self.COMMAND) | |
| kq_mock = mock.Mock() | |
| kq_mock.control.side_effect = OSError( | |
| errno.EPERM, os.strerror(errno.EPERM) | |
| ) | |
| kq_mock.close = mock.Mock() | |
| with mock.patch("select.kqueue", return_value=kq_mock) as m: | |
| with self.assertRaises(subprocess.TimeoutExpired): | |
| p.wait(self.WAIT_TIMEOUT) | |
| self.assertEqual(p.wait(timeout=support.SHORT_TIMEOUT), 0) | |
| self.assertTrue(m.called) | |
| def assert_wait_race_condition(self, patch_target, real_func): | |
| # Call pidfd_open() / kqueue(), then terminate the process. | |
| # Make sure that the wait call (poll() / kqueue.control()) | |
| # still works for a terminated PID. | |
| p = subprocess.Popen(self.COMMAND) | |
| def wrapper(*args, **kwargs): | |
| ret = real_func(*args, **kwargs) | |
| try: | |
| os.kill(p.pid, signal.SIGTERM) | |
| os.waitpid(p.pid, 0) | |
| except OSError: | |
| pass | |
| return ret | |
| with mock.patch(patch_target, side_effect=wrapper) as m: | |
| status = p.wait(timeout=support.SHORT_TIMEOUT) | |
| self.assertTrue(m.called) | |
| self.assertEqual(status, 0) | |
| @unittest.skipIf(not CAN_USE_PIDFD_OPEN, reason="needs pidfd_open()") | |
| def test_pidfd_open_race(self): | |
| self.assert_wait_race_condition("os.pidfd_open", os.pidfd_open) | |
| @unittest.skipIf(not CAN_USE_KQUEUE, reason="needs kqueue() for proc") | |
| def test_kqueue_race(self): | |
| self.assert_wait_race_condition("select.kqueue", select.kqueue) | |
| def assert_notification_without_immediate_reap(self, patch_target): | |
| # Verify fallback to busy polling when poll() / kqueue() | |
| # succeeds, but waitpid(pid, WNOHANG) returns (0, 0). | |
| def waitpid_wrapper(pid, flags): | |
| nonlocal ncalls | |
| ncalls += 1 | |
| if ncalls == 1: | |
| return (0, 0) | |
| return real_waitpid(pid, flags) | |
| ncalls = 0 | |
| real_waitpid = os.waitpid | |
| with mock.patch.object(subprocess.Popen, patch_target, return_value=True) as m1: | |
| with mock.patch("os.waitpid", side_effect=waitpid_wrapper) as m2: | |
| p = subprocess.Popen(self.COMMAND) | |
| with self.assertRaises(subprocess.TimeoutExpired): | |
| p.wait(self.WAIT_TIMEOUT) | |
| self.assertEqual(p.wait(timeout=support.SHORT_TIMEOUT), 0) | |
| self.assertTrue(m1.called) | |
| self.assertTrue(m2.called) | |
| @unittest.skipIf(not CAN_USE_PIDFD_OPEN, reason="needs pidfd_open()") | |
| def test_pidfd_open_notification_without_immediate_reap(self): | |
| self.assert_notification_without_immediate_reap("_wait_pidfd") | |
| @unittest.skipIf(not CAN_USE_KQUEUE, reason="needs kqueue() for proc") | |
| def test_kqueue_notification_without_immediate_reap(self): | |
| self.assert_notification_without_immediate_reap("_wait_kqueue") | |
| @unittest.skipUnless( | |
| CAN_USE_PIDFD_OPEN or CAN_USE_KQUEUE, | |
| "fast wait mechanism not available" | |
| ) | |
| def test_fast_path_avoid_busy_loop(self): | |
| # assert that the busy loop is not called as long as the fast | |
| # wait is available | |
| with mock.patch('time.sleep') as m: | |
| p = subprocess.Popen(self.COMMAND) | |
| with self.assertRaises(subprocess.TimeoutExpired): | |
| p.wait(self.WAIT_TIMEOUT) | |
| self.assertEqual(p.wait(timeout=support.LONG_TIMEOUT), 0) | |
| self.assertFalse(m.called) | |
| @unittest.skipIf(mswindows, "requires the POSIX wait implementation") | |
| def test_wait_huge_timeout(self): | |
| # gh-154836: very large timeout values used to overflow the C | |
| # timestamp conversion in poll() / kqueue.control() and raise | |
| # OverflowError / TypeError. | |
| for timeout in (10**10, sys.maxsize, float('inf')): | |
| with self.subTest(timeout=timeout): | |
| p = subprocess.Popen(ZERO_RETURN_CMD) | |
| self.assertEqual(p.wait(timeout=timeout), 0) | |
| @unittest.skipIf(mswindows, "requires the POSIX wait implementation") | |
| def test_run_huge_timeout(self): | |
| # gh-154836: same as test_wait_huge_timeout, via the | |
| # subprocess.run() / communicate() code path. | |
| cp = subprocess.run(ZERO_RETURN_CMD, timeout=1e10) | |
| self.assertEqual(cp.returncode, 0) | |
| @unittest.skipIf(mswindows, "requires the POSIX wait implementation") | |
| def test_wait_slices_do_not_expire_early(self): | |
| # A clamped wait slice must not raise TimeoutExpired before the | |
| # real deadline: with a tiny slice limit, a process that | |
| # outlives many slices must still be waited for successfully. | |
| with mock.patch.object(subprocess, "_MAXIMUM_WAIT_TIMEOUT", 0.01): | |
| p = subprocess.Popen(self.COMMAND) # sleeps 0.3s | |
| self.assertEqual(p.wait(timeout=support.SHORT_TIMEOUT), 0) | |
| if __name__ == "__main__": | |
| unittest.main() |