8000 bpo-43356: Allow passing a signal number to interrupt_main() by pitrou · Pull Request #24755 · python/cpython · GitHub
[go: up one dir, main page]

Skip to content

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

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 6 commits into from
Mar 11, 2021
Merged
Show file tree
Hide file tree
Changes from 4 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
70 changes: 57 additions & 13 deletions Doc/c-api/exceptions.rst
Original file line number Diff line number Diff line change
Expand Up @@ -505,29 +505,73 @@ Signal Handling
single: SIGINT
single: KeyboardInterrupt (built-in exception)

This function interacts with Python's signal handling. It checks whether a
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
signal handler written in Python. In all cases, the default effect for
:const:`SIGINT` is to raise the :exc:`KeyboardInterrupt` exception. If an
exception is raised the error indicator is set and the function returns ``-1``;
otherwise the function returns ``0``. The error indicator may or may not be
cleared if it was previously set.
This function interacts with Python's signal handling.

If the function is called from the main thread and under the main Python
interpreter, it checks whether a 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 signal handler written in Python.

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()

.. index::
module: signal
single: SIGINT
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.
This is equivalent to ``PyErr_SetInterruptEx(SIGINT)``.

.. 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
:const:`SIGINT` will be called.
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)

Expand Down
20 changes: 16 additions & 4 deletions Doc/library/_thread.rst
Original file line number Diff line number Diff line change
Expand Up @@ -61,15 +61,27 @@ This module defines the following constants and functions:
: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
thread. A thread can use this function to interrupt the main thread.
Simulate the effect of a signal arriving in 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
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()

Expand Down
12 changes: 12 additions & 0 deletions Doc/whatsnew/3.10.rst
Original file line number Diff line number Diff line change
Expand Up @@ -781,6 +781,13 @@ Add :data:`sys.stdlib_module_names`, containing the list of the standard library
module names.
(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
---------

Expand Down Expand Up @@ -1203,6 +1210,11 @@ New Features
object is an instance of :class:`set` but not an instance of a subtype.
(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
----------------------

Expand Down
16 changes: 16 additions & 0 deletions Include/internal/pycore_pylifecycle.h
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,22 @@ extern "C" {
# error "this header requires Py_BUILD_CORE define"
#endif

#ifdef HAVE_SIGNAL_H
#include <signal.h>
#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

/* Forward declarations */
struct _PyArgv;
struct pyruntimestate;
Expand Down
3 changes: 3 additions & 0 deletions Include/pyerrors.h
Original file line number Diff line number Diff line change
Expand Up @@ -224,6 +224,9 @@ PyAPI_FUNC(void) PyErr_WriteUnraisable(PyObject *);
/* In signalmodule.c */
PyAPI_FUNC(int) PyErr_CheckSignals(void);
PyAPI_FUNC(void) PyErr_SetInterrupt(void);
#if !defined(Py_LIMITED_API) || Py_LIMITED_API+0 >= 0x030A0000
PyAPI_FUNC(int) PyErr_SetInterruptEx(int signum);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would prefer to exclude it from the limited C API: define it in Include/cpython/pyerrors.h.

About the name, I dislike "Ex" suffix. What about "PyErr_RaiseSignal(signum)"?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd rather keep the name similar, since those functions are closely related. Also, PyErr_RaiseSignal sends the wrong message (since the signal is not raised).

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm, as for excluding it from the limited C API, it doesn't seem to make sense: PyErr_SetInterrupt is in it. And I don't think it places a burden.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you want to add a function to the stable ABI, you must run "make regen-limited-abi" and add it to PC/python3dll.c.

And I don't think it places a burden.

Other Python implementations must implement it, it's a maintenance burden for them. _thread.interrupt_main(signum) can be called in C.

If you don't need the function, I would even suggest to remove it (make it internal/private).

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You can't call _thread.interrupt_main from a signal handler.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Other Python implementations must implement it, it's a maintenance burden for them

It's almost zero-cost if PyErr_SetInterrupt is already implemented. PyErr_SetInterrupt is the potentially bothersome part, and it's already there.

#endif

/* Support for adding program text to SyntaxErrors */
PyAPI_FUNC(void) PyErr_SyntaxLocation(
Expand Down
43 changes: 33 additions & 10 deletions Lib/test/test_threading.py
Original file line number Diff line number Diff line change
Expand Up @@ -1489,6 +1489,29 @@ def test__all__(self):


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):
# Calling start_new_thread with a function that executes interrupt_main
# should raise KeyboardInterrupt upon completion.
Expand All @@ -1506,18 +1529,18 @@ def test_interrupt_main_mainthread(self):
with self.assertRaises(KeyboardInterrupt):
_thread.interrupt_main()

def test_interrupt_main_with_signal_handler(self):
self.check_interrupt_main_with_signal_handler(signal.SIGINT)
self.check_interrupt_main_with_signal_handler(signal.SIGTERM)

def test_interrupt_main_noerror(self):
handler = signal.getsignal(signal.SIGINT)
try:
# No exception should arise.
signal.signal(signal.SIGINT, signal.SIG_IGN)
_thread.interrupt_main()
self.check_interrupt_main_noerror(signal.SIGINT)
self.check_interrupt_main_noerror(signal.SIGTERM)

signal.signal(signal.SIGINT, signal.SIG_DFL)
_thread.interrupt_main()
finally:
# 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):
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Allow passing a signal number to ``_thread.interrupt_main()``.
30 changes: 23 additions & 7 deletions Modules/_threadmodule.c
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,10 @@
#include <stddef.h> // offsetof()
#include "structmember.h" // PyMemberDef

#ifdef HAVE_SIGNAL_H
# include <signal.h> // SIGINT
#endif

// ThreadError is just an alias to PyExc_RuntimeError
#define ThreadError PyExc_RuntimeError

Expand Down Expand Up @@ -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.");

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;
}

PyDoc_STRVAR(interrupt_doc,
"interrupt_main()\n\
"interrupt_main(signum=signal.SIGINT, /)\n\
\n\
Simulate the arrival of the given signal in the main thread,\n\
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\
Raise a KeyboardInterrupt in the main thread.\n\
A subthread can use this function to interrupt the main thread."
Note: the default signal hander for SIGINT raises ``KeyboardInterrupt``."
);

static lockobject *newlockobject(PyObject *module);
Expand Down Expand Up @@ -1527,8 +1543,8 @@ static PyMethodDef thread_methods[] = {
METH_NOARGS, exit_doc},
{"exit", thread_PyThread_exit_thread,
METH_NOARGS, exit_doc},
{"interrupt_main", thread_PyThread_interrupt_main,
METH_NOARGS, interrupt_doc},
{"interrupt_main", (PyCFunction)thread_PyThread_interrupt_main,
METH_VARARGS, interrupt_doc},
{"get_ident", thread_get_ident,
METH_NOARGS, get_ident_doc},
#ifdef PY_HAVE_THREAD_NATIVE_ID
Expand Down
Loading
0