Skip to content

bpo-33332: Add signal.valid_signals() #6581

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
May 4, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 11 additions & 2 deletions Doc/library/signal.rst
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Loading