From 5a5ab72fd6106bebc3f9eded2a8ac72c95450f4d Mon Sep 17 00:00:00 2001 From: user202729 <25191436+user202729@users.noreply.github.com> Date: Sat, 30 Aug 2025 10:40:36 +0700 Subject: [PATCH 1/2] Explicitly check signum in GAP error handler --- src/sage/doctest/util.py | 13 +++-------- src/sage/libs/gap/util.pyx | 48 +++++++++++++++++++++++++++++++++----- 2 files changed, 45 insertions(+), 16 deletions(-) diff --git a/src/sage/doctest/util.py b/src/sage/doctest/util.py index ddc59ddbaa2..cf0d0133d94 100644 --- a/src/sage/doctest/util.py +++ b/src/sage/doctest/util.py @@ -883,16 +883,9 @@ def ensure_interruptible_after(seconds: float, max_wait_after_interrupt: float = try: yield data - except KeyboardInterrupt as e: - # AlarmInterrupt is a subclass of KeyboardInterrupt, so this - # catches both. The "user interrupt" message is a quirk of - # GAP interrupts that result from SIGALRM. - if isinstance(e, AlarmInterrupt) or "user interrupt" in str(e): - # workaround for https://github.com/python/cpython/pull/129276 - e.__traceback__ = None - alarm_raised = True - else: - raise + except AlarmInterrupt as e: + e.__traceback__ = None # workaround for https://github.com/python/cpython/pull/129276 + alarm_raised = True finally: before_cancel_alarm_elapsed = walltime() - start_time cancel_alarm() diff --git a/src/sage/libs/gap/util.pyx b/src/sage/libs/gap/util.pyx index f23879040b0..684367a0964 100644 --- a/src/sage/libs/gap/util.pyx +++ b/src/sage/libs/gap/util.pyx @@ -17,7 +17,7 @@ from posix.dlfcn cimport dlopen, dlclose, dlerror, RTLD_LAZY, RTLD_GLOBAL from posix.signal cimport sigaction, sigaction_t, sigemptyset from cpython.exc cimport PyErr_Fetch, PyErr_Restore -from cpython.object cimport Py_LT, Py_LE, Py_EQ, Py_NE, Py_GT, Py_GE +from cpython.object cimport Py_LT, Py_LE, Py_EQ, Py_NE, Py_GT, Py_GE, Py_TYPE from cpython.ref cimport PyObject, Py_XINCREF, Py_XDECREF import os @@ -179,18 +179,24 @@ MakeReadOnlyGlobal("ERROR_OUTPUT"); MakeImmutable(libgap_errout); """ + # "Global" signal handler info structs. The GAP one we enable/disable # before/after GAP code. The Sage ones we use to store the existing # handlers before we do that. cdef sigaction_t gap_sigint_sa cdef sigaction_t sage_sigint_sa cdef sigaction_t sage_sigalrm_sa +cdef int last_signum = 0 + cdef void gap_interrupt_asap(int signum) noexcept: # A wrapper around InterruptExecStat(). This tells GAP to raise an # error at the next opportunity. + global last_signum + last_signum = signum InterruptExecStat() + cdef initialize(): """ Initialize the GAP library, if it hasn't already been @@ -303,6 +309,7 @@ cdef initialize(): f.close() gap_eval('SaveWorkspace("{0}")'.format(f.name)) + cpdef void gap_sig_on() noexcept: # Install GAP's own SIGINT handler, typically for the duration of # some libgap commands. We install it for SIGALRM too so that the @@ -313,6 +320,7 @@ cpdef void gap_sig_on() noexcept: sigaction(SIGINT, &gap_sigint_sa, &sage_sigint_sa) sigaction(SIGALRM, &gap_sigint_sa, &sage_sigalrm_sa) + cpdef void gap_sig_off() noexcept: # Restore the Sage handlers that were saved & overwritten in # gap_sig_on(). Better make sure the two are paired correctly! @@ -321,6 +329,7 @@ cpdef void gap_sig_off() noexcept: sigaction(SIGINT, &sage_sigint_sa, NULL) sigaction(SIGALRM, &sage_sigalrm_sa, NULL) + ############################################################################ ### Evaluate string in GAP ################################################# ############################################################################ @@ -434,6 +443,18 @@ cdef Obj gap_eval(str gap_string) except? NULL: ### Error handler ########################################################## ############################################################################ + +cdef object keyboard_interrupt_exc = KeyboardInterrupt(), alarm_interrupt_exc = KeyboardInterrupt("alarm interrupt") + + +try: + from cysignals.signals import AlarmInterrupt +except ImportError: + pass +else: + alarm_interrupt_exc = AlarmInterrupt() + + class GAPError(ValueError): # ValueError for historical reasons """ Exceptions raised by the GAP library @@ -478,6 +499,7 @@ cdef void error_handler() noexcept with gil: cdef PyObject* exc_type = NULL cdef PyObject* exc_val = NULL cdef PyObject* exc_tb = NULL + global last_signum try: GAP_EnterStack() @@ -503,19 +525,33 @@ cdef void error_handler() noexcept with gil: elif not msg: msg = "an unknown error occurred in GAP" + # PyErr_Fetch gives us a reference to the object, we need to free them + Py_XDECREF(exc_type) + Py_XDECREF(exc_val) + # Raise an exception using PyErr_Restore(). # This way, we can keep any existing traceback object. # Note that we manually need to deal with refcounts here. - Py_XDECREF(exc_type) - Py_XDECREF(exc_val) - if "user interrupt" in msg: - exc_type = KeyboardInterrupt + if last_signum: + if last_signum == SIGINT: + exc_type = Py_TYPE(keyboard_interrupt_exc) + exc_val = keyboard_interrupt_exc + elif last_signum == SIGALRM: + exc_type = Py_TYPE(alarm_interrupt_exc) + exc_val = alarm_interrupt_exc + else: + msg = f'{msg}\nunexpected signal value {last_signum} handled, this cannot happen' + exc_type = GAPError + exc_val = msg + last_signum = 0 else: exc_type = GAPError - exc_val = msg + exc_val = msg + Py_XINCREF(exc_type) Py_XINCREF(exc_val) PyErr_Restore(exc_type, exc_val, exc_tb) + # as explained in libgap.pyx, this is handled because GAP_Enter's declaration has "except 0" finally: # Reset ERROR_OUTPUT with a new text string stream GAP_EvalStringNoExcept(_reset_error_output_cmd) From 4010185d5578b857d93068d788534af43191226b Mon Sep 17 00:00:00 2001 From: user202729 <25191436+user202729@users.noreply.github.com> Date: Thu, 4 Sep 2025 01:09:33 +0700 Subject: [PATCH 2/2] Create new object every exception --- src/sage/libs/gap/util.pyx | 22 +++++++--------------- 1 file changed, 7 insertions(+), 15 deletions(-) diff --git a/src/sage/libs/gap/util.pyx b/src/sage/libs/gap/util.pyx index 684367a0964..295646732eb 100644 --- a/src/sage/libs/gap/util.pyx +++ b/src/sage/libs/gap/util.pyx @@ -444,17 +444,6 @@ cdef Obj gap_eval(str gap_string) except? NULL: ############################################################################ -cdef object keyboard_interrupt_exc = KeyboardInterrupt(), alarm_interrupt_exc = KeyboardInterrupt("alarm interrupt") - - -try: - from cysignals.signals import AlarmInterrupt -except ImportError: - pass -else: - alarm_interrupt_exc = AlarmInterrupt() - - class GAPError(ValueError): # ValueError for historical reasons """ Exceptions raised by the GAP library @@ -534,11 +523,14 @@ cdef void error_handler() noexcept with gil: # Note that we manually need to deal with refcounts here. if last_signum: if last_signum == SIGINT: - exc_type = Py_TYPE(keyboard_interrupt_exc) - exc_val = keyboard_interrupt_exc + exc_type = KeyboardInterrupt + exc_val_python = KeyboardInterrupt() + exc_val = exc_val_python elif last_signum == SIGALRM: - exc_type = Py_TYPE(alarm_interrupt_exc) - exc_val = alarm_interrupt_exc + from cysignals.signals import AlarmInterrupt + exc_type = AlarmInterrupt + exc_val_python = AlarmInterrupt() + exc_val = exc_val_python else: msg = f'{msg}\nunexpected signal value {last_signum} handled, this cannot happen' exc_type = GAPError