From fed466d218a6944e8f8b97cb3b88328ebb7471b2 Mon Sep 17 00:00:00 2001 From: Jonathan Kliem Date: Thu, 21 Sep 2023 10:15:50 -0700 Subject: [PATCH] Add general signal hook --- README.rst | 1 + docs/source/interrupt.rst | 55 ++++++++++++++++++++++++++++++++++ src/cysignals/implementation.c | 37 ++++++++++++++++++++++- src/cysignals/signals.pxd.in | 4 +++ src/cysignals/signals.pyx | 31 +++++++++++++++++++ 5 files changed, 127 insertions(+), 1 deletion(-) diff --git a/README.rst b/README.rst index d623d948..fdd68975 100644 --- a/README.rst +++ b/README.rst @@ -47,6 +47,7 @@ Changelog ^^^^^^^^^^^^^^^^^^^^^^^^^ * Replace `fprintf` by calls to `write`, which is async-signal-safe according to POSIX. [#162] +* Introduce a general hook to interface with custom signal handling 1.11.2 (2021-12-15) diff --git a/docs/source/interrupt.rst b/docs/source/interrupt.rst index ebcfde20..6a1e8e06 100644 --- a/docs/source/interrupt.rst +++ b/docs/source/interrupt.rst @@ -249,3 +249,58 @@ can conditionally call ``sig_on()`` and ``sig_off()``:: This should only be needed if both the check (``n > 100`` in the example) and the code inside the ``sig_on()`` block take very little time. + +.. _section_add_custom_signals: + +Using custom blocking and signal handlers +----------------------------------------- + +The following illustrates how signals can be held back similar to +``sig_block`` and ``sig_unblock``. The number theory libary PARI/GP +defines a variable, which indicates that the execution should not +currently be interrupted. Another variable is used to indicate a pending signal, +so that PARI/GP can treat it. + +Other external libraries might use a similar scheme. +Here we indicate this might work:: + + from cysignals.signals cimport sig_on, sig_off, add_custom_signals + + cdef extern from "stdio.h": + void sleep(int) + + cdef int SIGINT_block = 0 + cdef int SIGINT_pending = 0 + + cdef int signal_is_blocked(): + return SIGINT_block + + cdef void signal_unblock(): + global SIGINT_block + SIGINT_block = 0 + + cdef void set_pending_signal(int sig): + global SIGINT_pending + SIGINT_pending = sig + + # Use the hook provided by cysignals. + add_custom_signals(&signal_is_blocked, &signal_unblock, &set_pending_signal) + + def foo(size_t b, int blocked): + global SIGINT_block, SIGINT_pending + sig_on() + SIGINT_block = blocked + for i in range(b): + sleep(1) + if SIGINT_pending: + SIGINT_block = 0 + SIGINT_pending = 0 + raise KeyboardInterrupt("interrupt was held back") + SIGINT_block = 0 + sig_off() + return + +In the above scenario ``foo(10, 0)`` would just wait for 10 seconds, +while allowing interrupts. ``foo(10, 1)`` blocks the interrupt +until the end of the second. The pending signal is then treated +with a custom message. diff --git a/src/cysignals/implementation.c b/src/cysignals/implementation.c index a3585772..54e1c9fa 100644 --- a/src/cysignals/implementation.c +++ b/src/cysignals/implementation.c @@ -62,6 +62,37 @@ static int PARI_SIGINT_block = 0; static int PARI_SIGINT_pending = 0; #define paricfg_version NULL #endif + +// Custom signal handling of other packages. +#define MAX_N_CUSTOM_HANDLERS 16 + +static int (*custom_signal_is_blocked_pts[MAX_N_CUSTOM_HANDLERS])(); +static void (*custom_signal_unblock_pts[MAX_N_CUSTOM_HANDLERS])(); +static void (*custom_set_pending_signal_pts[MAX_N_CUSTOM_HANDLERS])(int); +static int n_custom_handlers = 0; + +int custom_signal_is_blocked(){ + // Check if a custom block is set. + for(int i = 0; i < n_custom_handlers; i++){ + if (custom_signal_is_blocked_pts[i]()) + return 1; + } + return 0; +} + +void custom_signal_unblock(){ + // Unset all custom blocks. + for(int i = 0; i < n_custom_handlers; i++) + custom_signal_unblock_pts[i](); +} + + +void custom_set_pending_signal(int sig){ + // Set a pending signal to custom handlers. + for(int i = 0; i < n_custom_handlers; i++) + custom_set_pending_signal_pts[i](sig); +} + #if HAVE_WINDOWS_H /* We must include after * See https://github.com/sagemath/cysignals/issues/107 */ @@ -214,7 +245,7 @@ static void cysigs_interrupt_handler(int sig) if (cysigs.sig_on_count > 0) { - if (!cysigs.block_sigint && !PARI_SIGINT_block) + if (!cysigs.block_sigint && !PARI_SIGINT_block && !custom_signal_is_blocked()) { /* Raise an exception so Python can see it */ do_raise_exception(sig); @@ -238,6 +269,7 @@ static void cysigs_interrupt_handler(int sig) { cysigs.interrupt_received = sig; PARI_SIGINT_pending = sig; + custom_set_pending_signal(sig); } } @@ -400,6 +432,7 @@ static void _sig_on_interrupt_received(void) cysigs.sig_on_count = 0; cysigs.interrupt_received = 0; PARI_SIGINT_pending = 0; + custom_signal_unblock(); #if HAVE_SIGPROCMASK sigprocmask(SIG_SETMASK, &oldset, NULL); @@ -412,9 +445,11 @@ static void _sig_on_recover(void) { cysigs.block_sigint = 0; PARI_SIGINT_block = 0; + custom_signal_unblock(); cysigs.sig_on_count = 0; cysigs.interrupt_received = 0; PARI_SIGINT_pending = 0; + custom_set_pending_signal(0); #if HAVE_SIGPROCMASK /* Reset signal mask */ diff --git a/src/cysignals/signals.pxd.in b/src/cysignals/signals.pxd.in index a98c8d1e..3a536907 100644 --- a/src/cysignals/signals.pxd.in +++ b/src/cysignals/signals.pxd.in @@ -49,6 +49,10 @@ cdef extern from "macros.h" nogil: int sig_str_no_except "sig_str"(const char*) int sig_check_no_except "sig_check"() +# This function adds custom block/unblock/pending. +cdef int add_custom_signals(int (*custom_signal_is_blocked)(), + void (*custom_signal_unblock)(), + void (*custom_set_pending_signal)(int)) except -1 # This function does nothing, but it is declared cdef except *, so it # can be used to make Cython check whether there is a pending exception diff --git a/src/cysignals/signals.pyx b/src/cysignals/signals.pyx index 74d932b6..04cf60e5 100644 --- a/src/cysignals/signals.pyx +++ b/src/cysignals/signals.pyx @@ -54,6 +54,12 @@ cdef extern from "implementation.c": # PARI version string; NULL if compiled without PARI support const char* paricfg_version + int (**custom_signal_is_blocked_pts)() + void (**custom_signal_unblock_pts)() + void (**custom_set_pending_signal_pts)(int) + int n_custom_handlers + int MAX_N_CUSTOM_HANDLERS + def _pari_version(): """ @@ -74,6 +80,31 @@ def _pari_version(): return v.decode('ascii') +cdef int add_custom_signals(int (*custom_signal_is_blocked)(), + void (*custom_signal_unblock)(), + void (*custom_set_pending_signal)(int)) except -1: + """ + Add an external block/unblock/pending to cysignals. + + INPUT: + + - ``custom_signal_is_blocked`` -- returns whether signals are currently blocked. + + - ``custom_signal_unblock`` -- unblocks signals + + - ``custom_set_pending_signal`` -- set a pending signal in case of blocking + """ + global n_custom_handlers + if n_custom_handlers == MAX_N_CUSTOM_HANDLERS: + raise IndexError("maximal number of custom handlers exceeded") + + custom_signal_is_blocked_pts[n_custom_handlers] = custom_signal_is_blocked + custom_signal_unblock_pts[n_custom_handlers] = custom_signal_unblock + custom_set_pending_signal_pts[n_custom_handlers] = custom_set_pending_signal + + n_custom_handlers += 1 + + class AlarmInterrupt(KeyboardInterrupt): """ Exception class for :func:`alarm` timeouts.