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

Skip to content

Commit 990a5fe

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.
2 parents 80d440a + 5135992 commit 990a5fe

8 files changed

Lines changed: 295 additions & 15 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()

Lib/test/test_io.py

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2912,7 +2912,7 @@ def _read():
29122912
try:
29132913
wio = self.io.open(w, **fdopen_kwargs)
29142914
t.start()
2915-
signal.alarm(1)
2915+
signal.setitimer(signal.ITIMER_REAL, 0.1)
29162916
# Fill the pipe enough that the write will be blocking.
29172917
# It will be interrupted by the timer armed above. Since the
29182918
# other thread has read one byte, the low-level write will
@@ -2957,7 +2957,7 @@ def on_alarm(*args):
29572957
r, w = os.pipe()
29582958
wio = self.io.open(w, **fdopen_kwargs)
29592959
try:
2960-
signal.alarm(1)
2960+
signal.setitimer(signal.ITIMER_REAL, 0.1)
29612961
# Either the reentrant call to wio.write() fails with RuntimeError,
29622962
# or the signal handler raises ZeroDivisionError.
29632963
with self.assertRaises((ZeroDivisionError, RuntimeError)) as cm:
@@ -2992,7 +2992,7 @@ def alarm_handler(sig, frame):
29922992
try:
29932993
rio = self.io.open(r, **fdopen_kwargs)
29942994
os.write(w, b"foo")
2995-
signal.alarm(1)
2995+
signal.setitimer(signal.ITIMER_REAL, 0.1)
29962996
# Expected behaviour:
29972997
# - first raw read() returns partial b"foo"
29982998
# - second raw read() returns EINTR
@@ -3036,13 +3036,13 @@ def _read():
30363036
t.daemon = True
30373037
def alarm1(sig, frame):
30383038
signal.signal(signal.SIGALRM, alarm2)
3039-
signal.alarm(1)
3039+
signal.setitimer(signal.ITIMER_REAL, 0.1)
30403040
def alarm2(sig, frame):
30413041
t.start()
30423042
signal.signal(signal.SIGALRM, alarm1)
30433043
try:
30443044
wio = self.io.open(w, **fdopen_kwargs)
3045-
signal.alarm(1)
3045+
signal.setitimer(signal.ITIMER_REAL, 0.1)
30463046
# Expected behaviour:
30473047
# - first raw write() is partial (because of the limited pipe buffer
30483048
# and the first alarm)

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.3.0 Beta 1?
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 #11626: Add _SizeT functions to stable ABI.
1419

1520
- Issue #15146: Add PyType_FromSpecWithBases. Patch by Robin Schreiber.

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
int kind, char *start, char *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
@@ -746,8 +746,8 @@ _buffered_init(buffered *self)
746746
clears the error indicator), 0 otherwise.
747747
Should only be called when PyErr_Occurred() is true.
748748
*/
749-
static int
750-
_trap_eintr(void)
749+
int
750+
_PyIO_trap_eintr(void)
751751
{
752752
static PyObject *eintr_int = NULL;
753753
PyObject *typ, *val, *tb;
@@ -1396,7 +1396,7 @@ _bufferedreader_raw_read(buffered *self, char *start, Py_ssize_t len)
13961396
*/
13971397
do {
13981398
res = PyObject_CallMethodObjArgs(self->raw, _PyIO_str_readinto, memobj, NULL);
1399-
} while (res == NULL && _trap_eintr());
1399+
} while (res == NULL && _PyIO_trap_eintr());
14001400
Py_DECREF(memobj);
14011401
if (res == NULL)
14021402
return -1;
@@ -1850,7 +1850,7 @@ _bufferedwriter_raw_write(buffered *self, char *start, Py_ssize_t len)
18501850
errno = 0;
18511851
res = PyObject_CallMethodObjArgs(self->raw, _PyIO_str_write, memobj, NULL);
18521852
errnum = errno;
1853-
} while (res == NULL && _trap_eintr());
1853+
} while (res == NULL && _PyIO_trap_eintr());
18541854
Py_DECREF(memobj);
18551855
if (res == NULL)
18561856
return -1;

Modules/_io/fileio.c

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -670,6 +670,13 @@ fileio_readall(fileio *self)
670670
if (n == 0)
671671
break;
672672
if (n < 0) {
673+
if (errno == EINTR) {
674+
if (PyErr_CheckSignals()) {
675+
Py_DECREF(result);
676+
return NULL;
677+
}
678+
continue;
679+
}
673680
if (total > 0)
674681
break;
675682
if (errno == EAGAIN) {

Modules/_io/iobase.c

Lines changed: 19 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -474,11 +474,15 @@ iobase_readline(PyObject *self, PyObject *args)
474474
PyObject *b;
475475

476476
if (has_peek) {
477-
_Py_IDENTIFIER(peek);
478477
PyObject *readahead = _PyObject_CallMethodId(self, &PyId_peek, "i", 1);
479-
480-
if (readahead == NULL)
478+
if (readahead == NULL) {
479+
/* NOTE: PyErr_SetFromErrno() calls PyErr_CheckSignals()
480+
when EINTR occurs so we needn't do it ourselves. */
481+
if (_PyIO_trap_eintr()) {
482+
continue;
483+
}
481484
goto fail;
485+
}
482486
if (!PyBytes_Check(readahead)) {
483487
PyErr_Format(PyExc_IOError,
484488
"peek() should have returned a bytes object, "
@@ -511,8 +515,14 @@ iobase_readline(PyObject *self, PyObject *args)
511515
}
512516

513517
b = _PyObject_CallMethodId(self, &PyId_read, "n", nreadahead);
514-
if (b == NULL)
518+
if (b == NULL) {
519+
/* NOTE: PyErr_SetFromErrno() calls PyErr_CheckSignals()
520+
when EINTR occurs so we needn't do it ourselves. */
521+
if (_PyIO_trap_eintr()) {
522+
continue;
523+
}
515524
goto fail;
525+
}
516526
if (!PyBytes_Check(b)) {
517527
PyErr_Format(PyExc_IOError,
518528
"read() should have returned a bytes object, "
@@ -827,6 +837,11 @@ rawiobase_readall(PyObject *self, PyObject *args)
827837
PyObject *data = _PyObject_CallMethodId(self, &PyId_read,
828838
"i", DEFAULT_BUFFER_SIZE);
829839
if (!data) {
840+
/* NOTE: PyErr_SetFromErrno() calls PyErr_CheckSignals()
841+
when EINTR occurs so we needn't do it ourselves. */
842+
if (_PyIO_trap_eintr()) {
843+
continue;
844+
}
830845
Py_DECREF(chunks);
831846
return NULL;
832847
}

0 commit comments

Comments
 (0)