diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index fa0b965..3cc1413 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,4 +1,4 @@ -name: Run Sage CI for Linux/Cygwin/macOS +name: CI ## This GitHub Actions workflow provides: ## @@ -51,7 +51,7 @@ concurrency: cancel-in-progress: true jobs: - cygwin-without-sage: + cygwin: runs-on: windows-latest strategy: fail-fast: false @@ -88,7 +88,7 @@ jobs: strategy: fail-fast: false matrix: - os: ['macos-13', 'macos-latest', 'ubuntu-latest'] + os: ['macos-13', 'macos-latest', 'ubuntu-latest', 'windows-latest'] python-version: ['3.10', '3.11', '3.12', '3.13-dev'] steps: - name: Set up the repository diff --git a/meson.build b/meson.build index 7784fce..d66145b 100644 --- a/meson.build +++ b/meson.build @@ -14,6 +14,7 @@ cxx = meson.get_compiler('cpp') is_windows = host_machine.system() == 'windows' is_cygwin = host_machine.system() == 'cygwin' is_msvc = cc.get_id() == 'msvc' +is_mingw = cc.get_id()=='gcc' and host_machine.system()=='windows' # Set preprocessor macros # Disable .c line numbers in exception tracebacks @@ -45,7 +46,7 @@ config.set('HAVE_TIME_H', cc.has_header('time.h') ? 1 : 0) config.set('HAVE_SYS_WAIT_H', cc.has_header('sys/wait.h') ? 1 : 0) config.set('HAVE_WINDOWS_H', cc.has_header('windows.h') ? 1 : 0) -config.set('HAVE_FORK', cc.has_function('fork') ? 1 : 0) +config.set('HAVE_FORK', (cc.has_function('fork') and not is_mingw) ? 1 : 0) config.set('HAVE_KILL', cc.has_function('kill') ? 1 : 0) config.set('HAVE_SIGPROCMASK', cc.has_function('sigprocmask') ? 1 : 0) config.set('HAVE_SIGALTSTACK', cc.has_function('sigaltstack') ? 1 : 0) diff --git a/src/conftest.py b/src/conftest.py index 693071c..9ffb1be 100755 --- a/src/conftest.py +++ b/src/conftest.py @@ -1,8 +1,17 @@ import pathlib +import platform from _pytest.nodes import Collector from _pytest.doctest import DoctestModule +if platform.system() == "Windows": + collect_ignore = [ + "cysignals/alarm.pyx", + "cysignals/pselect.pyx", + "cysignals/pysignals.pyx", + "cysignals/tests.pyx", + ] + def pytest_collect_file( file_path: pathlib.Path, diff --git a/src/cysignals/cysignals_config.h.in b/src/cysignals/cysignals_config.h.in deleted file mode 100644 index e7143de..0000000 --- a/src/cysignals/cysignals_config.h.in +++ /dev/null @@ -1,43 +0,0 @@ -/* - * Should cysignals be compiled with support for verbose debugging - * output? The debug level itself needs to be set using the C function - * _set_debug_level() or the Python function set_debug_level(). - * Enabling this will make the code slower, even if the debug level is - * set to 0. - */ -#ifndef ENABLE_DEBUG_CYSIGNALS -#undef ENABLE_DEBUG_CYSIGNALS -#endif - -/* - * Should sig_on() use sigsetjmp(env, 0) instead of setjmp(env)? This - * is needed on BSD and OS X because setjmp() saves the signal mask. - * With glibc, we also use sigsetjmp because it's faster. - */ -#ifndef CYSIGNALS_USE_SIGSETJMP -#undef CYSIGNALS_USE_SIGSETJMP -#endif - - -/* An implementation of atomic variables might not be allowed with OpenMP */ -#undef CYSIGNALS_C_ATOMIC_WITH_OPENMP -#undef CYSIGNALS_CXX_ATOMIC_WITH_OPENMP -#undef CYSIGNALS_STD_ATOMIC_WITH_OPENMP - -/* Which implementation of atomic variables (if any) to use? */ -#ifndef __cplusplus - -#if defined(CYSIGNALS_C_ATOMIC_WITH_OPENMP) || !defined(_OPENMP) -#undef CYSIGNALS_C_ATOMIC -#endif - -#else - -#if defined(CYSIGNALS_CXX_ATOMIC_WITH_OPENMP) || !defined(_OPENMP) -#undef CYSIGNALS_CXX_ATOMIC -#endif -#if defined(CYSIGNALS_STD_ATOMIC_WITH_OPENMP) || !defined(_OPENMP) -#undef CYSIGNALS_STD_ATOMIC -#endif - -#endif diff --git a/src/cysignals/implementation.c b/src/cysignals/implementation.c index b940b35..20a83d0 100644 --- a/src/cysignals/implementation.c +++ b/src/cysignals/implementation.c @@ -34,7 +34,6 @@ Interrupt and signal handling for Cython #include #include #include -#include #if HAVE_SYS_TYPES_H #include #endif @@ -88,6 +87,9 @@ void custom_set_pending_signal(int sig){ #if HAVE_WINDOWS_H #include #endif +#if !_WIN32 +#include +#endif #include "struct_signals.h" @@ -108,9 +110,11 @@ static sigset_t default_sigmask; static sigset_t sigmask_with_sigint; #endif +#if !_WIN32 /* A trampoline to jump to after handling a signal. */ static cyjmp_buf trampoline_setup; static sigjmp_buf trampoline; +#endif static void setup_cysignals_handlers(void); static void cysigs_interrupt_handler(int sig); @@ -194,15 +198,23 @@ static inline void print_stderr_ptr(void *ptr) /* Reset all signal handlers and the signal mask to their defaults. */ static inline void sig_reset_defaults(void) { +#ifdef SIGHUP signal(SIGHUP, SIG_DFL); +#endif signal(SIGINT, SIG_DFL); +#ifdef SIGQUIT signal(SIGQUIT, SIG_DFL); +#endif signal(SIGILL, SIG_DFL); signal(SIGABRT, SIG_DFL); signal(SIGFPE, SIG_DFL); +#ifdef SIGBUS signal(SIGBUS, SIG_DFL); +#endif signal(SIGSEGV, SIG_DFL); +#ifdef SIGALRM signal(SIGALRM, SIG_DFL); +#endif signal(SIGTERM, SIG_DFL); #if HAVE_SIGPROCMASK sigprocmask(SIG_SETMASK, &default_sigmask, NULL); @@ -228,10 +240,14 @@ static inline void sigdie_for_sig(int sig, int inside) sigdie(sig, "Unhandled SIGFPE during signal handling."); else if (sig == SIGSEGV) sigdie(sig, "Unhandled SIGSEGV during signal handling."); + #ifdef SIGBUS else if (sig == SIGBUS) sigdie(sig, "Unhandled SIGBUS during signal handling."); + #endif + #ifdef SIGQUIT else if (sig == SIGQUIT) sigdie(sig, NULL); + #endif else sigdie(sig, "Unknown signal during signal handling."); } @@ -244,10 +260,14 @@ static inline void sigdie_for_sig(int sig, int inside) sigdie(sig, "Unhandled SIGFPE: An unhandled floating point exception occurred."); else if (sig == SIGSEGV) sigdie(sig, "Unhandled SIGSEGV: A segmentation fault occurred."); + #ifdef SIGBUS else if (sig == SIGBUS) sigdie(sig, "Unhandled SIGBUS: A bus error occurred."); + #endif + #ifdef SIGQUIT else if (sig == SIGQUIT) sigdie(sig, NULL); + #endif else sigdie(sig, "Unknown signal received."); } @@ -330,8 +350,22 @@ static void cygwin_setup_alt_stack() { #endif /* CYGWIN && __x86_64__ */ +void get_monotonic_time(struct timespec *ts) { +#ifdef _WIN32 + LARGE_INTEGER frequency; + LARGE_INTEGER counter; + + QueryPerformanceFrequency(&frequency); + QueryPerformanceCounter(&counter); + + ts->tv_sec = counter.QuadPart / frequency.QuadPart; + ts->tv_nsec = (counter.QuadPart % frequency.QuadPart) * 1e9 / frequency.QuadPart; +#else + clock_gettime(CLOCK_MONOTONIC, ts); +#endif +} -/* Handler for SIGHUP, SIGINT, SIGALRM +/* Handler for SIGHUP, SIGINT, SIGALRM, SIGTERM * * Inside sig_on() (i.e. when cysigs.sig_on_count is positive), this * raises an exception and jumps back to sig_on(). @@ -350,7 +384,7 @@ static void cysigs_interrupt_handler(int sig) if (cysigs.debug_level >= 3) print_backtrace(); /* Store time of this signal, unless there is already a * pending signal. */ - if (!cysigs.interrupt_received) clock_gettime(CLOCK_MONOTONIC, &sigtime); + if (!cysigs.interrupt_received) get_monotonic_time(&sigtime); } #endif @@ -361,8 +395,10 @@ static void cysigs_interrupt_handler(int sig) /* Raise an exception so Python can see it */ do_raise_exception(sig); +#if !_WIN32 /* Jump back to sig_on() (the first one if there is a stack) */ siglongjmp(trampoline, sig); +#endif } } else @@ -376,7 +412,11 @@ static void cysigs_interrupt_handler(int sig) /* If we are here, we cannot handle the interrupt immediately, so * we store the signal number for later use. But make sure we * don't overwrite a SIGHUP or SIGTERM which we already received. */ - if (cysigs.interrupt_received != SIGHUP && cysigs.interrupt_received != SIGTERM) + if ( +#ifdef SIGHUP + cysigs.interrupt_received != SIGHUP && +#endif + cysigs.interrupt_received != SIGTERM) { cysigs.interrupt_received = sig; custom_set_pending_signal(sig); @@ -393,8 +433,11 @@ static void cysigs_signal_handler(int sig) int inside = cysigs.inside_signal_handler; cysigs.inside_signal_handler = 1; - if (inside == 0 && cysigs.sig_on_count > 0 && sig != SIGQUIT) - { + if (inside == 0 && cysigs.sig_on_count > 0 + #ifdef SIGQUIT + && sig != SIGQUIT + #endif + ) { /* We are inside sig_on(), so we can handle the signal! */ #if ENABLE_DEBUG_CYSIGNALS if (cysigs.debug_level >= 1) { @@ -402,15 +445,16 @@ static void cysigs_signal_handler(int sig) print_stderr_long(sig); print_stderr(" *** inside sig_on\n"); if (cysigs.debug_level >= 3) print_backtrace(); - clock_gettime(CLOCK_MONOTONIC, &sigtime); + get_monotonic_time(&sigtime); } #endif /* Raise an exception so Python can see it */ do_raise_exception(sig); - + #if !_WIN32 /* Jump back to sig_on() (the first one if there is a stack) */ siglongjmp(trampoline, sig); + #endif } else { @@ -422,7 +466,7 @@ static void cysigs_signal_handler(int sig) } } - +#if !_WIN32 /* A trampoline to jump to after handling a signal. * * The jump to sig_on() uses cylongjmp(), which does not restore the @@ -506,6 +550,7 @@ static void setup_trampoline(void) cylongjmp(trampoline_setup, 1); } } +#endif /* This calls sig_raise_exception() to actually raise the exception. */ @@ -514,7 +559,7 @@ static void do_raise_exception(int sig) #if ENABLE_DEBUG_CYSIGNALS struct timespec raisetime; if (cysigs.debug_level >= 2) { - clock_gettime(CLOCK_MONOTONIC, &raisetime); + get_monotonic_time(&raisetime); long delta_ms = (raisetime.tv_sec - sigtime.tv_sec)*1000L + (raisetime.tv_nsec - sigtime.tv_nsec)/1000000L; PyGILState_STATE gilstate = PyGILState_Ensure(); print_stderr("do_raise_exception(sig="); @@ -608,6 +653,11 @@ static void setup_alt_stack(void) static void setup_cysignals_handlers(void) { +#ifdef _WIN32 + signal(SIGINT, cysigs_interrupt_handler); + signal(SIGTERM, cysigs_interrupt_handler); + signal(SIGABRT, cysigs_signal_handler); +#else struct sigaction sa; memset(&sa, 0, sizeof(sa)); @@ -635,21 +685,30 @@ static void setup_cysignals_handlers(void) /* Handlers for interrupt-like signals */ sa.sa_handler = cysigs_interrupt_handler; sa.sa_flags = 0; +#ifdef SIGHUP if (sigaction(SIGHUP, &sa, NULL)) {perror("cysignals sigaction"); exit(1);} +#endif if (sigaction(SIGINT, &sa, NULL)) {perror("cysignals sigaction"); exit(1);} +#ifdef SIGALRM if (sigaction(SIGALRM, &sa, NULL)) {perror("cysignals sigaction"); exit(1);} +#endif /* Handlers for critical signals */ sa.sa_handler = cysigs_signal_handler; /* Allow signals during signal handling, we have code to deal with * this case. */ sa.sa_flags = SA_NODEFER | SA_ONSTACK; +#ifdef SIGQUIT if (sigaction(SIGQUIT, &sa, NULL)) {perror("cysignals sigaction"); exit(1);} +#endif if (sigaction(SIGILL, &sa, NULL)) {perror("cysignals sigaction"); exit(1);} if (sigaction(SIGABRT, &sa, NULL)) {perror("cysignals sigaction"); exit(1);} if (sigaction(SIGFPE, &sa, NULL)) {perror("cysignals sigaction"); exit(1);} +#ifdef SIGBUS if (sigaction(SIGBUS, &sa, NULL)) {perror("cysignals sigaction"); exit(1);} +#endif if (sigaction(SIGSEGV, &sa, NULL)) {perror("cysignals sigaction"); exit(1);} +#endif } diff --git a/src/cysignals/signals.pyx b/src/cysignals/signals.pyx index 6e7376e..b988167 100644 --- a/src/cysignals/signals.pyx +++ b/src/cysignals/signals.pyx @@ -33,6 +33,19 @@ cimport cython import sys from gc import collect +# On Windows, some signals are not pre-defined. +# We define them here with values that will never occur in practice +# (to avoid compilation errors and conditional compilation). +cdef extern from *: + """ + #if defined(_WIN32) || defined(WIN32) || defined(MS_WINDOWS) + #define NO_SUCH_SIGNAL 256 + #define SIGHUP NO_SUCH_SIGNAL + #define SIGALRM NO_SUCH_SIGNAL + #define SIGBUS NO_SUCH_SIGNAL + #endif + """ + pass cdef extern from "implementation.c": cysigs_t cysigs @@ -102,6 +115,9 @@ class AlarmInterrupt(KeyboardInterrupt): EXAMPLES:: + >>> import platform, pytest + >>> if platform.system() == 'Windows': + ... pytest.skip('this doctest does not work on Windows') >>> from cysignals import AlarmInterrupt >>> from signal import alarm >>> from time import sleep @@ -205,7 +221,7 @@ def sig_print_exception(sig, msg=None): >>> import signal >>> sig_print_exception(signal.SIGFPE) FloatingPointError: Floating point exception - >>> sig_print_exception(signal.SIGBUS, "CUSTOM MESSAGE") + >>> sig_print_exception(signal.SIGSEGV, "CUSTOM MESSAGE") cysignals.signals.SignalError: CUSTOM MESSAGE >>> sig_print_exception(0) SystemError: unknown signal number 0 @@ -214,6 +230,9 @@ def sig_print_exception(sig, msg=None): >>> sig_print_exception(signal.SIGINT, "ignored") KeyboardInterrupt + >>> import platform, pytest + >>> if platform.system() == 'Windows': + ... pytest.skip('this doctest does not work on Windows') >>> sig_print_exception(signal.SIGALRM, "ignored") cysignals.signals.AlarmInterrupt @@ -310,6 +329,9 @@ def sig_on_reset(): EXAMPLES:: + >>> import platform, pytest + >>> if platform.system() == 'Windows': + ... pytest.skip('this doctest does not work on Windows') >>> from cysignals.signals import sig_on_reset >>> from cysignals.tests import _sig_on >>> _sig_on(); sig_on_reset()