Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit 5135992

Browse files
committed
Fixes issue #12268: File readline, readlines and read() or readall() methods
no longer lose data when an underlying read system call is interrupted. IOError is no longer raised due to a read system call returning EINTR from within these methods.
1 parent 8150492 commit 5135992

7 files changed

Lines changed: 290 additions & 8 deletions

File tree

Lib/test/test_file_eintr.py

Lines changed: 236 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,236 @@
1+
# Written to test interrupted system calls interfering with our many buffered
2+
# IO implementations. http://bugs.python.org/issue12268
3+
#
4+
# It was suggested that this code could be merged into test_io and the tests
5+
# made to work using the same method as the existing signal tests in test_io.
6+
# I was unable to get single process tests using alarm or setitimer that way
7+
# to reproduce the EINTR problems. This process based test suite reproduces
8+
# the problems prior to the issue12268 patch reliably on Linux and OSX.
9+
# - gregory.p.smith
10+
11+
import os
12+
import select
13+
import signal
14+
import subprocess
15+
import sys
16+
from test.support import run_unittest
17+
import time
18+
import unittest
19+
20+
# Test import all of the things we're about to try testing up front.
21+
from _io import FileIO
22+
23+
24+
@unittest.skipUnless(os.name == 'posix', 'tests requires a posix system.')
25+
class TestFileIOSignalInterrupt(unittest.TestCase):
26+
def setUp(self):
27+
self._process = None
28+
29+
def tearDown(self):
30+
if self._process and self._process.poll() is None:
31+
try:
32+
self._process.kill()
33+
except OSError:
34+
pass
35+
36+
def _generate_infile_setup_code(self):
37+
"""Returns the infile = ... line of code for the reader process.
38+
39+
subclasseses should override this to test different IO objects.
40+
"""
41+
return ('import _io ;'
42+
'infile = _io.FileIO(sys.stdin.fileno(), "rb")')
43+
44+
def fail_with_process_info(self, why, stdout=b'', stderr=b'',
45+
communicate=True):
46+
"""A common way to cleanup and fail with useful debug output.
47+
48+
Kills the process if it is still running, collects remaining output
49+
and fails the test with an error message including the output.
50+
51+
Args:
52+
why: Text to go after "Error from IO process" in the message.
53+
stdout, stderr: standard output and error from the process so
54+
far to include in the error message.
55+
communicate: bool, when True we call communicate() on the process
56+
after killing it to gather additional output.
57+
"""
58+
if self._process.poll() is None:
59+
time.sleep(0.1) # give it time to finish printing the error.
60+
try:
61+
self._process.terminate() # Ensure it dies.
62+
except OSError:
63+
pass
64+
if communicate:
65+
stdout_end, stderr_end = self._process.communicate()
66+
stdout += stdout_end
67+
stderr += stderr_end
68+
self.fail('Error from IO process %s:\nSTDOUT:\n%sSTDERR:\n%s\n' %
69+
(why, stdout.decode(), stderr.decode()))
70+
71+
def _test_reading(self, data_to_write, read_and_verify_code):
72+
"""Generic buffered read method test harness to validate EINTR behavior.
73+
74+
Also validates that Python signal handlers are run during the read.
75+
76+
Args:
77+
data_to_write: String to write to the child process for reading
78+
before sending it a signal, confirming the signal was handled,
79+
writing a final newline and closing the infile pipe.
80+
read_and_verify_code: Single "line" of code to read from a file
81+
object named 'infile' and validate the result. This will be
82+
executed as part of a python subprocess fed data_to_write.
83+
"""
84+
infile_setup_code = self._generate_infile_setup_code()
85+
# Total pipe IO in this function is smaller than the minimum posix OS
86+
# pipe buffer size of 512 bytes. No writer should block.
87+
assert len(data_to_write) < 512, 'data_to_write must fit in pipe buf.'
88+
89+
# Start a subprocess to call our read method while handling a signal.
90+
self._process = subprocess.Popen(
91+
[sys.executable, '-u', '-c',
92+
'import signal, sys ;'
93+
'signal.signal(signal.SIGINT, '
94+
'lambda s, f: sys.stderr.write("$\\n")) ;'
95+
+ infile_setup_code + ' ;' +
96+
'sys.stderr.write("Worm Sign!\\n") ;'
97+
+ read_and_verify_code + ' ;' +
98+
'infile.close()'
99+
],
100+
stdin=subprocess.PIPE, stdout=subprocess.PIPE,
101+
stderr=subprocess.PIPE)
102+
103+
# Wait for the signal handler to be installed.
104+
worm_sign = self._process.stderr.read(len(b'Worm Sign!\n'))
105+
if worm_sign != b'Worm Sign!\n': # See also, Dune by Frank Herbert.
106+
self.fail_with_process_info('while awaiting a sign',
107+
stderr=worm_sign)
108+
self._process.stdin.write(data_to_write)
109+
110+
signals_sent = 0
111+
rlist = []
112+
# We don't know when the read_and_verify_code in our child is actually
113+
# executing within the read system call we want to interrupt. This
114+
# loop waits for a bit before sending the first signal to increase
115+
# the likelihood of that. Implementations without correct EINTR
116+
# and signal handling usually fail this test.
117+
while not rlist:
118+
rlist, _, _ = select.select([self._process.stderr], (), (), 0.05)
119+
self._process.send_signal(signal.SIGINT)
120+
signals_sent += 1
121+
if signals_sent > 200:
122+
self._process.kill()
123+
self.fail('reader process failed to handle our signals.')
124+
# This assumes anything unexpected that writes to stderr will also
125+
# write a newline. That is true of the traceback printing code.
126+
signal_line = self._process.stderr.readline()
127+
if signal_line != b'$\n':
128+
self.fail_with_process_info('while awaiting signal',
129+
stderr=signal_line)
130+
131+
# We append a newline to our input so that a readline call can
132+
# end on its own before the EOF is seen and so that we're testing
133+
# the read call that was interrupted by a signal before the end of
134+
# the data stream has been reached.
135+
stdout, stderr = self._process.communicate(input=b'\n')
136+
if self._process.returncode:
137+
self.fail_with_process_info(
138+
'exited rc=%d' % self._process.returncode,
139+
stdout, stderr, communicate=False)
140+
# PASS!
141+
142+
# String format for the read_and_verify_code used by read methods.
143+
_READING_CODE_TEMPLATE = (
144+
'got = infile.{read_method_name}() ;'
145+
'expected = {expected!r} ;'
146+
'assert got == expected, ('
147+
'"{read_method_name} returned wrong data.\\n"'
148+
'"got data %r\\nexpected %r" % (got, expected))'
149+
)
150+
151+
def test_readline(self):
152+
"""readline() must handle signals and not lose data."""
153+
self._test_reading(
154+
data_to_write=b'hello, world!',
155+
read_and_verify_code=self._READING_CODE_TEMPLATE.format(
156+
read_method_name='readline',
157+
expected=b'hello, world!\n'))
158+
159+
def test_readlines(self):
160+
"""readlines() must handle signals and not lose data."""
161+
self._test_reading(
162+
data_to_write=b'hello\nworld!',
163+
read_and_verify_code=self._READING_CODE_TEMPLATE.format(
164+
read_method_name='readlines',
165+
expected=[b'hello\n', b'world!\n']))
166+
167+
def test_readall(self):
168+
"""readall() must handle signals and not lose data."""
169+
self._test_reading(
170+
data_to_write=b'hello\nworld!',
171+
read_and_verify_code=self._READING_CODE_TEMPLATE.format(
172+
read_method_name='readall',
173+
expected=b'hello\nworld!\n'))
174+
# read() is the same thing as readall().
175+
self._test_reading(
176+
data_to_write=b'hello\nworld!',
177+
read_and_verify_code=self._READING_CODE_TEMPLATE.format(
178+
read_method_name='read',
179+
expected=b'hello\nworld!\n'))
180+
181+
182+
class TestBufferedIOSignalInterrupt(TestFileIOSignalInterrupt):
183+
def _generate_infile_setup_code(self):
184+
"""Returns the infile = ... line of code to make a BufferedReader."""
185+
return ('infile = open(sys.stdin.fileno(), "rb") ;'
186+
'import _io ;assert isinstance(infile, _io.BufferedReader)')
187+
188+
def test_readall(self):
189+
"""BufferedReader.read() must handle signals and not lose data."""
190+
self._test_reading(
191+
data_to_write=b'hello\nworld!',
192+
read_and_verify_code=self._READING_CODE_TEMPLATE.format(
193+
read_method_name='read',
194+
expected=b'hello\nworld!\n'))
195+
196+
197+
class TestTextIOSignalInterrupt(TestFileIOSignalInterrupt):
198+
def _generate_infile_setup_code(self):
199+
"""Returns the infile = ... line of code to make a TextIOWrapper."""
200+
return ('infile = open(sys.stdin.fileno(), "rt", newline=None) ;'
201+
'import _io ;assert isinstance(infile, _io.TextIOWrapper)')
202+
203+
def test_readline(self):
204+
"""readline() must handle signals and not lose data."""
205+
self._test_reading(
206+
data_to_write=b'hello, world!',
207+
read_and_verify_code=self._READING_CODE_TEMPLATE.format(
208+
read_method_name='readline',
209+
expected='hello, world!\n'))
210+
211+
def test_readlines(self):
212+
"""readlines() must handle signals and not lose data."""
213+
self._test_reading(
214+
data_to_write=b'hello\r\nworld!',
215+
read_and_verify_code=self._READING_CODE_TEMPLATE.format(
216+
read_method_name='readlines',
217+
expected=['hello\n', 'world!\n']))
218+
219+
def test_readall(self):
220+
"""read() must handle signals and not lose data."""
221+
self._test_reading(
222+
data_to_write=b'hello\nworld!',
223+
read_and_verify_code=self._READING_CODE_TEMPLATE.format(
224+
read_method_name='read',
225+
expected="hello\nworld!\n"))
226+
227+
228+
def test_main():
229+
test_cases = [
230+
tc for tc in globals().values()
231+
if isinstance(tc, type) and issubclass(tc, unittest.TestCase)]
232+
run_unittest(*test_cases)
233+
234+
235+
if __name__ == '__main__':
236+
test_main()

Misc/NEWS

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,11 @@ What's New in Python 3.2.4
1010
Core and Builtins
1111
-----------------
1212

13+
- Issue #12268: File readline, readlines and read() or readall() methods
14+
no longer lose data when an underlying read system call is interrupted.
15+
IOError is no longer raised due to a read system call returning EINTR
16+
from within these methods.
17+
1318
- Issue #15142: Fix reference leak when deallocating instances of types
1419
created using PyType_FromSpec().
1520

Modules/_io/_iomodule.h

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,11 @@ extern Py_ssize_t _PyIO_find_line_ending(
5757
int translated, int universal, PyObject *readnl,
5858
Py_UNICODE *start, Py_UNICODE *end, Py_ssize_t *consumed);
5959

60+
/* Return 1 if an EnvironmentError with errno == EINTR is set (and then
61+
clears the error indicator), 0 otherwise.
62+
Should only be called when PyErr_Occurred() is true.
63+
*/
64+
extern int _PyIO_trap_eintr(void);
6065

6166
#define DEFAULT_BUFFER_SIZE (8 * 1024) /* bytes */
6267

Modules/_io/bufferedio.c

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -730,8 +730,8 @@ _buffered_init(buffered *self)
730730
clears the error indicator), 0 otherwise.
731731
Should only be called when PyErr_Occurred() is true.
732732
*/
733-
static int
734-
_trap_eintr(void)
733+
int
734+
_PyIO_trap_eintr(void)
735735
{
736736
static PyObject *eintr_int = NULL;
737737
PyObject *typ, *val, *tb;
@@ -1314,7 +1314,7 @@ _bufferedreader_raw_read(buffered *self, char *start, Py_ssize_t len)
13141314
*/
13151315
do {
13161316
res = PyObject_CallMethodObjArgs(self->raw, _PyIO_str_readinto, memobj, NULL);
1317-
} while (res == NULL && _trap_eintr());
1317+
} while (res == NULL && _PyIO_trap_eintr());
13181318
Py_DECREF(memobj);
13191319
if (res == NULL)
13201320
return -1;
@@ -1742,7 +1742,7 @@ _bufferedwriter_raw_write(buffered *self, char *start, Py_ssize_t len)
17421742
errno = 0;
17431743
res = PyObject_CallMethodObjArgs(self->raw, _PyIO_str_write, memobj, NULL);
17441744
errnum = errno;
1745-
} while (res == NULL && _trap_eintr());
1745+
} while (res == NULL && _PyIO_trap_eintr());
17461746
Py_DECREF(memobj);
17471747
if (res == NULL)
17481748
return -1;

Modules/_io/fileio.c

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -605,6 +605,13 @@ fileio_readall(fileio *self)
605605
if (n == 0)
606606
break;
607607
if (n < 0) {
608+
if (errno == EINTR) {
609+
if (PyErr_CheckSignals()) {
610+
Py_DECREF(result);
611+
return NULL;
612+
}
613+
continue;
614+
}
608615
if (total > 0)
609616
break;
610617
if (errno == EAGAIN) {

Modules/_io/iobase.c

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -482,8 +482,14 @@ iobase_readline(PyObject *self, PyObject *args)
482482

483483
if (has_peek) {
484484
PyObject *readahead = PyObject_CallMethod(self, "peek", "i", 1);
485-
if (readahead == NULL)
485+
if (readahead == NULL) {
486+
/* NOTE: PyErr_SetFromErrno() calls PyErr_CheckSignals()
487+
when EINTR occurs so we needn't do it ourselves. */
488+
if (_PyIO_trap_eintr()) {
489+
continue;
490+
}
486491
goto fail;
492+
}
487493
if (!PyBytes_Check(readahead)) {
488494
PyErr_Format(PyExc_IOError,
489495
"peek() should have returned a bytes object, "
@@ -516,8 +522,14 @@ iobase_readline(PyObject *self, PyObject *args)
516522
}
517523

518524
b = PyObject_CallMethod(self, "read", "n", nreadahead);
519-
if (b == NULL)
525+
if (b == NULL) {
526+
/* NOTE: PyErr_SetFromErrno() calls PyErr_CheckSignals()
527+
when EINTR occurs so we needn't do it ourselves. */
528+
if (_PyIO_trap_eintr()) {
529+
continue;
530+
}
520531
goto fail;
532+
}
521533
if (!PyBytes_Check(b)) {
522534
PyErr_Format(PyExc_IOError,
523535
"read() should have returned a bytes object, "
@@ -826,6 +838,11 @@ rawiobase_readall(PyObject *self, PyObject *args)
826838
PyObject *data = PyObject_CallMethod(self, "read",
827839
"i", DEFAULT_BUFFER_SIZE);
828840
if (!data) {
841+
/* NOTE: PyErr_SetFromErrno() calls PyErr_CheckSignals()
842+
when EINTR occurs so we needn't do it ourselves. */
843+
if (_PyIO_trap_eintr()) {
844+
continue;
845+
}
829846
Py_DECREF(chunks);
830847
return NULL;
831848
}

Modules/_io/textio.c

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1541,8 +1541,14 @@ textiowrapper_read(textio *self, PyObject *args)
15411541
/* Keep reading chunks until we have n characters to return */
15421542
while (remaining > 0) {
15431543
res = textiowrapper_read_chunk(self);
1544-
if (res < 0)
1544+
if (res < 0) {
1545+
/* NOTE: PyErr_SetFromErrno() calls PyErr_CheckSignals()
1546+
when EINTR occurs so we needn't do it ourselves. */
1547+
if (_PyIO_trap_eintr()) {
1548+
continue;
1549+
}
15451550
goto fail;
1551+
}
15461552
if (res == 0) /* EOF */
15471553
break;
15481554
if (chunks == NULL) {
@@ -1701,8 +1707,14 @@ _textiowrapper_readline(textio *self, Py_ssize_t limit)
17011707
while (!self->decoded_chars ||
17021708
!PyUnicode_GET_SIZE(self->decoded_chars)) {
17031709
res = textiowrapper_read_chunk(self);
1704-
if (res < 0)
1710+
if (res < 0) {
1711+
/* NOTE: PyErr_SetFromErrno() calls PyErr_CheckSignals()
1712+
when EINTR occurs so we needn't do it ourselves. */
1713+
if (_PyIO_trap_eintr()) {
1714+
continue;
1715+
}
17051716
goto error;
1717+
}
17061718
if (res == 0)
17071719
break;
17081720
}

0 commit comments

Comments
 (0)