Skip to content

Commit

Permalink
bpo-33332: Add signal.valid_signals() (GH-6581)
Browse files Browse the repository at this point in the history
  • Loading branch information
pitrou committed May 4, 2018
1 parent 491bbed commit 9d3627e
Show file tree
Hide file tree
Showing 14 changed files with 171 additions and 26 deletions.
13 changes: 11 additions & 2 deletions Doc/library/signal.rst
Expand Up @@ -216,6 +216,15 @@ The :mod:`signal` module defines the following functions:
.. versionadded:: 3.8


.. function:: valid_signals()

Return the set of valid signal numbers on this platform. This can be
less than ``range(1, NSIG)`` if some signals are reserved by the system
for internal use.

.. versionadded:: 3.8


.. function:: pause()

Cause the process to sleep until a signal is received; the appropriate handler
Expand Down Expand Up @@ -268,8 +277,8 @@ The :mod:`signal` module defines the following functions:
argument.

*mask* is a set of signal numbers (e.g. {:const:`signal.SIGINT`,
:const:`signal.SIGTERM`}). Use ``range(1, signal.NSIG)`` for a full mask
including all signals.
:const:`signal.SIGTERM`}). Use :func:`~signal.valid_signals` for a full
mask including all signals.

For example, ``signal.pthread_sigmask(signal.SIG_BLOCK, [])`` reads the
signal mask of the calling thread.
Expand Down
4 changes: 2 additions & 2 deletions Lib/asyncio/unix_events.py
Expand Up @@ -167,8 +167,8 @@ def _check_signal(self, sig):
if not isinstance(sig, int):
raise TypeError(f'sig must be an int, not {sig!r}')

if not (1 <= sig < signal.NSIG):
raise ValueError(f'sig {sig} out of range(1, {signal.NSIG})')
if sig not in signal.valid_signals():
raise ValueError(f'invalid signal number {sig}')

def _make_read_pipe_transport(self, pipe, protocol, waiter=None,
extra=None):
Expand Down
2 changes: 1 addition & 1 deletion Lib/multiprocessing/resource_sharer.py
Expand Up @@ -136,7 +136,7 @@ def _start(self):

def _serve(self):
if hasattr(signal, 'pthread_sigmask'):
signal.pthread_sigmask(signal.SIG_BLOCK, range(1, signal.NSIG))
signal.pthread_sigmask(signal.SIG_BLOCK, signal.valid_signals())
while 1:
try:
with self._listener.accept() as conn:
Expand Down
10 changes: 8 additions & 2 deletions Lib/signal.py
Expand Up @@ -65,8 +65,7 @@ def pthread_sigmask(how, mask):
if 'sigpending' in _globals:
@_wraps(_signal.sigpending)
def sigpending():
sigs = _signal.sigpending()
return set(_int_to_enum(x, Signals) for x in sigs)
return {_int_to_enum(x, Signals) for x in _signal.sigpending()}


if 'sigwait' in _globals:
Expand All @@ -76,4 +75,11 @@ def sigwait(sigset):
return _int_to_enum(retsig, Signals)
sigwait.__doc__ = _signal.sigwait


if 'valid_signals' in _globals:
@_wraps(_signal.valid_signals)
def valid_signals():
return {_int_to_enum(x, Signals) for x in _signal.valid_signals()}


del _globals, _wraps
2 changes: 1 addition & 1 deletion Lib/test/support/__init__.py
Expand Up @@ -2810,7 +2810,7 @@ class SaveSignals:
def __init__(self):
import signal
self.signal = signal
self.signals = list(range(1, signal.NSIG))
self.signals = signal.valid_signals()
# SIGKILL and SIGSTOP signals cannot be ignored nor caught
for signame in ('SIGKILL', 'SIGSTOP'):
try:
Expand Down
12 changes: 12 additions & 0 deletions Lib/test/test_asyncio/test_unix_events.py
Expand Up @@ -69,6 +69,7 @@ def test_handle_signal_cancelled_handler(self):
@mock.patch('asyncio.unix_events.signal')
def test_add_signal_handler_setup_error(self, m_signal):
m_signal.NSIG = signal.NSIG
m_signal.valid_signals = signal.valid_signals
m_signal.set_wakeup_fd.side_effect = ValueError

self.assertRaises(
Expand Down Expand Up @@ -96,6 +97,7 @@ async def simple_coroutine():
@mock.patch('asyncio.unix_events.signal')
def test_add_signal_handler(self, m_signal):
m_signal.NSIG = signal.NSIG
m_signal.valid_signals = signal.valid_signals

cb = lambda: True
self.loop.add_signal_handler(signal.SIGHUP, cb)
Expand All @@ -106,6 +108,7 @@ def test_add_signal_handler(self, m_signal):
@mock.patch('asyncio.unix_events.signal')
def test_add_signal_handler_install_error(self, m_signal):
m_signal.NSIG = signal.NSIG
m_signal.valid_signals = signal.valid_signals

def set_wakeup_fd(fd):
if fd == -1:
Expand All @@ -125,6 +128,7 @@ class Err(OSError):
@mock.patch('asyncio.base_events.logger')
def test_add_signal_handler_install_error2(self, m_logging, m_signal):
m_signal.NSIG = signal.NSIG
m_signal.valid_signals = signal.valid_signals

class Err(OSError):
errno = errno.EINVAL
Expand All @@ -145,6 +149,7 @@ class Err(OSError):
errno = errno.EINVAL
m_signal.signal.side_effect = Err
m_signal.NSIG = signal.NSIG
m_signal.valid_signals = signal.valid_signals

self.assertRaises(
RuntimeError,
Expand All @@ -156,6 +161,7 @@ class Err(OSError):
@mock.patch('asyncio.unix_events.signal')
def test_remove_signal_handler(self, m_signal):
m_signal.NSIG = signal.NSIG
m_signal.valid_signals = signal.valid_signals

self.loop.add_signal_handler(signal.SIGHUP, lambda: True)

Expand All @@ -170,6 +176,7 @@ def test_remove_signal_handler(self, m_signal):
def test_remove_signal_handler_2(self, m_signal):
m_signal.NSIG = signal.NSIG
m_signal.SIGINT = signal.SIGINT
m_signal.valid_signals = signal.valid_signals

self.loop.add_signal_handler(signal.SIGINT, lambda: True)
self.loop._signal_handlers[signal.SIGHUP] = object()
Expand All @@ -187,6 +194,7 @@ def test_remove_signal_handler_2(self, m_signal):
@mock.patch('asyncio.base_events.logger')
def test_remove_signal_handler_cleanup_error(self, m_logging, m_signal):
m_signal.NSIG = signal.NSIG
m_signal.valid_signals = signal.valid_signals
self.loop.add_signal_handler(signal.SIGHUP, lambda: True)

m_signal.set_wakeup_fd.side_effect = ValueError
Expand All @@ -197,6 +205,7 @@ def test_remove_signal_handler_cleanup_error(self, m_logging, m_signal):
@mock.patch('asyncio.unix_events.signal')
def test_remove_signal_handler_error(self, m_signal):
m_signal.NSIG = signal.NSIG
m_signal.valid_signals = signal.valid_signals
self.loop.add_signal_handler(signal.SIGHUP, lambda: True)

m_signal.signal.side_effect = OSError
Expand All @@ -207,6 +216,7 @@ def test_remove_signal_handler_error(self, m_signal):
@mock.patch('asyncio.unix_events.signal')
def test_remove_signal_handler_error2(self, m_signal):
m_signal.NSIG = signal.NSIG
m_signal.valid_signals = signal.valid_signals
self.loop.add_signal_handler(signal.SIGHUP, lambda: True)

class Err(OSError):
Expand All @@ -219,6 +229,7 @@ class Err(OSError):
@mock.patch('asyncio.unix_events.signal')
def test_close(self, m_signal):
m_signal.NSIG = signal.NSIG
m_signal.valid_signals = signal.valid_signals

self.loop.add_signal_handler(signal.SIGHUP, lambda: True)
self.loop.add_signal_handler(signal.SIGCHLD, lambda: True)
Expand All @@ -236,6 +247,7 @@ def test_close(self, m_signal):
@mock.patch('asyncio.unix_events.signal')
def test_close_on_finalizing(self, m_signal, m_sys):
m_signal.NSIG = signal.NSIG
m_signal.valid_signals = signal.valid_signals
self.loop.add_signal_handler(signal.SIGHUP, lambda: True)

self.assertEqual(len(self.loop._signal_handlers), 1)
Expand Down
30 changes: 30 additions & 0 deletions Lib/test/test_signal.py
Expand Up @@ -67,9 +67,28 @@ def test_interprocess_signal(self):
script = os.path.join(dirname, 'signalinterproctester.py')
assert_python_ok(script)

def test_valid_signals(self):
s = signal.valid_signals()
self.assertIsInstance(s, set)
self.assertIn(signal.Signals.SIGINT, s)
self.assertIn(signal.Signals.SIGALRM, s)
self.assertNotIn(0, s)
self.assertNotIn(signal.NSIG, s)
self.assertLess(len(s), signal.NSIG)


@unittest.skipUnless(sys.platform == "win32", "Windows specific")
class WindowsSignalTests(unittest.TestCase):

def test_valid_signals(self):
s = signal.valid_signals()
self.assertIsInstance(s, set)
self.assertGreaterEqual(len(s), 6)
self.assertIn(signal.Signals.SIGINT, s)
self.assertNotIn(0, s)
self.assertNotIn(signal.NSIG, s)
self.assertLess(len(s), signal.NSIG)

def test_issue9324(self):
# Updated for issue #10003, adding SIGBREAK
handler = lambda x, y: None
Expand Down Expand Up @@ -922,6 +941,17 @@ def test_pthread_sigmask_arguments(self):
self.assertRaises(TypeError, signal.pthread_sigmask, 1)
self.assertRaises(TypeError, signal.pthread_sigmask, 1, 2, 3)
self.assertRaises(OSError, signal.pthread_sigmask, 1700, [])
with self.assertRaises(ValueError):
signal.pthread_sigmask(signal.SIG_BLOCK, [signal.NSIG])

@unittest.skipUnless(hasattr(signal, 'pthread_sigmask'),
'need signal.pthread_sigmask()')
def test_pthread_sigmask_valid_signals(self):
s = signal.pthread_sigmask(signal.SIG_BLOCK, signal.valid_signals())
self.addCleanup(signal.pthread_sigmask, signal.SIG_SETMASK, s)
# Get current blocked set
s = signal.pthread_sigmask(signal.SIG_UNBLOCK, signal.valid_signals())
self.assertLessEqual(s, signal.valid_signals())

@unittest.skipUnless(hasattr(signal, 'pthread_sigmask'),
'need signal.pthread_sigmask()')
Expand Down
@@ -0,0 +1,2 @@
Add ``signal.valid_signals()`` to expose the POSIX sigfillset()
functionality.
31 changes: 30 additions & 1 deletion Modules/clinic/signalmodule.c.h
Expand Up @@ -341,6 +341,31 @@ PyDoc_STRVAR(signal_sigwait__doc__,

#endif /* defined(HAVE_SIGWAIT) */

#if (defined(HAVE_SIGFILLSET) || defined(MS_WINDOWS))

PyDoc_STRVAR(signal_valid_signals__doc__,
"valid_signals($module, /)\n"
"--\n"
"\n"
"Return a set of valid signal numbers on this platform.\n"
"\n"
"The signal numbers returned by this function can be safely passed to\n"
"functions like `pthread_sigmask`.");

#define SIGNAL_VALID_SIGNALS_METHODDEF \
{"valid_signals", (PyCFunction)signal_valid_signals, METH_NOARGS, signal_valid_signals__doc__},

static PyObject *
signal_valid_signals_impl(PyObject *module);

static PyObject *
signal_valid_signals(PyObject *module, PyObject *Py_UNUSED(ignored))
{
return signal_valid_signals_impl(module);
}

#endif /* (defined(HAVE_SIGFILLSET) || defined(MS_WINDOWS)) */

#if defined(HAVE_SIGWAITINFO)

PyDoc_STRVAR(signal_sigwaitinfo__doc__,
Expand Down Expand Up @@ -459,6 +484,10 @@ signal_pthread_kill(PyObject *module, PyObject *const *args, Py_ssize_t nargs)
#define SIGNAL_SIGWAIT_METHODDEF
#endif /* !defined(SIGNAL_SIGWAIT_METHODDEF) */

#ifndef SIGNAL_VALID_SIGNALS_METHODDEF
#define SIGNAL_VALID_SIGNALS_METHODDEF
#endif /* !defined(SIGNAL_VALID_SIGNALS_METHODDEF) */

#ifndef SIGNAL_SIGWAITINFO_METHODDEF
#define SIGNAL_SIGWAITINFO_METHODDEF
#endif /* !defined(SIGNAL_SIGWAITINFO_METHODDEF) */
Expand All @@ -470,4 +499,4 @@ signal_pthread_kill(PyObject *module, PyObject *const *args, Py_ssize_t nargs)
#ifndef SIGNAL_PTHREAD_KILL_METHODDEF
#define SIGNAL_PTHREAD_KILL_METHODDEF
#endif /* !defined(SIGNAL_PTHREAD_KILL_METHODDEF) */
/*[clinic end generated code: output=7b41486acf93aa8e input=a9049054013a1b77]*/
/*[clinic end generated code: output=f35d79e0cfee3f1b input=a9049054013a1b77]*/
64 changes: 59 additions & 5 deletions Modules/signalmodule.c
Expand Up @@ -841,11 +841,21 @@ iterable_to_sigset(PyObject *iterable, sigset_t *mask)
if (signum == -1 && PyErr_Occurred())
goto error;
if (0 < signum && signum < NSIG) {
/* bpo-33329: ignore sigaddset() return value as it can fail
* for some reserved signals, but we want the `range(1, NSIG)`
* idiom to allow selecting all valid signals.
*/
(void) sigaddset(mask, (int)signum);
if (sigaddset(mask, (int)signum)) {
if (errno != EINVAL) {
/* Probably impossible */
PyErr_SetFromErrno(PyExc_OSError);
goto error;
}
/* For backwards compatibility, allow idioms such as
* `range(1, NSIG)` but warn about invalid signal numbers
*/
const char *msg =
"invalid signal number %ld, please use valid_signals()";
if (PyErr_WarnFormat(PyExc_RuntimeWarning, 1, msg, signum)) {
goto error;
}
}
}
else {
PyErr_Format(PyExc_ValueError,
Expand Down Expand Up @@ -1001,6 +1011,47 @@ signal_sigwait(PyObject *module, PyObject *sigset)
#endif /* #ifdef HAVE_SIGWAIT */


#if defined(HAVE_SIGFILLSET) || defined(MS_WINDOWS)

/*[clinic input]
signal.valid_signals
Return a set of valid signal numbers on this platform.
The signal numbers returned by this function can be safely passed to
functions like `pthread_sigmask`.
[clinic start generated code]*/

static PyObject *
signal_valid_signals_impl(PyObject *module)
/*[clinic end generated code: output=1609cffbcfcf1314 input=86a3717ff25288f2]*/
{
#ifdef MS_WINDOWS
#ifdef SIGBREAK
PyObject *tup = Py_BuildValue("(iiiiiii)", SIGABRT, SIGBREAK, SIGFPE,
SIGILL, SIGINT, SIGSEGV, SIGTERM);
#else
PyObject *tup = Py_BuildValue("(iiiiii)", SIGABRT, SIGFPE, SIGILL,
SIGINT, SIGSEGV, SIGTERM);
#endif
if (tup == NULL) {
return NULL;
}
PyObject *set = PySet_New(tup);
Py_DECREF(tup);
return set;
#else
sigset_t mask;
if (sigemptyset(&mask) || sigfillset(&mask)) {
return PyErr_SetFromErrno(PyExc_OSError);
}
return sigset_to_set(mask);
#endif
}

#endif /* #if defined(HAVE_SIGFILLSET) || defined(MS_WINDOWS) */


#if defined(HAVE_SIGWAITINFO) || defined(HAVE_SIGTIMEDWAIT)
static int initialized;
static PyStructSequence_Field struct_siginfo_fields[] = {
Expand Down Expand Up @@ -1225,6 +1276,9 @@ static PyMethodDef signal_methods[] = {
SIGNAL_SIGWAIT_METHODDEF
SIGNAL_SIGWAITINFO_METHODDEF
SIGNAL_SIGTIMEDWAIT_METHODDEF
#if defined(HAVE_SIGFILLSET) || defined(MS_WINDOWS)
SIGNAL_VALID_SIGNALS_METHODDEF
#endif
{NULL, NULL} /* sentinel */
};

Expand Down

0 comments on commit 9d3627e

Please sign in to comment.