8000 bpo-43356: Allow passing a signal number to interrupt_main() (GH-24755) · python/cpython@ba251c2 · GitHub
[go: up one dir, main page]

Skip to content

Commit ba251c2

Browse files
authored
bpo-43356: Allow passing a signal number to interrupt_main() (GH-24755)
Also introduce a new C API ``PyErr_SetInterruptEx(int signum)``.
1 parent b4fc44b commit ba251c2

File tree

11 files changed

+209
-64
lines changed

11 files changed

+209
-64
lines changed

Doc/c-api/exceptions.rst

Lines changed: 57 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -505,29 +505,73 @@ Signal Handling
505505
single: SIGINT
506506
single: KeyboardInterrupt (built-in exception)
507507
508-
This function interacts with Python's signal handling. It checks whether a
509-
signal has been sent to the processes and if so, invokes the corresponding
510-
signal handler. If the :mod:`signal` module is supported, this can invoke a
511-
signal handler written in Python. In all cases, the default effect for
512-
:const:`SIGINT` is to raise the :exc:`KeyboardInterrupt` exception. If an
513-
exception is raised the error indicator is set and the function returns ``-1``;
514-
otherwise the function returns ``0``. The error indicator may or may not be
515-
cleared if it was previously set.
508+
This function interacts with Python's signal handling.
509+
510+
If the function is called from the main thread and under the main Python
511+
interpreter, it checks whether a signal has been sent to the processes
512+
and if so, invokes the corresponding signal handler. If the :mod:`signal`
513+
module is supported, this can invoke a signal handler written in Python.
514+
515+
The function attemps to handle all pending signals, and then returns ``0``.
516+
However, if a Python signal handler raises an exception, the error
517+
indicator is set and the function returns ``-1`` immediately (such that
518+
other pending signals may not have been handled yet: they will be on the
519+
next :c:func:`PyErr_CheckSignals()` invocation).
520+
521+
If the function is called from a non-main thread, or under a non-main
522+
Python interpreter, it does nothing and returns ``0``.
523+
524+
This function can be called by long-running C code that wants to
525+
be interruptible by user requests (such as by pressing Ctrl-C).
526+
527+
.. note::
528+
The default Python signal handler for :const:`SIGINT` raises the
529+
:exc:`KeyboardInterrupt` exception.
516530
517531
518532
.. c:function:: void PyErr_SetInterrupt()
519533
520534
.. index::
535+
module: signal
521536
single: SIGINT
522537
single: KeyboardInterrupt (built-in exception)
523538
524-
Simulate the effect of a :const:`SIGINT` signal arriving. The next time
539+
Simulate the effect of a :const:`SIGINT` signal arriving.
540+
This is equivalent to ``PyErr_SetInterruptEx(SIGINT)``.
541+
542+
.. note::
543+
This function is async-signal-safe. It can be called without
544+
the :term:`GIL` and from a C signal handler.
545+
546+
547+
.. c:function:: int PyErr_SetInterruptEx(int signum)
548+
549+
.. index::
550+
module: signal
551+
single: KeyboardInterrupt (built-in exception)
552+
553+
Simulate the effect of a signal arriving. The next time
525554
:c:func:`PyErr_CheckSignals` is called, the Python signal handler for
526-
:const:`SIGINT` will be called.
555+
the given signal number will be called.
556+
557+
This function can be called by C code that sets up its own signal handling
558+
and wants Python signal handlers to be invoked as expected when an
559+
interruption is requested (for example when the user presses Ctrl-C
560+
to interrupt an operation).
561+
562+
If the given signal isn't handled by Python (it was set to
563+
:data:`signal.SIG_DFL` or :data:`signal.SIG_IGN`), it will be ignored.
564+
565+
If *signum* is outside of the allowed range of signal numbers, ``-1``
566+
is returned. Otherwise, ``0`` is returned. The error indicator is
567+
never changed by this function.
568+
569+
.. note::
570+
This function is async-signal-safe. It can be called without
571+
the :term:`GIL` and from a C signal handler.
572+
573+
.. versionadded:: 3.10
527574
528-
If :const:`SIGINT` isn't handled by Python (it was set to
529-
:data:`signal.SIG_DFL` or :data:`signal.SIG_IGN`), this function does
530-
nothing.
531575
532576
.. c:function:: int PySignal_SetWakeupFd(int fd)
533577

Doc/data/stable_abi.dat

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ PyCFunction_Call
3636
PyCFunction_GetFlags
3737
PyCFunction_GetFunction
3838
PyCFunction_GetSelf
39+
PyCFunction_New
3940
PyCFunction_NewEx
4041
PyCFunction_Type
4142
PyCMethod_New
@@ -144,6 +145,7 @@ PyErr_SetFromErrnoWithFilenameObjects
144145
PyErr_SetImportError
145146
PyErr_SetImportErrorSubclass
146147
PyErr_SetInterrupt
148+
PyErr_SetInterruptEx
147149
PyErr_SetNone
148150
PyErr_SetObject
149151
PyErr_SetString

Doc/library/_thread.rst

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ - F438 61,15 +61,27 @@ This module defines the following constants and functions:
6161
:func:`sys.unraisablehook` is now used to handle unhandled exceptions.
6262

6363

64-
.. function:: interrupt_main()
64+
.. function:: interrupt_main(signum=signal.SIGINT, /)
6565

66-
Simulate the effect of a :data:`signal.SIGINT` signal arriving in the main
67-
thread. A thread can use this function to interrupt the main thread.
66+
Simulate the effect of a signal arriving in the main thread.
67+
A thread can use this function to interrupt the main thread, though
68+
there is no guarantee that the interruption will happen immediately.
6869

69-
If :data:`signal.SIGINT` isn't handled by Python (it was set to
70+
If given, *signum* is the number of the signal to simulate.
71+
If *signum* is not given, :data:`signal.SIGINT` is simulated.
72+
73+
If the given signal isn't handled by Python (it was set to
7074
:data:`signal.SIG_DFL` or :data:`signal.SIG_IGN`), this function does
7175
nothing.
7276

77+
.. versionchanged:: 3.10
78+
The *signum* argument is added to customize the signal number.
79+
80+
.. note::
81+
This does not emit the corresponding signal but schedules a call to
82+
the associated handler (if it exists).
83+
If you want to truly emit the signal, use :func:`signal.raise_signal`.
84+
7385

7486
.. function:: exit()
7587

Doc/whatsnew/3.10.rst

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -788,6 +788,13 @@ Add :data:`sys.stdlib_module_names`, containing the list of the standard library
788788
module names.
789789
(Contributed by Victor Stinner in :issue:`42955`.)
790790
791+
_thread
792+
-------
793+
794+
:func:`_thread.interrupt_main` now takes an optional signal number to
795+
simulate (the default is still :data:`signal.SIGINT`).
796+
(Contributed by Antoine Pitrou in :issue:`43356`.)
797+
791798
threading
792799
---------
793800
@@ -1210,6 +1217,11 @@ New Features
12101217
object is an instance of :class:`set` but not an instance of a subtype.
12111218
(Contributed by Pablo Galindo in :issue:`43277`.)
12121219
1220+
* Added :c:func:`PyErr_SetInterruptEx` which allows passing a signal number
1221+
to simulate.
1222+
(Contributed by Antoine Pitrou in :issue:`43356`.)
1223+
1224+
12131225
Porting to Python 3.10
12141226
----------------------
12151227

Include/internal/pycore_pylifecycle.h

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,24 @@ extern "C" {
88
# error "this header requires Py_BUILD_CORE define"
99
#endif
1010

11+
#ifdef HAVE_SIGNAL_H
12+
#include <signal.h>
13+
#endif
14+
1115
#include "pycore_runtime.h" // _PyRuntimeState
1216

17+
#ifndef NSIG
18+
# if defined(_NSIG)
19+
# define NSIG _NSIG /* For BSD/SysV */
20+
# elif defined(_SIGMAX)
21+
# define NSIG (_SIGMAX + 1) /* For QNX */
22+
# elif defined(SIGMAX)
23+
# define NSIG (SIGMAX + 1) /* For djgpp */
24+
# else
25+
# define NSIG 64 /* Use a reasonable default value */
26+
# endif
27+
#endif
28+
1329
/* Forward declarations */
1430
struct _PyArgv;
1531
struct pyruntimestate;

Include/pyerrors.h

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -224,6 +224,9 @@ PyAPI_FUNC(void) PyErr_WriteUnraisable(PyObject *);
224224
/* In signalmodule.c */
225225
PyAPI_FUNC(int) PyErr_CheckSignals(void);
226226
PyAPI_FUNC(void) PyErr_SetInterrupt(void);
227+
#if !defined(Py_LIMITED_API) || Py_LIMITED_API+0 >= 0x030A0000
228+
PyAPI_FUNC(int) PyErr_SetInterruptEx(int signum);
229+
#endif
227230

228231
/* Support for adding program text to SyntaxErrors */
229232
PyAPI_FUNC(void) PyErr_SyntaxLocation(

Lib/test/test_threading.py

Lines changed: 33 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1489,6 +1489,29 @@ def test__all__(self):
14891489

14901490

14911491
class InterruptMainTests(unittest.TestCase):
1492+
def check_interrupt_main_with_signal_handler(self, signum):
1493+
def handler(signum, frame):
1494+
1/0
1495+
1496+
old_handler = signal.signal(signum, handler)
1497+
self.addCleanup(signal.signal, signum, old_handler)
1498+
1499+
with self.assertRaises(ZeroDivisionError):
1500+
_thread.interrupt_main()
1501+
1502+
def check_interrupt_main_noerror(self, signum):
1503+
handler = signal.getsignal(signum)
1504+
try:
1505+
# No exception should arise.
1506+
signal.signal(signum, signal.SIG_IGN)
1507+
_thread.interrupt_main(signum)
1508+
1509+
signal.signal(signum, signal.SIG_DFL)
1510+
_thread.interrupt_main(signum)
1511+
finally:
1512+
# Restore original handler
1513+
signal.signal(signum, handler)
1514+
14921515
def test_interrupt_main_subthread(self):
14931516
# Calling start_new_thread with a function that executes interrupt_main
14941517
# should raise KeyboardInterrupt upon completion.
@@ -1506,18 +1529,18 @@ def test_interrupt_main_mainthread(self):
15061529
with self.assertRaises(KeyboardInterrupt):
15071530
_thread.interrupt_main()
15081531

1532+
def test_interrupt_main_with_signal_handler(self):
1533+
self.check_interrupt_main_with_signal_handler(signal.SIGINT)
1534+
self.check_interrupt_main_with_signal_handler(signal.SIGTERM)
1535+
15091536
def test_interrupt_main_noerror(self):
1510-
handler = signal.getsignal(signal.SIGINT)
1511-
try:
1512-
# No exception should arise.
1513-
signal.signal(signal.SIGINT, signal.SIG_IGN)
1514-
_thread.interrupt_main()
1537+
self.check_interrupt_main_noerror(signal.SIGINT)
1538+
self.check_interrupt_main_noerror(signal.SIGTERM)
15151539

1516-
signal.signal(signal.SIGINT, signal.SIG_DFL)
1517-
_thread.interrupt_main()
1518-
finally:
1519-
# Restore original handler
1520-
signal.signal(signal.SIGINT, handler)
1540+
def test_interrupt_main_invalid_signal(self):
1541+
self.assertRaises(ValueError, _thread.interrupt_main, -1)
1542+
self.assertRaises(ValueError, _thread.interrupt_main, signal.NSIG)
1543+
self.assertRaises(ValueError, _thread.interrupt_main, 1000000)
15211544

15221545

15231546
class AtexitTests(unittest.TestCase):
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Allow passing a signal number to ``_thread.interrupt_main()``.

Modules/_threadmodule.c

Lines changed: 23 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,10 @@
99
#include <stddef.h> // offsetof()
1010
#include "structmember.h" // PyMemberDef
1111

12+
#ifdef HAVE_SIGNAL_H
13+
# include <signal.h> // SIGINT
14+
#endif
15+
1216
// ThreadError is just an alias to PyExc_RuntimeError
1317
#define ThreadError PyExc_RuntimeError
1418

@@ -1173,17 +1177,29 @@ This is synonymous to ``raise SystemExit''. It will cause the current\n\
11731177
thread to exit silently unless the exception is caught.");
11741178

11751179
static PyObject *
1176-
thread_PyThread_interrupt_main(PyObject * self, PyObject *Py_UNUSED(ignored))
1180+
thread_PyThread_interrupt_main(PyObject *self, PyObject *args)
11771181
{
1178-
PyErr_SetInterrupt();
1182+
int signum = SIGINT;
1183+
if (!PyArg_ParseTuple(args, "|i:signum", &signum)) {
1184+
return NULL;
1185+
}
1186+
1187+
if (PyErr_SetInterruptEx(signum)) {
1188+
PyErr_SetString(PyExc_ValueError, "signal number out of range");
1189+
return NULL;
1190+
}
11791191
Py_RETURN_NONE;
11801192
}
11811193

11821194
PyDoc_STRVAR(interrupt_doc,
1183-
"interrupt_main()\n\
1195+
"interrupt_main(signum=signal.SIGINT, /)\n\
1196+
\n\
1197+
Simulate the arrival of the given signal in the main thread,\n\
1198+
where the corresponding signal handler will be executed.\n\
1199+
If *signum* is omitted, SIGINT is assumed.\n\
1200+
A subthread can use this function to interrupt the main thread.\n\
11841201
\n\
1185-
Raise a KeyboardInterrupt in the main thread.\n\
1186-
A subthread can use this function to interrupt the main thread."
1202+
Note: the default signal hander for SIGINT raises ``KeyboardInterrupt``."
11871203
);
11881204

11891205
static lockobject *newlockobject(PyObject *module);
@@ -1527,8 +1543,8 @@ static PyMethodDef thread_methods[] = {
15271543
METH_NOARGS, exit_doc},
15281544
{"exit", thread_PyThread_exit_thread,
15291545
METH_NOARGS, exit_doc},
1530-
{"interrupt_main", thread_PyThread_interrupt_main,
1531-
METH_NOARGS, interrupt_doc},
1546+
{"interrupt_main", (PyCFunction)thread_PyThread_interrupt_main,
1547+
METH_VARARGS, interrupt_doc},
15321548
{"get_ident", thread_get_ident,
15331549
METH_NOARGS, get_ident_doc},
15341550
#ifdef PY_HAVE_THREAD_NATIVE_ID

0 commit comments

Comments
 (0)
0