From 867c4354609b09b2425b188f212e54fc027be18f Mon Sep 17 00:00:00 2001 From: Jean-Paul Calderone Date: Sat, 19 Jun 2010 19:54:48 +0000 Subject: [PATCH] merge forward from the python 2.x branch --- Doc/library/signal.rst | 81 +++++++++++++++- Lib/test/test_signal.py | 205 +++++++++++++++++++++++++++++++++++++++- Misc/NEWS | 5 + Modules/signalmodule.c | 180 +++++++++++++++++++++++++++++++++++ configure | 2 +- configure.in | 2 +- pyconfig.h.in | 6 ++ 7 files changed, 474 insertions(+), 7 deletions(-) diff --git a/Doc/library/signal.rst b/Doc/library/signal.rst index 7b40e8e67da..26d9320dee7 100644 --- a/Doc/library/signal.rst +++ b/Doc/library/signal.rst @@ -13,9 +13,6 @@ rules for working with signals and their handlers: underlying implementation), with the exception of the handler for :const:`SIGCHLD`, which follows the underlying implementation. -* There is no way to "block" signals temporarily from critical sections (since - this is not supported by all Unix flavors). - * Although Python signal handlers are called asynchronously as far as the Python user is concerned, they can only occur between the "atomic" instructions of the Python interpreter. This means that signals arriving during long calculations @@ -115,6 +112,46 @@ The variables defined in the :mod:`signal` module are: in user and kernel space. SIGPROF is delivered upon expiration. +.. data:: SIG_BLOCK + + A possible value for the *how* parameter to :func:`sigprocmask` + indicating that signals are to be blocked. + + .. versionadded:: 2.7 + + +.. data:: SIG_UNBLOCK + + A possible value for the *how* parameter to :func:`sigprocmask` + indicating that signals are to be unblocked. + + .. versionadded:: 2.7 + + +.. data:: SIG_SETMASK + + A possible value for the *how* parameter to :func:`sigprocmask` + indicating that the signal mask is to be replaced. + + .. versionadded:: 2.7 + + +.. 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:: 2.7 + + +.. data:: SFD_NONBLOCK + + A possible flag in the *flags* parameter to :func:`signalfd` which causes + the new file description to be set non-blocking. + + .. versionadded:: 2.7 + + The :mod:`signal` module defines one exception: .. exception:: ItimerError @@ -227,6 +264,44 @@ The :mod:`signal` module defines the following functions: attribute descriptions in the :mod:`inspect` module). +.. 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. + Availability: Linux (See the manpage :manpage:`signalfd(2)` for further + information). + + 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 any :const:`signal.SFD_*` flag. + + .. versionadded:: 2.7 + + +.. function:: sigprocmask(how, mask) + + Set the signal mask for the process. The old signal mask is returned. + Availability: Unix (See the Unix man page :manpage:`sigprocmask(2)` and + :manpage:`pthread_sigmask(2)`.) + + If *how* is :const:`signal.SIG_BLOCK`, the signals in the mask are added + to the set of blocked signals. + + If *how* is :const:`signal.SIG_UNBLOCK`, the signals in the mask are + removed from the set of blocked signals. + + If *how* is :const:`signal.SIG_SETMASK`, the signals in the mask are set + as blocked and the signals not in the mask are set as unblocked. + + *mask* is a list of signal numbers (eg :const:`signal.SIGUSR1`). + + .. versionadded:: 2.7 + + .. _signal-example: Example diff --git a/Lib/test/test_signal.py b/Lib/test/test_signal.py index a246695b1a9..5414a61960f 100644 --- a/Lib/test/test_signal.py +++ b/Lib/test/test_signal.py @@ -462,9 +462,210 @@ class ItimerTest(unittest.TestCase): # and the handler should have been called self.assertEqual(self.hndl_called, True) + + +class SomeException(Exception): + """ + A unique exception class to be raised by a signal handler to verify that the + signal handler was invoked. + """ + + + +def raiser(*args): + """A signal handler which raises SomeException.""" + raise SomeException() + + + +class SigprocmaskTests(unittest.TestCase): + """Tests for sigprocmask.""" + def _handle_sigusr1(self): + old_handler = signal.signal(signal.SIGUSR1, raiser) + self.addCleanup(signal.signal, signal.SIGUSR1, old_handler) + + + def test_signature(self): + """When invoked with other than two arguments, sigprocmask raises + TypeError. + """ + self.assertRaises(TypeError, signal.sigprocmask) + self.assertRaises(TypeError, signal.sigprocmask, 1) + self.assertRaises(TypeError, signal.sigprocmask, 1, 2, 3) + + + def test_invalid_how(self): + """If a value other than SIG_BLOCK, SIG_UNBLOCK, or SIG_SETMASK is + passed for the how argument to sigprocmask, ValueError is raised. + """ + message = "value specified for how \(1700\) invalid" + with self.assertRaisesRegexp(ValueError, message): + signal.sigprocmask(1700, []) + + + def test_invalid_signal_iterable(self): + """If iterating over the value passed for the signals parameter to + sigprocmask raises an exception, sigprocmask raises that exception. + """ + class BrokenIter(object): + def __iter__(self): + raise RuntimeError("my __iter__ is broken") + with self.assertRaisesRegexp(RuntimeError, "my __iter__ is broken"): + signal.sigprocmask(signal.SIG_BLOCK, BrokenIter()) + + + def test_invalid_signal(self): + """If an object in the iterable passed for the signals parameter to + sigprocmask isn't an integer, TypeError is raised.""" + with self.assertRaisesRegexp(TypeError, "an integer is required"): + signal.sigprocmask(signal.SIG_BLOCK, [object()]) + + + def test_return_previous_mask(self): + """sigprocmask returns a list of the signals previously masked. + """ + previous = signal.sigprocmask(signal.SIG_BLOCK, [1, 3, 5]) + result = signal.sigprocmask(signal.SIG_BLOCK, previous) + self.assertEquals(result, [1, 3, 5]) + + + def test_block(self): + """When invoked with SIG_BLOCK, sigprocmask blocks the signals in the + sigmask list. + """ + self._handle_sigusr1() + previous = signal.sigprocmask(signal.SIG_BLOCK, [signal.SIGUSR1]) + os.kill(os.getpid(), signal.SIGUSR1) + with self.assertRaises(SomeException): + # Expect to receive SIGUSR1 after unblocking it. + signal.sigprocmask(signal.SIG_SETMASK, previous) + + + def test_unblock(self): + """When invoked with SIG_UNBLOCK, sigprocmask unblocks the signals in + the sigmask list. + """ + self._handle_sigusr1() + previous = signal.sigprocmask(signal.SIG_BLOCK, [signal.SIGUSR1]) + self.addCleanup(signal.sigprocmask, signal.SIG_SETMASK, previous) + signal.sigprocmask(signal.SIG_UNBLOCK, [signal.SIGUSR1]) + + with self.assertRaises(SomeException): + os.kill(os.getpid(), signal.SIGUSR1) + + + def test_long_signals(self): + """sigprocmask accepts signal numbers as instances of long.""" + previous = signal.sigprocmask( + signal.SIG_SETMASK, [long(signal.SIGUSR1), long(signal.SIGUSR2)]) + masked = signal.sigprocmask(signal.SIG_SETMASK, previous) + self.assertEquals(masked, [signal.SIGUSR1, signal.SIGUSR2]) + + + +class SignalfdTests(unittest.TestCase): + """ + Tests for signal.signalfd. + """ + def test_signature(self): + """When invoked with fewer than two arguments or more than three, + signalfd raises TypeError. + """ + self.assertRaises(TypeError, signal.signalfd) + self.assertRaises(TypeError, signal.signalfd, 1) + self.assertRaises(TypeError, signal.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 = signal.signalfd(-1, []) + self.assertTrue(isinstance(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, signal.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, signal.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" + with self.assertRaisesRegexp(ValueError, message): + signal.signalfd(-1, [-2]) + + + 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 = signal.signalfd(-1, [signal.SIGUSR2]) + self.addCleanup(os.close, fd) + previous = signal.sigprocmask(signal.SIG_BLOCK, [signal.SIGUSR2]) + self.addCleanup(signal.sigprocmask, 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 = signal.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 = signal.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 = signal.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(): - support.run_unittest(BasicSignalTests, InterProcessSignalTests, - WakeupSignalTests, SiginterruptTest, ItimerTest) + support.run_unittest( + BasicSignalTests, InterProcessSignalTests, + WakeupSignalTests, SiginterruptTest, ItimerTest, SignalfdTests, + SigprocmaskTests) if __name__ == "__main__": diff --git a/Misc/NEWS b/Misc/NEWS index a1d3a838f06..3b21eb71099 100644 --- a/Misc/NEWS +++ b/Misc/NEWS @@ -1025,6 +1025,11 @@ Library - Issue #5949: added check for correct lineends in input from IMAP server in imaplib. +- Issue #8407: The signal module gains the ``signalfd()`` and + ``sigprocmask(2)`` functions providing access to the signalfd(2) and + sigprocmask(2) system calls respectively on Linux systems which implement + them. + - Add count() and reverse() methods to collections.deque(). - Fix variations of extending deques: d.extend(d) d.extendleft(d) d+=d diff --git a/Modules/signalmodule.c b/Modules/signalmodule.c index f4fd96491f3..026846b3bb6 100644 --- a/Modules/signalmodule.c +++ b/Modules/signalmodule.c @@ -22,6 +22,10 @@ #ifdef HAVE_SYS_TIME_H #include #endif +#ifdef HAVE_SIGNALFD +#include +#endif + #ifndef SIG_ERR #define SIG_ERR ((PyOS_sighandler_t)(-1)) @@ -464,6 +468,144 @@ Returns current value of given itimer."); #endif +static int +_iterable_to_mask(PyObject *iterable, sigset_t *mask) +{ + static const char* range_format = "signal number %d out of range"; + char range_buffer[1024]; + int result = 0; + + PyObject *item, *iterator = NULL; + + sigemptyset(mask); + + iterator = PyObject_GetIter(iterable); + if (iterator == NULL) { + result = -1; + goto error; + } + + while ((item = PyIter_Next(iterator))) { + int signum = PyInt_AsLong(item); + Py_DECREF(item); + if (signum == -1 && PyErr_Occurred()) { + result = -1; + goto error; + } + if (sigaddset(mask, signum) == -1) { + PyOS_snprintf(range_buffer, sizeof(range_buffer), range_format, signum); + PyErr_SetString(PyExc_ValueError, range_buffer); + result = -1; + goto error; + } + } + +error: + Py_XDECREF(iterator); + return result; +} + +#if defined(HAVE_PTHREAD_SIGMASK) && !defined(HAVE_BROKEN_PTHREAD_SIGMASK) +# define PY_SIGMASK pthread_sigmask +#elif defined(HAVE_SIGPROCMASK) +# define PY_SIGMASK sigprocmask +#endif + +#ifdef PY_SIGMASK +static PyObject * +signal_sigprocmask(PyObject *self, PyObject *args) +{ + static const char* how_format = "value specified for how (%d) invalid"; + char how_buffer[1024]; + + int how, sig; + PyObject *signals, *result, *signum; + sigset_t mask, previous; + + if (!PyArg_ParseTuple(args, "iO:sigprocmask", &how, &signals)) { + return NULL; + } + + if (_iterable_to_mask(signals, &mask) == -1) { + return NULL; + } + + if (PY_SIGMASK(how, &mask, &previous) != 0) { + PyOS_snprintf(how_buffer, sizeof(how_buffer), how_format, how); + PyErr_SetString(PyExc_ValueError, how_buffer); + return NULL; + } + + result = PyList_New(0); + if (result == NULL) { + return NULL; + } + + for (sig = 1; sig < NSIG; ++sig) { + if (sigismember(&previous, sig) == 1) { + /* Handle the case where it is a member by adding the signal to + the result list. Ignore the other cases because they mean the + signal isn't a member of the mask or the signal was invalid, + and an invalid signal must have been our fault in constructing + the loop boundaries. */ + signum = PyInt_FromLong(sig); + if (signum == NULL) { + Py_DECREF(result); + return NULL; + } + if (PyList_Append(result, signum) == -1) { + Py_DECREF(signum); + Py_DECREF(result); + return NULL; + } + Py_DECREF(signum); + } + } + return result; +} + +PyDoc_STRVAR(sigprocmask_doc, +"sigprocmask(how, mask) -> old mask\n\ +\n\ +Examine and change blocked signals."); +#endif + +#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_mask(signals, &mask) == -1) { + return NULL; + } + + result = signalfd(-1, &mask, flags); + + if (result == -1) { + PyErr_SetFromErrno(PyExc_OSError); + return NULL; + } + + return PyInt_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 @@ -478,6 +620,14 @@ static PyMethodDef signal_methods[] = { {"signal", signal_signal, METH_VARARGS, signal_doc}, {"getsignal", signal_getsignal, METH_VARARGS, getsignal_doc}, {"set_wakeup_fd", signal_set_wakeup_fd, METH_VARARGS, set_wakeup_fd_doc}, +#ifdef PY_SIGMASK + {"sigprocmask", signal_sigprocmask, METH_VARARGS, sigprocmask_doc}, +/* It's no longer needed, so clean up the namespace. */ +#undef PY_SIGMASK +#endif +#ifdef HAVE_SIGNALFD + {"signalfd", signal_signalfd, METH_VARARGS, signalfd_doc}, +#endif #ifdef HAVE_SIGINTERRUPT {"siginterrupt", signal_siginterrupt, METH_VARARGS, siginterrupt_doc}, #endif @@ -811,6 +961,36 @@ PyInit_signal(void) PyDict_SetItemString(d, "ItimerError", ItimerError); #endif +#ifdef SIG_BLOCK + x = PyLong_FromLong(SIG_BLOCK); + PyDict_SetItemString(d, "SIG_BLOCK", x); + Py_DECREF(x); +#endif + +#ifdef SIG_UNBLOCK + x = PyLong_FromLong(SIG_UNBLOCK); + PyDict_SetItemString(d, "SIG_UNBLOCK", x); + Py_DECREF(x); +#endif + +#ifdef SIG_SETMASK + x = PyLong_FromLong(SIG_SETMASK); + PyDict_SetItemString(d, "SIG_SETMASK", x); + Py_DECREF(x); +#endif + +#ifdef SFD_CLOEXEC + x = PyLong_FromLong(SFD_CLOEXEC); + PyDict_SetItemString(d, "SFD_CLOEXEC", x); + Py_DECREF(x); +#endif + +#ifdef SFD_NONBLOCK + x = PyLong_FromLong(SFD_NONBLOCK); + PyDict_SetItemString(d, "SFD_NONBLOCK", x); + Py_DECREF(x); +#endif + #ifdef CTRL_C_EVENT x = PyLong_FromLong(CTRL_C_EVENT); PyDict_SetItemString(d, "CTRL_C_EVENT", x); diff --git a/configure b/configure index 3655b27aa33..8ff9e91c89c 100755 --- a/configure +++ b/configure @@ -9297,7 +9297,7 @@ for ac_func in alarm setitimer getitimer bind_textdomain_codeset chown \ select sem_open sem_timedwait sem_getvalue sem_unlink setegid seteuid \ setgid \ setlocale setregid setreuid setresuid setresgid setsid setpgid setpgrp setuid setvbuf \ - sigaction siginterrupt sigrelse snprintf strftime strlcpy \ + sigaction siginterrupt signalfd sigprocmask sigrelse snprintf strftime strlcpy \ sysconf tcgetpgrp tcsetpgrp tempnam timegm times tmpfile tmpnam tmpnam_r \ truncate uname unsetenv utimes waitpid wait3 wait4 \ wcscoll wcsftime wcsxfrm _getpty diff --git a/configure.in b/configure.in index b3d51587743..b37226a9cc6 100644 --- a/configure.in +++ b/configure.in @@ -2580,7 +2580,7 @@ AC_CHECK_FUNCS(alarm setitimer getitimer bind_textdomain_codeset chown \ select sem_open sem_timedwait sem_getvalue sem_unlink setegid seteuid \ setgid \ setlocale setregid setreuid setresuid setresgid setsid setpgid setpgrp setuid setvbuf \ - sigaction siginterrupt sigrelse snprintf strftime strlcpy \ + sigaction siginterrupt signalfd sigprocmask sigrelse snprintf strftime strlcpy \ sysconf tcgetpgrp tcsetpgrp tempnam timegm times tmpfile tmpnam tmpnam_r \ truncate uname unsetenv utimes waitpid wait3 wait4 \ wcscoll wcsftime wcsxfrm _getpty) diff --git a/pyconfig.h.in b/pyconfig.h.in index d3a87861e1e..9750b7b0a68 100644 --- a/pyconfig.h.in +++ b/pyconfig.h.in @@ -620,6 +620,12 @@ /* Define to 1 if you have the header file. */ #undef HAVE_SIGNAL_H +/* Define to 1 if you have the `signalfd' function. */ +#undef HAVE_SIGNALFD + +/* Define to 1 if you have the `sigprocmask' function. */ +#undef HAVE_SIGPROCMASK + /* Define to 1 if you have the `sigrelse' function. */ #undef HAVE_SIGRELSE