Fixes Issue #26373: subprocess.Popen.communicate now correctly ignores

BrokenPipeError when the child process dies before .communicate()
is called in more (all?) circumstances.
parent ead9bfc5
...@@ -1011,8 +1011,7 @@ class Popen(object): ...@@ -1011,8 +1011,7 @@ class Popen(object):
try: try:
self.stdin.write(input) self.stdin.write(input)
except BrokenPipeError: except BrokenPipeError:
# communicate() must ignore broken pipe error pass # communicate() must ignore broken pipe errors.
pass
except OSError as e: except OSError as e:
if e.errno == errno.EINVAL and self.poll() is not None: if e.errno == errno.EINVAL and self.poll() is not None:
# Issue #19612: On Windows, stdin.write() fails with EINVAL # Issue #19612: On Windows, stdin.write() fails with EINVAL
...@@ -1020,7 +1019,15 @@ class Popen(object): ...@@ -1020,7 +1019,15 @@ class Popen(object):
pass pass
else: else:
raise raise
try:
self.stdin.close() self.stdin.close()
except BrokenPipeError:
pass # communicate() must ignore broken pipe errors.
except OSError as e:
if e.errno == errno.EINVAL and self.poll() is not None:
pass
else:
raise
def communicate(self, input=None, timeout=None): def communicate(self, input=None, timeout=None):
"""Interact with process: Send data to stdin. Read data from """Interact with process: Send data to stdin. Read data from
...@@ -1661,9 +1668,15 @@ class Popen(object): ...@@ -1661,9 +1668,15 @@ class Popen(object):
if self.stdin and not self._communication_started: if self.stdin and not self._communication_started:
# Flush stdio buffer. This might block, if the user has # Flush stdio buffer. This might block, if the user has
# been writing to .stdin in an uncontrolled fashion. # been writing to .stdin in an uncontrolled fashion.
try:
self.stdin.flush() self.stdin.flush()
except BrokenPipeError:
pass # communicate() must ignore BrokenPipeError.
if not input: if not input:
try:
self.stdin.close() self.stdin.close()
except BrokenPipeError:
pass # communicate() must ignore BrokenPipeError.
stdout = None stdout = None
stderr = None stderr = None
......
import unittest import unittest
from unittest import mock
from test.support import script_helper from test.support import script_helper
from test import support from test import support
import subprocess import subprocess
...@@ -1240,6 +1241,52 @@ class ProcessTestCase(BaseTestCase): ...@@ -1240,6 +1241,52 @@ class ProcessTestCase(BaseTestCase):
fds_after_exception = os.listdir(fd_directory) fds_after_exception = os.listdir(fd_directory)
self.assertEqual(fds_before_popen, fds_after_exception) self.assertEqual(fds_before_popen, fds_after_exception)
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([sys.executable, '-c', 'pass'])
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([sys.executable, '-c', 'pass'])
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('/dev/null', '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()
class RunFuncTestCase(BaseTestCase): class RunFuncTestCase(BaseTestCase):
def run_python(self, code, **kwargs): def run_python(self, code, **kwargs):
......
...@@ -128,6 +128,10 @@ Core and Builtins ...@@ -128,6 +128,10 @@ Core and Builtins
Library Library
------- -------
- Issue #26373: subprocess.Popen.communicate now correctly ignores
BrokenPipeError when the child process dies before .communicate()
is called in more/all circumstances.
- Issue #21776: distutils.upload now correctly handles HTTPError. - Issue #21776: distutils.upload now correctly handles HTTPError.
Initial patch by Claudiu Popa. Initial patch by Claudiu Popa.
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment