diff --git a/Doc/library/signal.rst b/Doc/library/signal.rst --- a/Doc/library/signal.rst +++ b/Doc/library/signal.rst @@ -116,6 +116,22 @@ The variables defined in the :mod:`signa in user and kernel space. SIGPROF is delivered upon expiration. +.. data:: SFD_CLOEXEC + + A possible flag in the *flags* parameter to :func:`signalfd` which causes + the new file descriptor to be marked as close-on-exec. + + .. versionadded:: 3.3 + + +.. data:: SFD_NONBLOCK + + A possible flag in the *flags* parameter to :func:`signalfd` which causes + the new :term:`file description` to be set non-blocking. + + .. versionadded:: 3.3 + + .. data:: SIG_BLOCK A possible value for the *how* parameter to :func:`pthread_sigmask` @@ -247,6 +263,8 @@ The :mod:`signal` module defines the fol attempting to call it from other threads will cause a :exc:`ValueError` exception to be raised. + See also the :func:`signalfd` function. + .. function:: siginterrupt(signalnum, flag) @@ -282,6 +300,28 @@ The :mod:`signal` module defines the fol :const:`SIGTERM`. A :exc:`ValueError` will be raised in any other case. +.. function:: signalfd(fd, mask[, flags]) + + Create a new file descriptor on which to receive signals or modify the + mask of such a file descriptor previously created by this function. + + If *fd* is ``-1``, a new file descriptor will be created. Otherwise, + *fd* must be a file descriptor previously returned by this function. + + *mask* is a list of signal numbers which will trigger data on this file + descriptor. + + *flags* is a bit mask which may include :const:`signal.SFD_CLOEXEC` and + :const:`signal.SFD_NONBLOCK` flags. + + See also the :func:`set_wakeup_fd` function. + + Availability: Linux. See the manpage :manpage:`signalfd(2)` for further + information. + + .. versionadded:: 3.3 + + .. _signal-example: Example diff --git a/Lib/test/test_signal.py b/Lib/test/test_signal.py --- a/Lib/test/test_signal.py +++ b/Lib/test/test_signal.py @@ -491,7 +491,7 @@ class PthreadSigmaskTests(unittest.TestC self.assertRaises(TypeError, signal.pthread_sigmask) self.assertRaises(TypeError, signal.pthread_sigmask, 1) self.assertRaises(TypeError, signal.pthread_sigmask, 1, 2, 3) - self.assertRaises(RuntimeError, signal.pthread_sigmask, 1700, []) + self.assertRaises(OSError, signal.pthread_sigmask, 1700, []) def test_block_unlock(self): pid = os.getpid() @@ -536,12 +536,136 @@ class PthreadSigmaskTests(unittest.TestC self.assertSequenceEqual(old_mask, unblocked) +@unittest.skipUnless(hasattr(signal, 'signalfd'), + 'need signal.signalfd()') +class SignalfdTests(unittest.TestCase): + """ + Tests for signal.signalfd. + """ + def signalfd(self, *a, **kw): + try: + return signal.signalfd(*a, **kw) + except OSError: + type, value, traceback = sys.exc_info() + if value.errno == errno.ENOSYS: + raise unittest.Skip( + "signalfd() not implemented on this platform") + + + def test_signature(self): + """ + When invoked with fewer than two arguments or more than three, signalfd + raises TypeError. + """ + self.assertRaises(TypeError, self.signalfd) + self.assertRaises(TypeError, self.signalfd, 1) + self.assertRaises(TypeError, self.signalfd, 1, 2, 3, 4) + + + def test_create_signalfd(self): + """ + When invoked with a file descriptor of -1, signalfd allocates a new file + descriptor for signal information delivery and returns it. + """ + fd = self.signalfd(-1, []) + self.assertIsInstance(fd, int) + os.close(fd) + + + def test_non_iterable_signals(self): + """ + If an object which is not iterable is passed for the sigmask list + argument to signalfd, the exception raised by trying to iterate over + that object is raised. + """ + self.assertRaises(TypeError, self.signalfd, -1, object()) + + + def test_non_integer_signals(self): + """ + If any non-integer values are included in the sigmask list argument to + signalfd, the exception raised by the attempt to convert them to an + integer is raised. + """ + self.assertRaises(TypeError, self.signalfd, -1, [object()]) + + + def test_out_of_range_signal(self): + """ + If a signal number that is out of the valid range is included in the + sigmask list argument to signalfd, ValueError is raised. + """ + message = "signal number -2 out of range" + try: + self.signalfd(-1, [-2]) + except ValueError: + type, value, traceback = sys.exc_info() + self.assertEquals(str(value), message) + else: + self.fail("Expected negative signal number to trigger ValueError") + + + def test_handle_signals(self): + """ + After signalfd is called, if a signal is received which was in the + sigmask list passed to that call, information about the signal can be + read from the fd returned by that call. + """ + fd = self.signalfd(-1, [signal.SIGUSR2]) + self.addCleanup(os.close, fd) + previous = signal.pthread_sigmask(signal.SIG_BLOCK, [signal.SIGUSR2]) + self.addCleanup(signal.pthread_sigmask, signal.SIG_SETMASK, previous) + os.kill(os.getpid(), signal.SIGUSR2) + bytes = os.read(fd, 128) + self.assertTrue(bytes) + + + def test_close_on_exec(self): + """ + If the bit mask passed as the 3rd argument to signalfd includes + SFD_CLOEXEC, the returned file descriptor has FD_CLOEXEC set on it. + """ + import fcntl + fd = self.signalfd(-1, [], signal.SFD_CLOEXEC) + self.addCleanup(os.close, fd) + flags = fcntl.fcntl(fd, fcntl.F_GETFD) + self.assertTrue(flags & fcntl.FD_CLOEXEC) + + + def test_nonblocking(self): + """ + If the bit mask passed as the 3rd argument to signalfd includes + SFD_NOBLOCK, the file description referenced by the returned file + descriptor has O_NONBLOCK set on it. + """ + import fcntl + fd = self.signalfd(-1, [], signal.SFD_NONBLOCK) + self.addCleanup(os.close, fd) + flags = fcntl.fcntl(fd, fcntl.F_GETFL) + self.assertTrue(flags & os.O_NONBLOCK) + + + def test_default_flags(self): + """ + If an empty bit mask is passed as the 3rd argument to signalfd, neither + FD_CLOEXEC nor O_NONBLOCK is set on the resulting file + descriptor/description. + """ + import fcntl + fd = self.signalfd(-1, []) + self.addCleanup(os.close, fd) + flags = fcntl.fcntl(fd, fcntl.F_GETFD) + self.assertFalse(flags & fcntl.FD_CLOEXEC) + flags = fcntl.fcntl(fd, fcntl.F_GETFL) + self.assertFalse(flags & os.O_NONBLOCK) + + def test_main(): try: support.run_unittest(BasicSignalTests, InterProcessSignalTests, WakeupSignalTests, SiginterruptTest, ItimerTest, WindowsSignalTests, - PthreadSigmaskTests) + PthreadSigmaskTests, SignalfdTests) finally: support.reap_children() diff --git a/Modules/signalmodule.c b/Modules/signalmodule.c --- a/Modules/signalmodule.c +++ b/Modules/signalmodule.c @@ -21,6 +21,9 @@ #ifdef HAVE_SYS_TIME_H #include #endif +#ifdef HAVE_SIGNALFD +#include +#endif #if defined(HAVE_PTHREAD_SIGMASK) && !defined(HAVE_BROKEN_PTHREAD_SIGMASK) # define PYPTHREAD_SIGMASK @@ -503,7 +506,7 @@ PyDoc_STRVAR(getitimer_doc, Returns current value of given itimer."); #endif -#ifdef PYPTHREAD_SIGMASK +#if defined(PYPTHREAD_SIGMASK) || defined(HAVE_SIGNALFD) /* Convert an iterable to a sigset. Return 0 on success, return -1 and raise an exception on error. */ @@ -551,7 +554,9 @@ error: Py_XDECREF(iterator); return result; } +#endif +#ifdef PYPTHREAD_SIGMASK static PyObject * signal_pthread_sigmask(PyObject *self, PyObject *args) { @@ -569,7 +574,7 @@ signal_pthread_sigmask(PyObject *self, P err = pthread_sigmask(how, &mask, &previous); if (err != 0) { errno = err; - PyErr_SetFromErrno(PyExc_RuntimeError); + PyErr_SetFromErrno(PyExc_OSError); return NULL; } @@ -608,6 +613,38 @@ Fetch and/or change the signal mask of t #endif /* #ifdef PYPTHREAD_SIGMASK */ +#ifdef HAVE_SIGNALFD +static PyObject * +signal_signalfd(PyObject *self, PyObject *args) +{ + int result, flags = 0; + sigset_t mask; + + int fd; + PyObject *signals; + + if (!PyArg_ParseTuple(args, "iO|i:signalfd", &fd, &signals, &flags)) + return NULL; + + if (iterable_to_sigset(signals, &mask) == -1) + return NULL; + + result = signalfd(fd, &mask, flags); + if (result == -1) { + PyErr_SetFromErrno(PyExc_OSError); + return NULL; + } + + return PyLong_FromLong(result); +} + +PyDoc_STRVAR(signalfd_doc, +"signalfd(fd, mask, flags)\n\ +\n\ +Create a file descriptor for accepting signals."); +#endif + + /* List of functions defined in the module */ static PyMethodDef signal_methods[] = { #ifdef HAVE_ALARM @@ -635,6 +672,9 @@ static PyMethodDef signal_methods[] = { {"pthread_sigmask", (PyCFunction)signal_pthread_sigmask, METH_VARARGS, signal_pthread_sigmask_doc}, #endif +#ifdef HAVE_SIGNALFD + {"signalfd", signal_signalfd, METH_VARARGS, signalfd_doc}, +#endif {NULL, NULL} /* sentinel */ }; @@ -720,24 +760,25 @@ PyInit_signal(void) Py_DECREF(x); #ifdef SIG_BLOCK - x = PyLong_FromLong(SIG_BLOCK); - if (!x || PyDict_SetItemString(d, "SIG_BLOCK", x) < 0) + if (PyModule_AddIntMacro(m, SIG_BLOCK)) goto finally; - Py_DECREF(x); +#endif +#ifdef SIG_UNBLOCK + if (PyModule_AddIntMacro(m, SIG_UNBLOCK)) + goto finally; +#endif +#ifdef SIG_SETMASK + if (PyModule_AddIntMacro(m, SIG_SETMASK)) + goto finally; #endif -#ifdef SIG_UNBLOCK - x = PyLong_FromLong(SIG_UNBLOCK); - if (!x || PyDict_SetItemString(d, "SIG_UNBLOCK", x) < 0) +#ifdef SFD_CLOEXEC + if (PyModule_AddIntMacro(m, SFD_CLOEXEC)) goto finally; - Py_DECREF(x); #endif - -#ifdef SIG_SETMASK - x = PyLong_FromLong(SIG_SETMASK); - if (!x || PyDict_SetItemString(d, "SIG_SETMASK", x) < 0) +#ifdef SFD_NONBLOCK + if (PyModule_AddIntMacro(m, SFD_NONBLOCK)) goto finally; - Py_DECREF(x); #endif x = IntHandler = PyDict_GetItemString(d, "default_int_handler"); diff --git a/configure b/configure --- a/configure +++ b/configure @@ -9261,7 +9261,8 @@ for ac_func in alarm accept4 setitimer g select sem_open sem_timedwait sem_getvalue sem_unlink sendfile setegid seteuid \ setgid sethostname \ setlocale setregid setreuid setresuid setresgid setsid setpgid setpgrp setpriority setuid setvbuf \ - sigaction sigaltstack siginterrupt sigrelse snprintf strftime strlcpy symlinkat sync \ + sigaction sigaltstack siginterrupt signalfd sigrelse \ + snprintf strftime strlcpy symlinkat sync \ sysconf tcgetpgrp tcsetpgrp tempnam timegm times tmpfile tmpnam tmpnam_r \ truncate uname unlinkat unsetenv utimensat utimes waitid waitpid wait3 wait4 \ wcscoll wcsftime wcsxfrm writev _getpty diff --git a/configure.in b/configure.in --- a/configure.in +++ b/configure.in @@ -2507,7 +2507,8 @@ AC_CHECK_FUNCS(alarm accept4 setitimer g select sem_open sem_timedwait sem_getvalue sem_unlink sendfile setegid seteuid \ setgid sethostname \ setlocale setregid setreuid setresuid setresgid setsid setpgid setpgrp setpriority setuid setvbuf \ - sigaction sigaltstack siginterrupt sigrelse snprintf strftime strlcpy symlinkat sync \ + sigaction sigaltstack siginterrupt signalfd sigrelse \ + snprintf strftime strlcpy symlinkat sync \ sysconf tcgetpgrp tcsetpgrp tempnam timegm times tmpfile tmpnam tmpnam_r \ truncate uname unlinkat unsetenv utimensat utimes waitid waitpid wait3 wait4 \ wcscoll wcsftime wcsxfrm writev _getpty) diff --git a/pyconfig.h.in b/pyconfig.h.in --- a/pyconfig.h.in +++ b/pyconfig.h.in @@ -716,6 +716,9 @@ /* Define to 1 if you have the `siginterrupt' function. */ #undef HAVE_SIGINTERRUPT +/* Define to 1 if you have the `signalfd' function. */ +#undef HAVE_SIGNALFD + /* Define to 1 if you have the header file. */ #undef HAVE_SIGNAL_H