Skip to content

Commit 902ab80

Browse files
njsmith1st1
authored andcommitted
bpo-30050: Allow disabling full buffer warnings in signal.set_wakeup_fd (#4792)
1 parent 1b7c11f commit 902ab80

File tree

4 files changed

+170
-57
lines changed

4 files changed

+170
-57
lines changed

Doc/library/signal.rst

Lines changed: 24 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -300,7 +300,7 @@ The :mod:`signal` module defines the following functions:
300300
Availability: Unix.
301301

302302

303-
.. function:: set_wakeup_fd(fd)
303+
.. function:: set_wakeup_fd(fd, *, warn_on_full_buffer=True)
304304

305305
Set the wakeup file descriptor to *fd*. When a signal is received, the
306306
signal number is written as a single byte into the fd. This can be used by
@@ -312,16 +312,36 @@ The :mod:`signal` module defines the following functions:
312312
If not -1, *fd* must be non-blocking. It is up to the library to remove
313313
any bytes from *fd* before calling poll or select again.
314314

315-
Use for example ``struct.unpack('%uB' % len(data), data)`` to decode the
316-
signal numbers list.
317-
318315
When threads are enabled, this function can only be called from the main thread;
319316
attempting to call it from other threads will cause a :exc:`ValueError`
320317
exception to be raised.
321318

319+
There are two common ways to use this function. In both approaches,
320+
you use the fd to wake up when a signal arrives, but then they
321+
differ in how they determine *which* signal or signals have
322+
arrived.
323+
324+
In the first approach, we read the data out of the fd's buffer, and
325+
the byte values give you the signal numbers. This is simple, but in
326+
rare cases it can run into a problem: generally the fd will have a
327+
limited amount of buffer space, and if too many signals arrive too
328+
quickly, then the buffer may become full, and some signals may be
329+
lost. If you use this approach, then you should set
330+
``warn_on_full_buffer=True``, which will at least cause a warning
331+
to be printed to stderr when signals are lost.
332+
333+
In the second approach, we use the wakeup fd *only* for wakeups,
334+
and ignore the actual byte values. In this case, all we care about
335+
is whether the fd's buffer is empty or non-empty; a full buffer
336+
doesn't indicate a problem at all. If you use this approach, then
337+
you should set ``warn_on_full_buffer=False``, so that your users
338+
are not confused by spurious warning messages.
339+
322340
.. versionchanged:: 3.5
323341
On Windows, the function now also supports socket handles.
324342

343+
.. versionchanged:: 3.7
344+
Added ``warn_on_full_buffer`` parameter.
325345

326346
.. function:: siginterrupt(signalnum, flag)
327347

Lib/test/test_signal.py

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,15 @@ def test_issue9324(self):
9191

9292
class WakeupFDTests(unittest.TestCase):
9393

94+
def test_invalid_call(self):
95+
# First parameter is positional-only
96+
with self.assertRaises(TypeError):
97+
signal.set_wakeup_fd(signum=signal.SIGINT)
98+
99+
# warn_on_full_buffer is a keyword-only parameter
100+
with self.assertRaises(TypeError):
101+
signal.set_wakeup_fd(signal.SIGINT, False)
102+
94103
def test_invalid_fd(self):
95104
fd = support.make_bad_fd()
96105
self.assertRaises((ValueError, OSError),
@@ -423,6 +432,89 @@ def handler(signum, frame):
423432
""".format(action=action)
424433
assert_python_ok('-c', code)
425434

435+
@unittest.skipIf(_testcapi is None, 'need _testcapi')
436+
def test_warn_on_full_buffer(self):
437+
# Use a subprocess to have only one thread.
438+
if os.name == 'nt':
439+
action = 'send'
440+
else:
441+
action = 'write'
442+
code = """if 1:
443+
import errno
444+
import signal
445+
import socket
446+
import sys
447+
import time
448+
import _testcapi
449+
from test.support import captured_stderr
450+
451+
signum = signal.SIGINT
452+
453+
# This handler will be called, but we intentionally won't read from
454+
# the wakeup fd.
455+
def handler(signum, frame):
456+
pass
457+
458+
signal.signal(signum, handler)
459+
460+
read, write = socket.socketpair()
461+
read.setblocking(False)
462+
write.setblocking(False)
463+
464+
# Fill the send buffer
465+
try:
466+
while True:
467+
write.send(b"x")
468+
except BlockingIOError:
469+
pass
470+
471+
# By default, we get a warning when a signal arrives
472+
signal.set_wakeup_fd(write.fileno())
473+
474+
with captured_stderr() as err:
475+
_testcapi.raise_signal(signum)
476+
477+
err = err.getvalue()
478+
if ('Exception ignored when trying to {action} to the signal wakeup fd'
479+
not in err):
480+
raise AssertionError(err)
481+
482+
# And also if warn_on_full_buffer=True
483+
signal.set_wakeup_fd(write.fileno(), warn_on_full_buffer=True)
484+
485+
with captured_stderr() as err:
486+
_testcapi.raise_signal(signum)
487+
488+
err = err.getvalue()
489+
if ('Exception ignored when trying to {action} to the signal wakeup fd'
490+
not in err):
491+
raise AssertionError(err)
492+
493+
# But not if warn_on_full_buffer=False
494+
signal.set_wakeup_fd(write.fileno(), warn_on_full_buffer=False)
495+
496+
with captured_stderr() as err:
497+
_testcapi.raise_signal(signum)
498+
499+
err = err.getvalue()
500+
if err != "":
501+
raise AssertionError("got unexpected output %r" % (err,))
502+
503+
# And then check the default again, to make sure warn_on_full_buffer
504+
# settings don't leak across calls.
505+
signal.set_wakeup_fd(write.fileno())
506+
507+
with captured_stderr() as err:
508+
_testcapi.raise_signal(signum)
509+
510+
err = err.getvalue()
511+
if ('Exception ignored when trying to {action} to the signal wakeup fd'
512+
not in err):
513+
raise AssertionError(err)
514+
515+
""".format(action=action)
516+
assert_python_ok('-c', code)
517+
426518

427519
@unittest.skipIf(sys.platform == "win32", "Not valid on Windows")
428520
class SiginterruptTest(unittest.TestCase):
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
New argument warn_on_full_buffer to signal.set_wakeup_fd lets you control
2+
whether Python prints a warning on stderr when the wakeup fd buffer
3+
overflows.

Modules/signalmodule.c

Lines changed: 51 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -100,14 +100,15 @@ static volatile struct {
100100

101101
static volatile struct {
102102
SOCKET_T fd;
103+
int warn_on_full_buffer;
103104
int use_send;
104-
int send_err_set;
105-
int send_errno;
106-
int send_win_error;
107-
} wakeup = {INVALID_FD, 0, 0};
105+
} wakeup = {.fd = INVALID_FD, .warn_on_full_buffer = 1, .use_send = 0};
108106
#else
109107
#define INVALID_FD (-1)
110-
static volatile sig_atomic_t wakeup_fd = -1;
108+
static volatile struct {
109+
sig_atomic_t fd;
110+
int warn_on_full_buffer;
111+
} wakeup = {.fd = INVALID_FD, .warn_on_full_buffer = 1};
111112
#endif
112113

113114
/* Speed up sigcheck() when none tripped */
@@ -210,28 +211,15 @@ report_wakeup_write_error(void *data)
210211

211212
#ifdef MS_WINDOWS
212213
static int
213-
report_wakeup_send_error(void* Py_UNUSED(data))
214+
report_wakeup_send_error(void* data)
214215
{
215-
PyObject *res;
216-
217-
if (wakeup.send_win_error) {
218-
/* PyErr_SetExcFromWindowsErr() invokes FormatMessage() which
219-
recognizes the error codes used by both GetLastError() and
220-
WSAGetLastError */
221-
res = PyErr_SetExcFromWindowsErr(PyExc_OSError, wakeup.send_win_error);
222-
}
223-
else {
224-
errno = wakeup.send_errno;
225-
res = PyErr_SetFromErrno(PyExc_OSError);
226-
}
227-
228-
assert(res == NULL);
229-
wakeup.send_err_set = 0;
230-
216+
/* PyErr_SetExcFromWindowsErr() invokes FormatMessage() which
217+
recognizes the error codes used by both GetLastError() and
218+
WSAGetLastError */
219+
PyErr_SetExcFromWindowsErr(PyExc_OSError, (int) (intptr_t) data);
231220
PySys_WriteStderr("Exception ignored when trying to send to the "
232221
"signal wakeup fd:\n");
233222
PyErr_WriteUnraisable(NULL);
234-
235223
return 0;
236224
}
237225
#endif /* MS_WINDOWS */
@@ -257,7 +245,7 @@ trip_signal(int sig_num)
257245
and then set the flag, but this allowed the following sequence of events
258246
(especially on windows, where trip_signal may run in a new thread):
259247
260-
- main thread blocks on select([wakeup_fd], ...)
248+
- main thread blocks on select([wakeup.fd], ...)
261249
- signal arrives
262250
- trip_signal writes to the wakeup fd
263251
- the main thread wakes up
@@ -274,41 +262,43 @@ trip_signal(int sig_num)
274262
#ifdef MS_WINDOWS
275263
fd = Py_SAFE_DOWNCAST(wakeup.fd, SOCKET_T, int);
276264
#else
277-
fd = wakeup_fd;
265+
fd = wakeup.fd;
278266
#endif
279267

280268
if (fd != INVALID_FD) {
281269
byte = (unsigned char)sig_num;
282270
#ifdef MS_WINDOWS
283271
if (wakeup.use_send) {
284-
do {
285-
rc = send(fd, &byte, 1, 0);
286-
} while (rc < 0 && errno == EINTR);
287-
288-
/* we only have a storage for one error in the wakeup structure */
289-
if (rc < 0 && !wakeup.send_err_set) {
290-
wakeup.send_err_set = 1;
291-
wakeup.send_errno = errno;
292-
wakeup.send_win_error = GetLastError();
293-
/* Py_AddPendingCall() isn't signal-safe, but we
294-
still use it for this exceptional case. */
295-
Py_AddPendingCall(report_wakeup_send_error, NULL);
272+
rc = send(fd, &byte, 1, 0);
273+
274+
if (rc < 0) {
275+
int last_error = GetLastError();
276+
if (wakeup.warn_on_full_buffer ||
277+
last_error != WSAEWOULDBLOCK)
278+
{
279+
/* Py_AddPendingCall() isn't signal-safe, but we
280+
still use it for this exceptional case. */
281+
Py_AddPendingCall(report_wakeup_send_error,
282+
(void *)(intptr_t) last_error);
283+
}
296284
}
297285
}
298286
else
299287
#endif
300288
{
301-
byte = (unsigned char)sig_num;
302-
303289
/* _Py_write_noraise() retries write() if write() is interrupted by
304290
a signal (fails with EINTR). */
305291
rc = _Py_write_noraise(fd, &byte, 1);
306292

307293
if (rc < 0) {
308-
/* Py_AddPendingCall() isn't signal-safe, but we
309-
still use it for this exceptional case. */
310-
Py_AddPendingCall(report_wakeup_write_error,
311-
(void *)(intptr_t)errno);
294+
if (wakeup.warn_on_full_buffer ||
295+
(errno != EWOULDBLOCK && errno != EAGAIN))
296+
{
297+
/* Py_AddPendingCall() isn't signal-safe, but we
298+
still use it for this exceptional case. */
299+
Py_AddPendingCall(report_wakeup_write_error,
300+
(void *)(intptr_t)errno);
301+
}
312302
}
313303
}
314304
}
@@ -549,9 +539,13 @@ signal_siginterrupt_impl(PyObject *module, int signalnum, int flag)
549539

550540

551541
static PyObject*
552-
signal_set_wakeup_fd(PyObject *self, PyObject *args)
542+
signal_set_wakeup_fd(PyObject *self, PyObject *args, PyObject *kwds)
553543
{
554544
struct _Py_stat_struct status;
545+
static char *kwlist[] = {
546+
"", "warn_on_full_buffer", NULL,
547+
};
548+
int warn_on_full_buffer = 1;
555549
#ifdef MS_WINDOWS
556550
PyObject *fdobj;
557551
SOCKET_T sockfd, old_sockfd;
@@ -560,7 +554,8 @@ signal_set_wakeup_fd(PyObject *self, PyObject *args)
560554
PyObject *mod;
561555
int is_socket;
562556

563-
if (!PyArg_ParseTuple(args, "O:set_wakeup_fd", &fdobj))
557+
if (!PyArg_ParseTupleAndKeywords(args, kwds, "O|$p:set_wakeup_fd", kwlist,
558+
&fdobj, &warn_on_full_buffer))
564559
return NULL;
565560

566561
sockfd = PyLong_AsSocket_t(fdobj);
@@ -569,7 +564,8 @@ signal_set_wakeup_fd(PyObject *self, PyObject *args)
569564
#else
570565
int fd, old_fd;
571566

572-
if (!PyArg_ParseTuple(args, "i:set_wakeup_fd", &fd))
567+
if (!PyArg_ParseTupleAndKeywords(args, kwds, "i|$p:set_wakeup_fd", kwlist,
568+
&fd, &warn_on_full_buffer))
573569
return NULL;
574570
#endif
575571

@@ -620,6 +616,7 @@ signal_set_wakeup_fd(PyObject *self, PyObject *args)
620616

621617
old_sockfd = wakeup.fd;
622618
wakeup.fd = sockfd;
619+
wakeup.warn_on_full_buffer = warn_on_full_buffer;
623620
wakeup.use_send = is_socket;
624621

625622
if (old_sockfd != INVALID_FD)
@@ -644,15 +641,16 @@ signal_set_wakeup_fd(PyObject *self, PyObject *args)
644641
}
645642
}
646643

647-
old_fd = wakeup_fd;
648-
wakeup_fd = fd;
644+
old_fd = wakeup.fd;
645+
wakeup.fd = fd;
646+
wakeup.warn_on_full_buffer = warn_on_full_buffer;
649647

650648
return PyLong_FromLong(old_fd);
651649
#endif
652650
}
653651

654652
PyDoc_STRVAR(set_wakeup_fd_doc,
655-
"set_wakeup_fd(fd) -> fd\n\
653+
"set_wakeup_fd(fd, *, warn_on_full_buffer=True) -> fd\n\
656654
\n\
657655
Sets the fd to be written to (with the signal number) when a signal\n\
658656
comes in. A library can use this to wakeup select or poll.\n\
@@ -670,11 +668,11 @@ PySignal_SetWakeupFd(int fd)
670668

671669
#ifdef MS_WINDOWS
672670
old_fd = Py_SAFE_DOWNCAST(wakeup.fd, SOCKET_T, int);
673-
wakeup.fd = fd;
674671
#else
675-
old_fd = wakeup_fd;
676-
wakeup_fd = fd;
672+
old_fd = wakeup.fd;
677673
#endif
674+
wakeup.fd = fd;
675+
wakeup.warn_on_full_buffer = 1;
678676
return old_fd;
679677
}
680678

@@ -1155,7 +1153,7 @@ static PyMethodDef signal_methods[] = {
11551153
SIGNAL_GETITIMER_METHODDEF
11561154
SIGNAL_SIGNAL_METHODDEF
11571155
SIGNAL_GETSIGNAL_METHODDEF
1158-
{"set_wakeup_fd", signal_set_wakeup_fd, METH_VARARGS, set_wakeup_fd_doc},
1156+
{"set_wakeup_fd", (PyCFunction)signal_set_wakeup_fd, METH_VARARGS | METH_KEYWORDS, set_wakeup_fd_doc},
11591157
SIGNAL_SIGINTERRUPT_METHODDEF
11601158
SIGNAL_PAUSE_METHODDEF
11611159
SIGNAL_PTHREAD_KILL_METHODDEF

0 commit comments

Comments
 (0)