bpo-43356: Allow passing a signal number to interrupt_main() (GH-24755)

Also introduce a new C API ``PyErr_SetInterruptEx(int signum)``.
This commit is contained in:
Antoine Pitrou 2021-03-11 23:35:45 +01:00 committed by GitHub
parent b4fc44bb2d
commit ba251c2ae6
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
11 changed files with 211 additions and 66 deletions

View file

@ -505,29 +505,73 @@ Signal Handling
single: SIGINT single: SIGINT
single: KeyboardInterrupt (built-in exception) single: KeyboardInterrupt (built-in exception)
This function interacts with Python's signal handling. It checks whether a This function interacts with Python's signal handling.
signal has been sent to the processes and if so, invokes the corresponding
signal handler. If the :mod:`signal` module is supported, this can invoke a If the function is called from the main thread and under the main Python
signal handler written in Python. In all cases, the default effect for interpreter, it checks whether a signal has been sent to the processes
:const:`SIGINT` is to raise the :exc:`KeyboardInterrupt` exception. If an and if so, invokes the corresponding signal handler. If the :mod:`signal`
exception is raised the error indicator is set and the function returns ``-1``; module is supported, this can invoke a signal handler written in Python.
otherwise the function returns ``0``. The error indicator may or may not be
cleared if it was previously set. The function attemps to handle all pending signals, and then returns ``0``.
However, if a Python signal handler raises an exception, the error
indicator is set and the function returns ``-1`` immediately (such that
other pending signals may not have been handled yet: they will be on the
next :c:func:`PyErr_CheckSignals()` invocation).
If the function is called from a non-main thread, or under a non-main
Python interpreter, it does nothing and returns ``0``.
This function can be called by long-running C code that wants to
be interruptible by user requests (such as by pressing Ctrl-C).
.. note::
The default Python signal handler for :const:`SIGINT` raises the
:exc:`KeyboardInterrupt` exception.
.. c:function:: void PyErr_SetInterrupt() .. c:function:: void PyErr_SetInterrupt()
.. index:: .. index::
module: signal
single: SIGINT single: SIGINT
single: KeyboardInterrupt (built-in exception) single: KeyboardInterrupt (built-in exception)
Simulate the effect of a :const:`SIGINT` signal arriving. The next time Simulate the effect of a :const:`SIGINT` signal arriving.
:c:func:`PyErr_CheckSignals` is called, the Python signal handler for This is equivalent to ``PyErr_SetInterruptEx(SIGINT)``.
:const:`SIGINT` will be called.
.. note::
This function is async-signal-safe. It can be called without
the :term:`GIL` and from a C signal handler.
.. c:function:: int PyErr_SetInterruptEx(int signum)
.. index::
module: signal
single: KeyboardInterrupt (built-in exception)
Simulate the effect of a signal arriving. The next time
:c:func:`PyErr_CheckSignals` is called, the Python signal handler for
the given signal number will be called.
This function can be called by C code that sets up its own signal handling
and wants Python signal handlers to be invoked as expected when an
interruption is requested (for example when the user presses Ctrl-C
to interrupt an operation).
If the given signal isn't handled by Python (it was set to
:data:`signal.SIG_DFL` or :data:`signal.SIG_IGN`), it will be ignored.
If *signum* is outside of the allowed range of signal numbers, ``-1``
is returned. Otherwise, ``0`` is returned. The error indicator is
never changed by this function.
.. note::
This function is async-signal-safe. It can be called without
the :term:`GIL` and from a C signal handler.
.. versionadded:: 3.10
If :const:`SIGINT` isn't handled by Python (it was set to
:data:`signal.SIG_DFL` or :data:`signal.SIG_IGN`), this function does
nothing.
.. c:function:: int PySignal_SetWakeupFd(int fd) .. c:function:: int PySignal_SetWakeupFd(int fd)

View file

@ -36,6 +36,7 @@ PyCFunction_Call
PyCFunction_GetFlags PyCFunction_GetFlags
PyCFunction_GetFunction PyCFunction_GetFunction
PyCFunction_GetSelf PyCFunction_GetSelf
PyCFunction_New
PyCFunction_NewEx PyCFunction_NewEx
PyCFunction_Type PyCFunction_Type
PyCMethod_New PyCMethod_New
@ -144,6 +145,7 @@ PyErr_SetFromErrnoWithFilenameObjects
PyErr_SetImportError PyErr_SetImportError
PyErr_SetImportErrorSubclass PyErr_SetImportErrorSubclass
PyErr_SetInterrupt PyErr_SetInterrupt
PyErr_SetInterruptEx
PyErr_SetNone PyErr_SetNone
PyErr_SetObject PyErr_SetObject
PyErr_SetString PyErr_SetString

View file

@ -61,15 +61,27 @@ This module defines the following constants and functions:
:func:`sys.unraisablehook` is now used to handle unhandled exceptions. :func:`sys.unraisablehook` is now used to handle unhandled exceptions.
.. function:: interrupt_main() .. function:: interrupt_main(signum=signal.SIGINT, /)
Simulate the effect of a :data:`signal.SIGINT` signal arriving in the main Simulate the effect of a signal arriving in the main thread.
thread. A thread can use this function to interrupt the main thread. A thread can use this function to interrupt the main thread, though
there is no guarantee that the interruption will happen immediately.
If :data:`signal.SIGINT` isn't handled by Python (it was set to If given, *signum* is the number of the signal to simulate.
If *signum* is not given, :data:`signal.SIGINT` is simulated.
If the given signal isn't handled by Python (it was set to
:data:`signal.SIG_DFL` or :data:`signal.SIG_IGN`), this function does :data:`signal.SIG_DFL` or :data:`signal.SIG_IGN`), this function does
nothing. nothing.
.. versionchanged:: 3.10
The *signum* argument is added to customize the signal number.
.. note::
This does not emit the corresponding signal but schedules a call to
the associated handler (if it exists).
If you want to truly emit the signal, use :func:`signal.raise_signal`.
.. function:: exit() .. function:: exit()

View file

@ -788,6 +788,13 @@ Add :data:`sys.stdlib_module_names`, containing the list of the standard library
module names. module names.
(Contributed by Victor Stinner in :issue:`42955`.) (Contributed by Victor Stinner in :issue:`42955`.)
_thread
-------
:func:`_thread.interrupt_main` now takes an optional signal number to
simulate (the default is still :data:`signal.SIGINT`).
(Contributed by Antoine Pitrou in :issue:`43356`.)
threading threading
--------- ---------
@ -1210,6 +1217,11 @@ New Features
object is an instance of :class:`set` but not an instance of a subtype. object is an instance of :class:`set` but not an instance of a subtype.
(Contributed by Pablo Galindo in :issue:`43277`.) (Contributed by Pablo Galindo in :issue:`43277`.)
* Added :c:func:`PyErr_SetInterruptEx` which allows passing a signal number
to simulate.
(Contributed by Antoine Pitrou in :issue:`43356`.)
Porting to Python 3.10 Porting to Python 3.10
---------------------- ----------------------

View file

@ -8,8 +8,24 @@ extern "C" {
# error "this header requires Py_BUILD_CORE define" # error "this header requires Py_BUILD_CORE define"
#endif #endif
#ifdef HAVE_SIGNAL_H
#include <signal.h>
#endif
#include "pycore_runtime.h" // _PyRuntimeState #include "pycore_runtime.h" // _PyRuntimeState
#ifndef NSIG
# if defined(_NSIG)
# define NSIG _NSIG /* For BSD/SysV */
# elif defined(_SIGMAX)
# define NSIG (_SIGMAX + 1) /* For QNX */
# elif defined(SIGMAX)
# define NSIG (SIGMAX + 1) /* For djgpp */
# else
# define NSIG 64 /* Use a reasonable default value */
# endif
#endif
/* Forward declarations */ /* Forward declarations */
struct _PyArgv; struct _PyArgv;
struct pyruntimestate; struct pyruntimestate;

View file

@ -224,6 +224,9 @@ PyAPI_FUNC(void) PyErr_WriteUnraisable(PyObject *);
/* In signalmodule.c */ /* In signalmodule.c */
PyAPI_FUNC(int) PyErr_CheckSignals(void); PyAPI_FUNC(int) PyErr_CheckSignals(void);
PyAPI_FUNC(void) PyErr_SetInterrupt(void); PyAPI_FUNC(void) PyErr_SetInterrupt(void);
#if !defined(Py_LIMITED_API) || Py_LIMITED_API+0 >= 0x030A0000
PyAPI_FUNC(int) PyErr_SetInterruptEx(int signum);
#endif
/* Support for adding program text to SyntaxErrors */ /* Support for adding program text to SyntaxErrors */
PyAPI_FUNC(void) PyErr_SyntaxLocation( PyAPI_FUNC(void) PyErr_SyntaxLocation(

View file

@ -1489,6 +1489,29 @@ class MiscTestCase(unittest.TestCase):
class InterruptMainTests(unittest.TestCase): class InterruptMainTests(unittest.TestCase):
def check_interrupt_main_with_signal_handler(self, signum):
def handler(signum, frame):
1/0
old_handler = signal.signal(signum, handler)
self.addCleanup(signal.signal, signum, old_handler)
with self.assertRaises(ZeroDivisionError):
_thread.interrupt_main()
def check_interrupt_main_noerror(self, signum):
handler = signal.getsignal(signum)
try:
# No exception should arise.
signal.signal(signum, signal.SIG_IGN)
_thread.interrupt_main(signum)
signal.signal(signum, signal.SIG_DFL)
_thread.interrupt_main(signum)
finally:
# Restore original handler
signal.signal(signum, handler)
def test_interrupt_main_subthread(self): def test_interrupt_main_subthread(self):
# Calling start_new_thread with a function that executes interrupt_main # Calling start_new_thread with a function that executes interrupt_main
# should raise KeyboardInterrupt upon completion. # should raise KeyboardInterrupt upon completion.
@ -1506,18 +1529,18 @@ class InterruptMainTests(unittest.TestCase):
with self.assertRaises(KeyboardInterrupt): with self.assertRaises(KeyboardInterrupt):
_thread.interrupt_main() _thread.interrupt_main()
def test_interrupt_main_noerror(self): def test_interrupt_main_with_signal_handler(self):
handler = signal.getsignal(signal.SIGINT) self.check_interrupt_main_with_signal_handler(signal.SIGINT)
try: self.check_interrupt_main_with_signal_handler(signal.SIGTERM)
# No exception should arise.
signal.signal(signal.SIGINT, signal.SIG_IGN)
_thread.interrupt_main()
signal.signal(signal.SIGINT, signal.SIG_DFL) def test_interrupt_main_noerror(self):
_thread.interrupt_main() self.check_interrupt_main_noerror(signal.SIGINT)
finally: self.check_interrupt_main_noerror(signal.SIGTERM)
# Restore original handler
signal.signal(signal.SIGINT, handler) def test_interrupt_main_invalid_signal(self):
self.assertRaises(ValueError, _thread.interrupt_main, -1)
self.assertRaises(ValueError, _thread.interrupt_main, signal.NSIG)
self.assertRaises(ValueError, _thread.interrupt_main, 1000000)
class AtexitTests(unittest.TestCase): class AtexitTests(unittest.TestCase):

View file

@ -0,0 +1 @@
Allow passing a signal number to ``_thread.interrupt_main()``.

View file

@ -9,6 +9,10 @@
#include <stddef.h> // offsetof() #include <stddef.h> // offsetof()
#include "structmember.h" // PyMemberDef #include "structmember.h" // PyMemberDef
#ifdef HAVE_SIGNAL_H
# include <signal.h> // SIGINT
#endif
// ThreadError is just an alias to PyExc_RuntimeError // ThreadError is just an alias to PyExc_RuntimeError
#define ThreadError PyExc_RuntimeError #define ThreadError PyExc_RuntimeError
@ -1173,17 +1177,29 @@ This is synonymous to ``raise SystemExit''. It will cause the current\n\
thread to exit silently unless the exception is caught."); thread to exit silently unless the exception is caught.");
static PyObject * static PyObject *
thread_PyThread_interrupt_main(PyObject * self, PyObject *Py_UNUSED(ignored)) thread_PyThread_interrupt_main(PyObject *self, PyObject *args)
{ {
PyErr_SetInterrupt(); int signum = SIGINT;
if (!PyArg_ParseTuple(args, "|i:signum", &signum)) {
return NULL;
}
if (PyErr_SetInterruptEx(signum)) {
PyErr_SetString(PyExc_ValueError, "signal number out of range");
return NULL;
}
Py_RETURN_NONE; Py_RETURN_NONE;
} }
PyDoc_STRVAR(interrupt_doc, PyDoc_STRVAR(interrupt_doc,
"interrupt_main()\n\ "interrupt_main(signum=signal.SIGINT, /)\n\
\n\ \n\
Raise a KeyboardInterrupt in the main thread.\n\ Simulate the arrival of the given signal in the main thread,\n\
A subthread can use this function to interrupt the main thread." where the corresponding signal handler will be executed.\n\
If *signum* is omitted, SIGINT is assumed.\n\
A subthread can use this function to interrupt the main thread.\n\
\n\
Note: the default signal hander for SIGINT raises ``KeyboardInterrupt``."
); );
static lockobject *newlockobject(PyObject *module); static lockobject *newlockobject(PyObject *module);
@ -1527,8 +1543,8 @@ static PyMethodDef thread_methods[] = {
METH_NOARGS, exit_doc}, METH_NOARGS, exit_doc},
{"exit", thread_PyThread_exit_thread, {"exit", thread_PyThread_exit_thread,
METH_NOARGS, exit_doc}, METH_NOARGS, exit_doc},
{"interrupt_main", thread_PyThread_interrupt_main, {"interrupt_main", (PyCFunction)thread_PyThread_interrupt_main,
METH_NOARGS, interrupt_doc}, METH_VARARGS, interrupt_doc},
{"get_ident", thread_get_ident, {"get_ident", thread_get_ident,
METH_NOARGS, get_ident_doc}, METH_NOARGS, get_ident_doc},
#ifdef PY_HAVE_THREAD_NATIVE_ID #ifdef PY_HAVE_THREAD_NATIVE_ID

View file

@ -8,6 +8,7 @@
#include "pycore_call.h" #include "pycore_call.h"
#include "pycore_ceval.h" #include "pycore_ceval.h"
#include "pycore_pyerrors.h" #include "pycore_pyerrors.h"
#include "pycore_pylifecycle.h"
#include "pycore_pystate.h" // _PyThreadState_GET() #include "pycore_pystate.h" // _PyThreadState_GET()
#ifndef MS_WINDOWS #ifndef MS_WINDOWS
@ -49,18 +50,6 @@
#define SIG_ERR ((PyOS_sighandler_t)(-1)) #define SIG_ERR ((PyOS_sighandler_t)(-1))
#endif #endif
#ifndef NSIG
# if defined(_NSIG)
# define NSIG _NSIG /* For BSD/SysV */
# elif defined(_SIGMAX)
# define NSIG (_SIGMAX + 1) /* For QNX */
# elif defined(SIGMAX)
# define NSIG (SIGMAX + 1) /* For djgpp */
# else
# define NSIG 64 /* Use a reasonable default value */
# endif
#endif
#include "clinic/signalmodule.c.h" #include "clinic/signalmodule.c.h"
/*[clinic input] /*[clinic input]
@ -106,7 +95,10 @@ class sigset_t_converter(CConverter):
static volatile struct { static volatile struct {
_Py_atomic_int tripped; _Py_atomic_int tripped;
PyObject *func; /* func is atomic to ensure that PyErr_SetInterrupt is async-signal-safe
* (even though it would probably be otherwise, anyway).
*/
_Py_atomic_address func;
} Handlers[NSIG]; } Handlers[NSIG];
#ifdef MS_WINDOWS #ifdef MS_WINDOWS
@ -143,6 +135,16 @@ static HANDLE sigint_event = NULL;
static PyObject *ItimerError; static PyObject *ItimerError;
#endif #endif
Py_LOCAL_INLINE(PyObject *)
get_handler(int i) {
return (PyObject *)_Py_atomic_load(&Handlers[i].func);
}
Py_LOCAL_INLINE(void)
SetHandler(int i, PyObject* func) {
_Py_atomic_store(&Handlers[i].func, (uintptr_t)func);
}
#ifdef HAVE_GETITIMER #ifdef HAVE_GETITIMER
/* auxiliary functions for setitimer */ /* auxiliary functions for setitimer */
static int static int
@ -516,8 +518,8 @@ signal_signal_impl(PyObject *module, int signalnum, PyObject *handler)
return NULL; return NULL;
} }
old_handler = Handlers[signalnum].func; old_handler = get_handler(signalnum);
Handlers[signalnum].func = Py_NewRef(handler); SetHandler(signalnum, Py_NewRef(handler));
if (old_handler != NULL) { if (old_handler != NULL) {
return old_handler; return old_handler;
@ -553,7 +555,7 @@ signal_getsignal_impl(PyObject *module, int signalnum)
"signal number out of range"); "signal number out of range");
return NULL; return NULL;
} }
old_handler = Handlers[signalnum].func; old_handler = get_handler(signalnum);
if (old_handler != NULL) { if (old_handler != NULL) {
return Py_NewRef(old_handler); return Py_NewRef(old_handler);
} }
@ -1584,17 +1586,21 @@ signal_module_exec(PyObject *m)
} }
// If signal_module_exec() is called more than one, we must // If signal_module_exec() is called more than one, we must
// clear the strong reference to the previous function. // clear the strong reference to the previous function.
Py_XSETREF(Handlers[signum].func, Py_NewRef(func)); PyObject* old_func = get_handler(signum);
SetHandler(signum, Py_NewRef(func));
Py_XDECREF(old_func);
} }
// Instal Python SIGINT handler which raises KeyboardInterrupt // Instal Python SIGINT handler which raises KeyboardInterrupt
if (Handlers[SIGINT].func == DefaultHandler) { PyObject* sigint_func = get_handler(SIGINT);
if (sigint_func == DefaultHandler) {
PyObject *int_handler = PyMapping_GetItemString(d, "default_int_handler"); PyObject *int_handler = PyMapping_GetItemString(d, "default_int_handler");
if (!int_handler) { if (!int_handler) {
return -1; return -1;
} }
Py_SETREF(Handlers[SIGINT].func, int_handler); SetHandler(SIGINT, int_handler);
Py_DECREF(sigint_func);
PyOS_setsig(SIGINT, signal_handler); PyOS_setsig(SIGINT, signal_handler);
} }
@ -1630,9 +1636,9 @@ _PySignal_Fini(void)
{ {
// Restore default signals and clear handlers // Restore default signals and clear handlers
for (int signum = 1; signum < NSIG; signum++) { for (int signum = 1; signum < NSIG; signum++) {
PyObject *func = Handlers[signum].func; PyObject *func = get_handler(signum);
_Py_atomic_store_relaxed(&Handlers[signum].tripped, 0); _Py_atomic_store_relaxed(&Handlers[signum].tripped, 0);
Handlers[signum].func = NULL; SetHandler(signum, NULL);
if (func != NULL if (func != NULL
&& func != Py_None && func != Py_None
&& func != DefaultHandler && func != DefaultHandler
@ -1712,7 +1718,7 @@ _PyErr_CheckSignalsTstate(PyThreadState *tstate)
* signal handler for it by the time PyErr_CheckSignals() is called * signal handler for it by the time PyErr_CheckSignals() is called
* (see bpo-43406). * (see bpo-43406).
*/ */
PyObject *func = Handlers[i].func; PyObject *func = get_handler(i);
if (func == NULL || func == Py_None || func == IgnoreHandler || if (func == NULL || func == Py_None || func == IgnoreHandler ||
func == DefaultHandler) { func == DefaultHandler) {
/* No Python signal handler due to aforementioned race condition. /* No Python signal handler due to aforementioned race condition.
@ -1761,18 +1767,27 @@ _PyErr_CheckSignals(void)
} }
/* Simulate the effect of a signal.SIGINT signal arriving. The next time /* Simulate the effect of a signal arriving. The next time PyErr_CheckSignals
PyErr_CheckSignals is called, the Python SIGINT signal handler will be is called, the corresponding Python signal handler will be raised.
raised.
Missing signal handler for the given signal number is silently ignored. */
int
PyErr_SetInterruptEx(int signum)
{
if (signum < 1 || signum >= NSIG) {
return -1;
}
PyObject* func = get_handler(signum);
if (func != IgnoreHandler && func != DefaultHandler) {
trip_signal(signum);
}
return 0;
}
Missing signal handler for the SIGINT signal is silently ignored. */
void void
PyErr_SetInterrupt(void) PyErr_SetInterrupt(void)
{ {
if ((Handlers[SIGINT].func != IgnoreHandler) && (void) PyErr_SetInterruptEx(SIGINT);
(Handlers[SIGINT].func != DefaultHandler)) {
trip_signal(SIGINT);
}
} }
static int static int

View file

@ -209,6 +209,7 @@ EXPORT_FUNC(PyErr_SetFromWindowsErrWithFilename)
EXPORT_FUNC(PyErr_SetImportError) EXPORT_FUNC(PyErr_SetImportError)
EXPORT_FUNC(PyErr_SetImportErrorSubclass) EXPORT_FUNC(PyErr_SetImportErrorSubclass)
EXPORT_FUNC(PyErr_SetInterrupt) EXPORT_FUNC(PyErr_SetInterrupt)
EXPORT_FUNC(PyErr_SetInterruptEx)
EXPORT_FUNC(PyErr_SetNone) EXPORT_FUNC(PyErr_SetNone)
EXPORT_FUNC(PyErr_SetObject) EXPORT_FUNC(PyErr_SetObject)
EXPORT_FUNC(PyErr_SetString) EXPORT_FUNC(PyErr_SetString)