diff --git a/Doc/library/signal.rst b/Doc/library/signal.rst index f2a37cc65a6..9bca72e2176 100644 --- a/Doc/library/signal.rst +++ b/Doc/library/signal.rst @@ -179,7 +179,8 @@ The :mod:`signal` module defines the following functions: will then be called. Returns nothing. Not on Windows. (See the Unix man page :manpage:`signal(2)`.) - See also :func:`sigwait` and :func:`sigpending`. + See also :func:`sigwait`, :func:`sigwaitinfo`, :func:`sigtimedwait` and + :func:`sigpending`. .. function:: pthread_kill(thread_id, signum) @@ -334,7 +335,47 @@ The :mod:`signal` module defines the following functions: Availability: Unix (see the man page :manpage:`sigwait(3)` for further information). - See also :func:`pause`, :func:`pthread_sigmask` and :func:`sigpending`. + See also :func:`pause`, :func:`pthread_sigmask`, :func:`sigpending`, + :func:`sigwaitinfo` and :func:`sigtimedwait`. + + .. versionadded:: 3.3 + + +.. function:: sigwaitinfo(sigset) + + Suspend execution of the calling thread until the delivery of one of the + signals specified in the signal set *sigset*. The function accepts the + signal and removes it from the pending list of signals. If one of the + signals in *sigset* is already pending for the calling thread, the function + will return immediately with information about that signal. The signal + handler is not called for the delivered signal. The function raises an + :exc:`OSError` with error number set to :const:`errno.EINTR` if it is + interrupted by a signal that is not in *sigset*. + + The return value is an object representing the data contained in the + :c:type:`siginfo_t` structure, namely: :attr:`si_signo`, :attr:`si_code`, + :attr:`si_errno`, :attr:`si_pid`, :attr:`si_uid`, :attr:`si_status`, + :attr:`si_band`. + + Availability: Unix (see the man page :manpage:`sigwaitinfo(2)` for further + information). + + See also :func:`pause`, :func:`sigwait` and :func:`sigtimedwait`. + + .. versionadded:: 3.3 + + +.. function:: sigtimedwait(sigset, (timeout_sec, timeout_nsec)) + + Like :func:`sigtimedwait`, but takes a tuple of ``(seconds, nanoseconds)`` + as an additional argument specifying a timeout. If both *timeout_sec* and + *timeout_nsec* are specified as :const:`0`, a poll is performed. Returns + :const:`None` if a timeout occurs. + + Availability: Unix (see the man page :manpage:`sigtimedwait(2)` for further + information). + + See also :func:`pause`, :func:`sigwait` and :func:`sigwaitinfo`. .. versionadded:: 3.3 diff --git a/Doc/whatsnew/3.3.rst b/Doc/whatsnew/3.3.rst index e1d1cb4b3cd..4374d0267d4 100644 --- a/Doc/whatsnew/3.3.rst +++ b/Doc/whatsnew/3.3.rst @@ -169,6 +169,10 @@ signal * :func:`~signal.pthread_kill`: send a signal to a thread ; * :func:`~signal.sigpending`: examine pending functions ; * :func:`~signal.sigwait`: wait a signal. + * :func:`~signal.sigwaitinfo`: wait for a signal, returning detailed + information about it. + * :func:`~signal.sigtimedwait`: like :func:`~signal.sigwaitinfo` but with a + timeout. * The signal handler writes the signal number as a single byte instead of a nul byte into the wakeup file descriptor. So it is possible to wait more diff --git a/Lib/test/test_signal.py b/Lib/test/test_signal.py index f23031668d6..392280dd230 100644 --- a/Lib/test/test_signal.py +++ b/Lib/test/test_signal.py @@ -520,6 +520,7 @@ class PendingSignalsTests(unittest.TestCase): functions. """ def setUp(self): + self.hndl_called = False self.has_pthread_kill = hasattr(signal, 'pthread_kill') def handler(self, signum, frame): @@ -607,45 +608,35 @@ class PendingSignalsTests(unittest.TestCase): with self.assertRaises(ZeroDivisionError): signal.pthread_kill(current, signum) - @unittest.skipUnless(hasattr(signal, 'sigwait'), - 'need signal.sigwait()') @unittest.skipUnless(hasattr(signal, 'pthread_sigmask'), 'need signal.pthread_sigmask()') @unittest.skipUnless(hasattr(os, 'fork'), 'need os.fork()') - def test_sigwait(self): - def test(signum): - signal.alarm(1) - received = signal.sigwait([signum]) - if received != signum: - print("sigwait() received %s, not %s" - % (received, signum), - file=sys.stderr) - os._exit(1) - + def wait_helper(self, test, handler, blocked=signal.SIGALRM): signum = signal.SIGALRM - # sigwait must be called with the signal blocked: since the current + # sig*wait* must be called with the signal blocked: since the current # process might have several threads running, we fork() a child process # to have a single thread. pid = os.fork() if pid == 0: # child: block and wait the signal try: - signal.signal(signum, self.handler) - signal.pthread_sigmask(signal.SIG_BLOCK, [signum]) + signal.signal(signum, handler) + signal.pthread_sigmask(signal.SIG_BLOCK, [blocked]) # Do the tests test(signum) # The handler must not be called on unblock try: - signal.pthread_sigmask(signal.SIG_UNBLOCK, [signum]) + signal.pthread_sigmask(signal.SIG_UNBLOCK, [blocked]) except ZeroDivisionError: print("the signal handler has been called", file=sys.stderr) os._exit(1) except BaseException as err: print("error: {}".format(err), file=sys.stderr) + sys.stderr.flush() os._exit(1) else: os._exit(0) @@ -653,6 +644,82 @@ class PendingSignalsTests(unittest.TestCase): # parent: check that the child correcty received the signal self.assertEqual(os.waitpid(pid, 0), (pid, 0)) + @unittest.skipUnless(hasattr(signal, 'sigwait'), + 'need signal.sigwait()') + def test_sigwait(self): + def test(signum): + signal.alarm(1) + self.assertEqual(signum, signal.sigwait([signum])) + + self.wait_helper(test, self.handler) + + @unittest.skipUnless(hasattr(signal, 'sigwaitinfo'), + 'need signal.sigwaitinfo()') + def test_sigwaitinfo(self): + def test(signum): + signal.alarm(1) + info = signal.sigwaitinfo([signum]) + self.assertEqual(signum, info.si_signo) + + self.wait_helper(test, self.handler) + + @unittest.skipUnless(hasattr(signal, 'sigtimedwait'), + 'need signal.sigtimedwait()') + def test_sigtimedwait(self): + def test(signum): + signal.alarm(1) + info = signal.sigtimedwait([signum], (10, 1000)) + self.assertEqual(signum, info.si_signo) + + self.wait_helper(test, self.handler) + + # check that polling with sigtimedwait works + @unittest.skipUnless(hasattr(signal, 'sigtimedwait'), + 'need signal.sigtimedwait()') + def test_sigtimedwait_poll(self): + def test(signum): + self.kill(signum) + info = signal.sigtimedwait([signum], (0, 0)) + self.assertEqual(signum, info.si_signo) + + self.wait_helper(test, self.handler) + + @unittest.skipUnless(hasattr(signal, 'sigtimedwait'), + 'need signal.sigtimedwait()') + def test_sigtimedwait_timeout(self): + def test(signum): + self.assertEqual(None, signal.sigtimedwait([signum], (1, 35500))) + + self.wait_helper(test, self.handler) + + @unittest.skipUnless(hasattr(signal, 'sigtimedwait'), + 'need signal.sigtimedwait()') + def test_sigtimedwait_negative_timeout(self): + signum = signal.SIGALRM + self.assertRaises(ValueError, signal.sigtimedwait, [signum], (-1, -1)) + self.assertRaises(ValueError, signal.sigtimedwait, [signum], (0, -1)) + self.assertRaises(ValueError, signal.sigtimedwait, [signum], (-1, 0)) + + def alarm_handler(self, signum, frame): + self.hndl_called = True + + @unittest.skipUnless(hasattr(signal, 'sigwaitinfo'), + 'need signal.sigwaitinfo()') + def test_sigwaitinfo_interrupted(self): + def test(signum): + signal.alarm(1) + try: + signal.sigwaitinfo([signal.SIGUSR1]) + except OSError as e: + if e.errno == errno.EINTR: + self.assertTrue(self.hndl_called) + else: + self.fail("Expected EINTR to be raised by sigwaitinfo") + else: + self.fail("Expected EINTR to be raised by sigwaitinfo") + + self.wait_helper(test, self.alarm_handler, signal.SIGUSR1) + @unittest.skipUnless(hasattr(signal, 'sigwait'), 'need signal.sigwait()') @unittest.skipUnless(hasattr(signal, 'pthread_sigmask'), diff --git a/Misc/NEWS b/Misc/NEWS index 6786cad716a..cbbc975f3ae 100644 --- a/Misc/NEWS +++ b/Misc/NEWS @@ -200,6 +200,8 @@ Core and Builtins Library ------- +- Issue #12303: Add sigwaitinfo() and sigtimedwait() to the signal module. + - Issue #12404: Remove C89 incompatible code from mmap module. Patch by Akira Kitada. diff --git a/Modules/signalmodule.c b/Modules/signalmodule.c index 94e6bcbf312..45a7dfad3bc 100644 --- a/Modules/signalmodule.c +++ b/Modules/signalmodule.c @@ -507,7 +507,8 @@ PyDoc_STRVAR(getitimer_doc, Returns current value of given itimer."); #endif -#if defined(PYPTHREAD_SIGMASK) || defined(HAVE_SIGWAIT) +#if defined(PYPTHREAD_SIGMASK) || defined(HAVE_SIGWAIT) || \ + defined(HAVE_SIGWAITINFO) || defined(HAVE_SIGTIMEDWAIT) /* Convert an iterable to a sigset. Return 0 on success, return -1 and raise an exception on error. */ @@ -679,6 +680,140 @@ PyDoc_STRVAR(signal_sigwait_doc, Wait a signal."); #endif /* #ifdef HAVE_SIGPENDING */ +#if defined(HAVE_SIGWAITINFO) || defined(HAVE_SIGTIMEDWAIT) +static int initialized; +static PyStructSequence_Field struct_siginfo_fields[] = { + {"si_signo", "signal number"}, + {"si_code", "signal code"}, + {"si_errno", "errno associated with this signal"}, + {"si_pid", "sending process ID"}, + {"si_uid", "real user ID of sending process"}, + {"si_status", "exit value or signal"}, + {"si_band", "band event for SIGPOLL"}, + {0} +}; + +PyDoc_STRVAR(struct_siginfo__doc__, +"struct_siginfo: Result from sigwaitinfo or sigtimedwait.\n\n\ +This object may be accessed either as a tuple of\n\ +(si_signo, si_code, si_errno, si_pid, si_uid, si_status, si_band),\n\ +or via the attributes si_signo, si_code, and so on."); + +static PyStructSequence_Desc struct_siginfo_desc = { + "signal.struct_siginfo", /* name */ + struct_siginfo__doc__, /* doc */ + struct_siginfo_fields, /* fields */ + 7 /* n_in_sequence */ +}; + +static PyTypeObject SiginfoType; + +static PyObject * +fill_siginfo(siginfo_t *si) +{ + PyObject *result = PyStructSequence_New(&SiginfoType); + if (!result) + return NULL; + + PyStructSequence_SET_ITEM(result, 0, PyLong_FromLong((long)(si->si_signo))); + PyStructSequence_SET_ITEM(result, 1, PyLong_FromLong((long)(si->si_code))); + PyStructSequence_SET_ITEM(result, 2, PyLong_FromLong((long)(si->si_errno))); + PyStructSequence_SET_ITEM(result, 3, PyLong_FromPid(si->si_pid)); + PyStructSequence_SET_ITEM(result, 4, PyLong_FromLong((long)(si->si_uid))); + PyStructSequence_SET_ITEM(result, 5, + PyLong_FromLong((long)(si->si_status))); + PyStructSequence_SET_ITEM(result, 6, PyLong_FromLong(si->si_band)); + if (PyErr_Occurred()) { + Py_DECREF(result); + return NULL; + } + + return result; +} +#endif + +#ifdef HAVE_SIGWAITINFO +static PyObject * +signal_sigwaitinfo(PyObject *self, PyObject *args) +{ + PyObject *signals; + sigset_t set; + siginfo_t si; + int err; + + if (!PyArg_ParseTuple(args, "O:sigwaitinfo", &signals)) + return NULL; + + if (iterable_to_sigset(signals, &set)) + return NULL; + + Py_BEGIN_ALLOW_THREADS + err = sigwaitinfo(&set, &si); + Py_END_ALLOW_THREADS + if (err == -1) + return PyErr_SetFromErrno(PyExc_OSError); + + return fill_siginfo(&si); +} + +PyDoc_STRVAR(signal_sigwaitinfo_doc, +"sigwaitinfo(sigset) -> struct_siginfo\n\ +\n\ +Wait synchronously for a signal until one of the signals in *sigset* is\n\ +delivered.\n\ +Returns a struct_siginfo containing information about the signal."); +#endif /* #ifdef HAVE_SIGWAITINFO */ + +#ifdef HAVE_SIGTIMEDWAIT +static PyObject * +signal_sigtimedwait(PyObject *self, PyObject *args) +{ + PyObject *signals, *timeout; + struct timespec buf; + sigset_t set; + siginfo_t si; + int err; + + if (!PyArg_ParseTuple(args, "OO:sigtimedwait", &signals, &timeout)) + return NULL; + + if (!PyTuple_Check(timeout) || PyTuple_Size(timeout) != 2) { + PyErr_SetString(PyExc_TypeError, + "sigtimedwait() arg 2 must be a tuple " + "(timeout_sec, timeout_nsec)"); + return NULL; + } else if (!PyArg_ParseTuple(timeout, "ll:sigtimedwait", + &(buf.tv_sec), &(buf.tv_nsec))) + return NULL; + + if (buf.tv_sec < 0 || buf.tv_nsec < 0) { + PyErr_SetString(PyExc_ValueError, "timeout must be non-negative"); + return NULL; + } + + if (iterable_to_sigset(signals, &set)) + return NULL; + + Py_BEGIN_ALLOW_THREADS + err = sigtimedwait(&set, &si, &buf); + Py_END_ALLOW_THREADS + if (err == -1) { + if (errno == EAGAIN) + Py_RETURN_NONE; + else + return PyErr_SetFromErrno(PyExc_OSError); + } + + return fill_siginfo(&si); +} + +PyDoc_STRVAR(signal_sigtimedwait_doc, +"sigtimedwait(sigset, (timeout_sec, timeout_nsec)) -> struct_siginfo\n\ +\n\ +Like sigwaitinfo(), but with a timeout specified as a tuple of (seconds,\n\ +nanoseconds)."); +#endif /* #ifdef HAVE_SIGTIMEDWAIT */ + #if defined(HAVE_PTHREAD_KILL) && defined(WITH_THREAD) static PyObject * @@ -751,6 +886,14 @@ static PyMethodDef signal_methods[] = { #ifdef HAVE_SIGWAIT {"sigwait", (PyCFunction)signal_sigwait, METH_VARARGS, signal_sigwait_doc}, +#endif +#ifdef HAVE_SIGWAITINFO + {"sigwaitinfo", (PyCFunction)signal_sigwaitinfo, + METH_VARARGS, signal_sigwaitinfo_doc}, +#endif +#ifdef HAVE_SIGTIMEDWAIT + {"sigtimedwait", (PyCFunction)signal_sigtimedwait, + METH_VARARGS, signal_sigtimedwait_doc}, #endif {NULL, NULL} /* sentinel */ }; @@ -820,6 +963,15 @@ PyInit_signal(void) if (m == NULL) return NULL; +#if defined(HAVE_SIGWAITINFO) || defined(HAVE_SIGTIMEDWAIT) + if (!initialized) + PyStructSequence_InitType(&SiginfoType, &struct_siginfo_desc); + + Py_INCREF((PyObject*) &SiginfoType); + PyModule_AddObject(m, "struct_siginfo", (PyObject*) &SiginfoType); + initialized = 1; +#endif + /* Add some symbolic constants to the module */ d = PyModule_GetDict(m); diff --git a/configure b/configure index 7540e364ed5..4234a040220 100755 --- a/configure +++ b/configure @@ -9367,8 +9367,8 @@ for ac_func in alarm accept4 setitimer getitimer bind_textdomain_codeset chown \ 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 sigpending \ - sigrelse sigwait snprintf strftime strlcpy symlinkat sync \ + sigaction sigaltstack siginterrupt sigpending sigrelse \ + sigtimedwait sigwait sigwaitinfo 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 index e0b2ecb3a3b..a700288b1f4 100644 --- a/configure.in +++ b/configure.in @@ -2552,8 +2552,8 @@ AC_CHECK_FUNCS(alarm accept4 setitimer getitimer bind_textdomain_codeset chown \ 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 sigpending \ - sigrelse sigwait snprintf strftime strlcpy symlinkat sync \ + sigaction sigaltstack siginterrupt sigpending sigrelse \ + sigtimedwait sigwait sigwaitinfo 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 index 49350779694..d2af3e9a353 100644 --- a/pyconfig.h.in +++ b/pyconfig.h.in @@ -743,9 +743,15 @@ /* Define to 1 if you have the `sigrelse' function. */ #undef HAVE_SIGRELSE +/* Define to 1 if you have the `sigtimedwait' function. */ +#undef HAVE_SIGTIMEDWAIT + /* Define to 1 if you have the `sigwait' function. */ #undef HAVE_SIGWAIT +/* Define to 1 if you have the `sigwaitinfo' function. */ +#undef HAVE_SIGWAITINFO + /* Define to 1 if you have the `snprintf' function. */ #undef HAVE_SNPRINTF