From 7c46b73f19718d2182bdf351a2b4aa257199029e Mon Sep 17 00:00:00 2001 From: Michael Simacek Date: Fri, 13 Feb 2026 08:00:58 +0100 Subject: [PATCH 1/5] Add AllowSignalHandlers option --- .../oracle/graal/python/shell/GraalPythonMain.java | 2 ++ .../advanced/AsyncActionThreadingTest.java | 5 +++-- .../python/builtins/modules/SignalModuleBuiltins.java | 11 ++++++++--- .../oracle/graal/python/runtime/PythonOptions.java | 3 +++ 4 files changed, 16 insertions(+), 5 deletions(-) diff --git a/graalpython/com.oracle.graal.python.shell/src/com/oracle/graal/python/shell/GraalPythonMain.java b/graalpython/com.oracle.graal.python.shell/src/com/oracle/graal/python/shell/GraalPythonMain.java index 9ff11e94e6..24623ce1e3 100644 --- a/graalpython/com.oracle.graal.python.shell/src/com/oracle/graal/python/shell/GraalPythonMain.java +++ b/graalpython/com.oracle.graal.python.shell/src/com/oracle/graal/python/shell/GraalPythonMain.java @@ -812,6 +812,8 @@ protected void launch(Builder contextBuilder) { contextBuilder.option("python.PyCachePrefix", cachePrefix); } + setOptionIfNotSetViaCommandLine(contextBuilder, "AllowSignalHandlers", "true"); + if (IS_WINDOWS) { contextBuilder.option("python.PosixModuleBackend", "java"); } diff --git a/graalpython/com.oracle.graal.python.test.integration/src/com/oracle/graal/python/test/integration/advanced/AsyncActionThreadingTest.java b/graalpython/com.oracle.graal.python.test.integration/src/com/oracle/graal/python/test/integration/advanced/AsyncActionThreadingTest.java index 2329920f60..bb85664a45 100644 --- a/graalpython/com.oracle.graal.python.test.integration/src/com/oracle/graal/python/test/integration/advanced/AsyncActionThreadingTest.java +++ b/graalpython/com.oracle.graal.python.test.integration/src/com/oracle/graal/python/test/integration/advanced/AsyncActionThreadingTest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2017, 2024, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2017, 2026, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * The Universal Permissive License (UPL), Version 1.0 @@ -44,6 +44,7 @@ import static org.junit.Assert.assertTrue; import java.util.Arrays; +import java.util.Map; import java.util.stream.Collectors; import java.util.stream.Stream; @@ -77,7 +78,7 @@ Stream pythonThreads() { public void testNoNewThreadsWithoutAutomaticAsyncActions() { Assume.assumeTrue("false".equalsIgnoreCase(System.getProperty("python.AutomaticAsyncActions"))); long threadCount = pythonThreadCount(); - Context c = PythonTests.enterContext(); + Context c = PythonTests.enterContext(Map.of("python.AllowSignalHandlers", "true", "python.PosixModuleBackend", "native"), new String[0]); try { Runnable poll = c.getPolyglotBindings().getMember("PollPythonAsyncActions").asHostObject(); c.eval("python", "import re, itertools, functools, _testcapi"); diff --git a/graalpython/com.oracle.graal.python/src/com/oracle/graal/python/builtins/modules/SignalModuleBuiltins.java b/graalpython/com.oracle.graal.python/src/com/oracle/graal/python/builtins/modules/SignalModuleBuiltins.java index 82c1a457ec..11a708d37b 100644 --- a/graalpython/com.oracle.graal.python/src/com/oracle/graal/python/builtins/modules/SignalModuleBuiltins.java +++ b/graalpython/com.oracle.graal.python/src/com/oracle/graal/python/builtins/modules/SignalModuleBuiltins.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2018, 2025, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2018, 2026, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * The Universal Permissive License (UPL), Version 1.0 @@ -188,7 +188,7 @@ public void postInitialize(Python3Core core) { return poll; }); - if (!context.getEnv().isPreInitialization() && context.getOption(PythonOptions.InstallSignalHandlers)) { + if (!context.getEnv().isPreInitialization() && context.getOption(PythonOptions.InstallSignalHandlers) && context.getOption(PythonOptions.AllowSignalHandlers)) { Object defaultSigintHandler = signalModule.getAttribute(T_DEFAULT_INT_HANDLER); assert defaultSigintHandler != PNone.NO_VALUE; SignalNode.signal(null, new Signal("INT").getNumber(), defaultSigintHandler, moduleData); @@ -351,7 +351,12 @@ abstract static class SignalNode extends PythonTernaryBuiltinNode { @Specialization static Object signalHandler(VirtualFrame frame, PythonModule self, Object signal, Object handler, @Bind Node inliningTarget, - @Cached PyNumberAsSizeNode asSizeNode) { + @Bind PythonContext context, + @Cached PyNumberAsSizeNode asSizeNode, + @Cached PConstructAndRaiseNode.Lazy constructAndRaiseNode) { + if (!context.getOption(PythonOptions.AllowSignalHandlers)) { + throw constructAndRaiseNode.get(inliningTarget).raiseOSError(frame, OSErrorEnum.EPERM); + } int signum = asSizeNode.executeExact(frame, inliningTarget, signal); return signalHandlerBoundary(self, handler, inliningTarget, signum); } diff --git a/graalpython/com.oracle.graal.python/src/com/oracle/graal/python/runtime/PythonOptions.java b/graalpython/com.oracle.graal.python/src/com/oracle/graal/python/runtime/PythonOptions.java index 24743dbcd8..fb7fdb25f0 100644 --- a/graalpython/com.oracle.graal.python/src/com/oracle/graal/python/runtime/PythonOptions.java +++ b/graalpython/com.oracle.graal.python/src/com/oracle/graal/python/runtime/PythonOptions.java @@ -244,6 +244,9 @@ public static void checkBytecodeDSLEnv() { @Option(category = OptionCategory.USER, help = "Install default signal handlers on startup", usageSyntax = "true|false", stability = OptionStability.STABLE) // public static final OptionKey InstallSignalHandlers = new OptionKey<>(false); + @Option(category = OptionCategory.USER, help = "Allow installing signal handlers", usageSyntax = "true|false", stability = OptionStability.STABLE) // + public static final OptionKey AllowSignalHandlers = new OptionKey<>(false); + @Option(category = OptionCategory.EXPERT, help = "Sets the language and territory, which will be used for initial locale. Format: 'language[_territory]', e.g., 'en_GB'. Leave empty to use the JVM default locale.", stability = OptionStability.STABLE) // public static final OptionKey InitialLocale = new OptionKey<>(""); From 6a96f14a5f24b54a825c3cd1fd9d5e46b17c1afc Mon Sep 17 00:00:00 2001 From: Michael Simacek Date: Tue, 28 Apr 2026 12:52:38 +0200 Subject: [PATCH 2/5] Make signal.raise_signal use posix raise --- .../builtins/modules/SignalModuleBuiltins.java | 18 +++++++++++++++--- .../python/runtime/EmulatedPosixSupport.java | 6 ++++++ .../python/runtime/LoggingPosixSupport.java | 11 +++++++++++ .../graal/python/runtime/NFIPosixSupport.java | 10 ++++++++++ .../python/runtime/PosixSupportLibrary.java | 2 ++ .../python/runtime/PreInitPosixSupport.java | 7 +++++++ graalpython/python-libposix/src/posix.c | 4 ++++ 7 files changed, 55 insertions(+), 3 deletions(-) diff --git a/graalpython/com.oracle.graal.python/src/com/oracle/graal/python/builtins/modules/SignalModuleBuiltins.java b/graalpython/com.oracle.graal.python/src/com/oracle/graal/python/builtins/modules/SignalModuleBuiltins.java index 11a708d37b..ab363f26fe 100644 --- a/graalpython/com.oracle.graal.python/src/com/oracle/graal/python/builtins/modules/SignalModuleBuiltins.java +++ b/graalpython/com.oracle.graal.python/src/com/oracle/graal/python/builtins/modules/SignalModuleBuiltins.java @@ -85,6 +85,8 @@ import com.oracle.graal.python.nodes.util.CastToJavaIntExactNode; import com.oracle.graal.python.runtime.AsyncHandler; import com.oracle.graal.python.runtime.PosixSupportLibrary; +import com.oracle.graal.python.runtime.PosixSupportLibrary.PosixException; +import com.oracle.graal.python.runtime.PosixSupportLibrary.UnsupportedPosixFeatureException; import com.oracle.graal.python.runtime.PythonContext; import com.oracle.graal.python.runtime.PythonOptions; import com.oracle.graal.python.runtime.exception.PException; @@ -98,6 +100,7 @@ import com.oracle.truffle.api.dsl.NodeFactory; import com.oracle.truffle.api.dsl.Specialization; import com.oracle.truffle.api.frame.VirtualFrame; +import com.oracle.truffle.api.library.CachedLibrary; import com.oracle.truffle.api.nodes.Node; import com.oracle.truffle.api.strings.TruffleString; @@ -453,9 +456,18 @@ protected ArgumentClinicProvider getArgumentClinic() { @GenerateNodeFactory abstract static class RaiseSignalNode extends PythonUnaryClinicBuiltinNode { @Specialization - @TruffleBoundary - static PNone doInt(int signum) { - Signal.raise(new sun.misc.Signal(Signals.signalNumberToName(signum))); + static PNone doInt(VirtualFrame frame, int signum, + @Bind PythonContext context, + @Bind Node inliningTarget, + @CachedLibrary("context.getPosixSupport()") PosixSupportLibrary posixLib, + @Cached PConstructAndRaiseNode.Lazy constructAndRaiseNode) { + try { + posixLib.raise(context.getPosixSupport(), signum); + } catch (PosixException e) { + throw constructAndRaiseNode.get(inliningTarget).raiseOSErrorFromPosixException(frame, e); + } catch (UnsupportedPosixFeatureException e) { + throw constructAndRaiseNode.get(inliningTarget).raiseOSErrorUnsupported(frame, e); + } return PNone.NONE; } diff --git a/graalpython/com.oracle.graal.python/src/com/oracle/graal/python/runtime/EmulatedPosixSupport.java b/graalpython/com.oracle.graal.python/src/com/oracle/graal/python/runtime/EmulatedPosixSupport.java index 88ec3a30f9..5768560336 100644 --- a/graalpython/com.oracle.graal.python/src/com/oracle/graal/python/runtime/EmulatedPosixSupport.java +++ b/graalpython/com.oracle.graal.python/src/com/oracle/graal/python/runtime/EmulatedPosixSupport.java @@ -2014,6 +2014,12 @@ public void kill(long pid, int signal, } } + @ExportMessage + @SuppressWarnings("static-method") + public void raise(int signal) { + throw createUnsupportedFeature("raise"); + } + @ExportMessage @SuppressWarnings("static-method") public void signalSelf(int signal) { diff --git a/graalpython/com.oracle.graal.python/src/com/oracle/graal/python/runtime/LoggingPosixSupport.java b/graalpython/com.oracle.graal.python/src/com/oracle/graal/python/runtime/LoggingPosixSupport.java index e7371fadb9..1a2535210e 100644 --- a/graalpython/com.oracle.graal.python/src/com/oracle/graal/python/runtime/LoggingPosixSupport.java +++ b/graalpython/com.oracle.graal.python/src/com/oracle/graal/python/runtime/LoggingPosixSupport.java @@ -752,6 +752,17 @@ final void kill(long pid, int signal, } } + @ExportMessage + final void raise(int signal, + @CachedLibrary("this.delegate") PosixSupportLibrary lib) throws PosixException { + logEnter("raise", "%d", signal); + try { + lib.raise(delegate, signal); + } catch (PosixException e) { + throw logException("raise", e); + } + } + @ExportMessage final void signalSelf(int signal, @CachedLibrary("this.delegate") PosixSupportLibrary lib) throws PosixException { diff --git a/graalpython/com.oracle.graal.python/src/com/oracle/graal/python/runtime/NFIPosixSupport.java b/graalpython/com.oracle.graal.python/src/com/oracle/graal/python/runtime/NFIPosixSupport.java index 1bf99341f3..2078e00c6c 100644 --- a/graalpython/com.oracle.graal.python/src/com/oracle/graal/python/runtime/NFIPosixSupport.java +++ b/graalpython/com.oracle.graal.python/src/com/oracle/graal/python/runtime/NFIPosixSupport.java @@ -230,6 +230,7 @@ private enum PosixNativeFunction { get_blocking("(sint32):sint32"), set_blocking("(sint32, sint32):sint32"), get_terminal_size("(sint32, [sint32]):sint32"), + call_raise("(sint32):sint32"), signal_self("(sint32):sint32"), call_kill("(sint64, sint32):sint32"), call_killpg("(sint64, sint32):sint32"), @@ -1166,6 +1167,15 @@ public void kill(long pid, int signal, } } + @ExportMessage + public void raise(int signal, + @Shared("invoke") @Cached InvokeNativeFunction invokeNode) throws PosixException { + int res = invokeNode.callInt(this, PosixNativeFunction.call_raise, signal); + if (res == -1) { + throw getErrnoAndThrowPosixException(invokeNode); + } + } + @ExportMessage public void signalSelf(int signal, @Shared("invoke") @Cached InvokeNativeFunction invokeNode) throws PosixException { diff --git a/graalpython/com.oracle.graal.python/src/com/oracle/graal/python/runtime/PosixSupportLibrary.java b/graalpython/com.oracle.graal.python/src/com/oracle/graal/python/runtime/PosixSupportLibrary.java index 5ad6aaac3e..e7e3cdd623 100644 --- a/graalpython/com.oracle.graal.python/src/com/oracle/graal/python/runtime/PosixSupportLibrary.java +++ b/graalpython/com.oracle.graal.python/src/com/oracle/graal/python/runtime/PosixSupportLibrary.java @@ -258,6 +258,8 @@ public abstract class PosixSupportLibrary extends Library { public abstract Object readlinkat(Object receiver, int dirFd, Object path) throws PosixException; + public abstract void raise(Object receiver, int signal) throws PosixException; + public abstract void signalSelf(Object receiver, int signal) throws PosixException; public abstract void kill(Object receiver, long pid, int signal) throws PosixException; diff --git a/graalpython/com.oracle.graal.python/src/com/oracle/graal/python/runtime/PreInitPosixSupport.java b/graalpython/com.oracle.graal.python/src/com/oracle/graal/python/runtime/PreInitPosixSupport.java index 9b36dc9fd0..55e0881374 100644 --- a/graalpython/com.oracle.graal.python/src/com/oracle/graal/python/runtime/PreInitPosixSupport.java +++ b/graalpython/com.oracle.graal.python/src/com/oracle/graal/python/runtime/PreInitPosixSupport.java @@ -588,6 +588,13 @@ final Object readlinkat(int dirFd, Object path, return nativeLib.readlinkat(nativePosixSupport, dirFd, path); } + @ExportMessage + final void raise(int signal, + @CachedLibrary("this.nativePosixSupport") PosixSupportLibrary nativeLib) throws PosixException { + checkNotInPreInitialization(); + nativeLib.raise(nativePosixSupport, signal); + } + @ExportMessage final void signalSelf(int signal, @CachedLibrary("this.nativePosixSupport") PosixSupportLibrary nativeLib) throws PosixException { diff --git a/graalpython/python-libposix/src/posix.c b/graalpython/python-libposix/src/posix.c index 5d6d863e95..f1d47f3c19 100644 --- a/graalpython/python-libposix/src/posix.c +++ b/graalpython/python-libposix/src/posix.c @@ -604,6 +604,10 @@ int32_t call_kill(int64_t pid, int32_t signal) { return kill(pid, signal); } +int32_t call_raise(int32_t signal) { + return raise(signal); +} + int32_t signal_self(int32_t signal) { switch (signal) { case SIGABRT: From 2e860d05931c12703b933d74f1406d71c9ec5ece Mon Sep 17 00:00:00 2001 From: Michael Simacek Date: Tue, 28 Apr 2026 17:52:33 +0200 Subject: [PATCH 3/5] Implement alarm and setitimer in posix library --- .../builtins/PythonBuiltinClassType.java | 3 + .../modules/SignalModuleBuiltins.java | 193 +++++------------- .../graal/python/nodes/BuiltinNames.java | 6 +- .../python/runtime/EmulatedPosixSupport.java | 18 ++ .../python/runtime/LoggingPosixSupport.java | 33 +++ .../graal/python/runtime/NFIPosixSupport.java | 39 ++++ .../python/runtime/PosixSupportLibrary.java | 6 + .../python/runtime/PreInitPosixSupport.java | 21 ++ graalpython/python-libposix/src/posix.c | 38 ++++ 9 files changed, 217 insertions(+), 140 deletions(-) diff --git a/graalpython/com.oracle.graal.python/src/com/oracle/graal/python/builtins/PythonBuiltinClassType.java b/graalpython/com.oracle.graal.python/src/com/oracle/graal/python/builtins/PythonBuiltinClassType.java index c24b1ff866..5a8fca5ad5 100644 --- a/graalpython/com.oracle.graal.python/src/com/oracle/graal/python/builtins/PythonBuiltinClassType.java +++ b/graalpython/com.oracle.graal.python/src/com/oracle/graal/python/builtins/PythonBuiltinClassType.java @@ -51,6 +51,7 @@ import static com.oracle.graal.python.nodes.BuiltinNames.J_POLYGLOT; import static com.oracle.graal.python.nodes.BuiltinNames.J_POSIX; import static com.oracle.graal.python.nodes.BuiltinNames.J_PROPERTY; +import static com.oracle.graal.python.nodes.BuiltinNames.J_SIGNAL; import static com.oracle.graal.python.nodes.BuiltinNames.J_SHA1; import static com.oracle.graal.python.nodes.BuiltinNames.J_SHA2; import static com.oracle.graal.python.nodes.BuiltinNames.J_SHA3; @@ -63,6 +64,7 @@ import static com.oracle.graal.python.nodes.BuiltinNames.J_TYPING; import static com.oracle.graal.python.nodes.BuiltinNames.J_WRAPPER_DESCRIPTOR; import static com.oracle.graal.python.nodes.BuiltinNames.J__CONTEXTVARS; +import static com.oracle.graal.python.nodes.BuiltinNames.J__SIGNAL; import static com.oracle.graal.python.nodes.BuiltinNames.J__SOCKET; import static com.oracle.graal.python.nodes.BuiltinNames.J__SSL; import static com.oracle.graal.python.nodes.BuiltinNames.J__STRUCT; @@ -785,6 +787,7 @@ It can be called either on the class (e.g. C.f()) or on an instance PickleError("PickleError", Exception, newBuilder().publishInModule("_pickle").basetype().addDict()), PicklingError("PicklingError", PickleError, newBuilder().publishInModule("_pickle").basetype().addDict()), UnpicklingError("UnpicklingError", PickleError, newBuilder().publishInModule("_pickle").basetype().addDict()), + SignalItimerError("itimer_error", OSError, newBuilder().publishInModule(J__SIGNAL).moduleName(J_SIGNAL).basetype().addDict()), SocketGAIError("gaierror", OSError, newBuilder().publishInModule(J__SOCKET).basetype().addDict()), SocketHError("herror", OSError, newBuilder().publishInModule(J__SOCKET).basetype().addDict()), BinasciiError("Error", ValueError, newBuilder().publishInModule("binascii").basetype().addDict()), diff --git a/graalpython/com.oracle.graal.python/src/com/oracle/graal/python/builtins/modules/SignalModuleBuiltins.java b/graalpython/com.oracle.graal.python/src/com/oracle/graal/python/builtins/modules/SignalModuleBuiltins.java index ab363f26fe..c6fa8b6d04 100644 --- a/graalpython/com.oracle.graal.python/src/com/oracle/graal/python/builtins/modules/SignalModuleBuiltins.java +++ b/graalpython/com.oracle.graal.python/src/com/oracle/graal/python/builtins/modules/SignalModuleBuiltins.java @@ -51,11 +51,7 @@ import java.util.Map; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentLinkedDeque; -import java.util.concurrent.Executors; -import java.util.concurrent.ScheduledExecutorService; -import java.util.concurrent.ScheduledFuture; import java.util.concurrent.Semaphore; -import java.util.concurrent.TimeUnit; import com.oracle.graal.python.PythonLanguage; import com.oracle.graal.python.annotations.ArgumentClinic; @@ -63,6 +59,7 @@ import com.oracle.graal.python.builtins.CoreFunctions; import com.oracle.graal.python.builtins.Python3Core; import com.oracle.graal.python.builtins.PythonBuiltins; +import com.oracle.graal.python.builtins.PythonBuiltinClassType; import com.oracle.graal.python.builtins.objects.PNone; import com.oracle.graal.python.builtins.objects.exception.OSErrorEnum; import com.oracle.graal.python.builtins.objects.module.PythonModule; @@ -77,8 +74,8 @@ import com.oracle.graal.python.nodes.function.PythonBuiltinBaseNode; import com.oracle.graal.python.nodes.function.PythonBuiltinNode; import com.oracle.graal.python.nodes.function.builtins.PythonBinaryClinicBuiltinNode; -import com.oracle.graal.python.nodes.function.builtins.PythonQuaternaryClinicBuiltinNode; import com.oracle.graal.python.nodes.function.builtins.PythonTernaryBuiltinNode; +import com.oracle.graal.python.nodes.function.builtins.PythonTernaryClinicBuiltinNode; import com.oracle.graal.python.nodes.function.builtins.PythonUnaryClinicBuiltinNode; import com.oracle.graal.python.nodes.function.builtins.clinic.ArgumentClinicProvider; import com.oracle.graal.python.nodes.util.CannotCastException; @@ -86,6 +83,7 @@ import com.oracle.graal.python.runtime.AsyncHandler; import com.oracle.graal.python.runtime.PosixSupportLibrary; import com.oracle.graal.python.runtime.PosixSupportLibrary.PosixException; +import com.oracle.graal.python.runtime.PosixSupportLibrary.Timeval; import com.oracle.graal.python.runtime.PosixSupportLibrary.UnsupportedPosixFeatureException; import com.oracle.graal.python.runtime.PythonContext; import com.oracle.graal.python.runtime.PythonOptions; @@ -112,6 +110,7 @@ public final class SignalModuleBuiltins extends PythonBuiltins { private static final int ITIMER_REAL = 0; private static final int ITIMER_VIRTUAL = 1; private static final int ITIMER_PROF = 2; + private static final TruffleString T_ITIMER_ERROR = tsLiteral("ItimerError"); public static final String J_DEFAULT_INT_HANDLER = "default_int_handler"; public static final TruffleString T_DEFAULT_INT_HANDLER = tsLiteral(J_DEFAULT_INT_HANDLER); @@ -157,6 +156,7 @@ public void postInitialize(Python3Core core) { PythonModule signalModule = core.lookupBuiltinModule(T__SIGNAL); ModuleData moduleData = new ModuleData(); signalModule.setModuleState(moduleData); + signalModule.setAttribute(T_ITIMER_ERROR, core.lookupType(PythonBuiltinClassType.SignalItimerError)); for (int i = 0; i < Signals.PYTHON_SIGNAL_NAMES.length; i++) { TruffleString name = Signals.PYTHON_SIGNAL_NAMES[i]; @@ -269,31 +269,23 @@ static Object validSignals( } } - @Builtin(name = "alarm", minNumOfPositionalArgs = 2, numOfPositionalOnlyArgs = 2, declaresExplicitSelf = true, parameterNames = {"$mod", "seconds"}) + @Builtin(name = "alarm", minNumOfPositionalArgs = 1, numOfPositionalOnlyArgs = 1, parameterNames = {"seconds"}) @ArgumentClinic(name = "seconds", conversion = ArgumentClinic.ClinicConversion.Int) @GenerateNodeFactory - abstract static class AlarmNode extends PythonBinaryClinicBuiltinNode { + abstract static class AlarmNode extends PythonUnaryClinicBuiltinNode { @Specialization - @TruffleBoundary - int alarm(PythonModule module, int seconds) { - int remaining = 0; - ModuleData data = module.getModuleState(ModuleData.class); - Signals.Alarm currentAlarm = data.currentAlarm; - if (currentAlarm != null) { - if (currentAlarm.isRunning()) { - remaining = currentAlarm.getRemainingSeconds(); - if (remaining < 0) { - remaining = 0; - } - currentAlarm.cancel(); - } - } - if (seconds > 0) { - Signals.Alarm newAlarm = new Signals.Alarm(seconds); - data.currentAlarm = newAlarm; - newAlarm.start(); + static int alarm(VirtualFrame frame, int seconds, + @Bind PythonContext context, + @Bind Node inliningTarget, + @CachedLibrary("context.getPosixSupport()") PosixSupportLibrary posixLib, + @Cached PConstructAndRaiseNode.Lazy constructAndRaiseNode) { + try { + return posixLib.alarm(context.getPosixSupport(), seconds); + } catch (PosixException e) { + throw constructAndRaiseNode.get(inliningTarget).raiseOSErrorFromPosixException(frame, e); + } catch (UnsupportedPosixFeatureException e) { + throw constructAndRaiseNode.get(inliningTarget).raiseOSErrorUnsupported(frame, e); } - return remaining; } @Override @@ -477,10 +469,10 @@ protected ArgumentClinicProvider getArgumentClinic() { } } - @Builtin(name = "setitimer", minNumOfPositionalArgs = 2, declaresExplicitSelf = true, parameterNames = {"$self", "which", "seconds", "interval"}) + @Builtin(name = "setitimer", minNumOfPositionalArgs = 2, parameterNames = {"which", "seconds", "interval"}) @ArgumentClinic(name = "which", conversion = ArgumentClinic.ClinicConversion.Int) @GenerateNodeFactory - abstract static class SetitimerNode extends PythonQuaternaryClinicBuiltinNode { + abstract static class SetitimerNode extends PythonTernaryClinicBuiltinNode { @Override protected ArgumentClinicProvider getArgumentClinic() { @@ -488,68 +480,49 @@ protected ArgumentClinicProvider getArgumentClinic() { } @Specialization - Object doIt(VirtualFrame frame, PythonModule self, int which, Object seconds, Object interval, + static Object doIt(VirtualFrame frame, int which, Object seconds, Object interval, @Bind Node inliningTarget, + @Bind PythonContext context, + @CachedLibrary("context.getPosixSupport()") PosixSupportLibrary posixLib, @Cached PyTimeFromObjectNode timeFromObjectNode, @Cached PConstructAndRaiseNode.Lazy constructAndRaiseNode, @Bind PythonLanguage language) { - ModuleData moduleData = self.getModuleState(ModuleData.class); - long usDelay = toMicroseconds(frame, inliningTarget, seconds, timeFromObjectNode); - long usInterval = toMicroseconds(frame, inliningTarget, interval, timeFromObjectNode); - if (which != ITIMER_REAL) { - throw constructAndRaiseNode.get(inliningTarget).raiseOSError(frame, OSErrorEnum.EINVAL); + Timeval delay = toTimeval(frame, inliningTarget, seconds, timeFromObjectNode); + Timeval intervalTimeval = toTimeval(frame, inliningTarget, interval, timeFromObjectNode); + try { + return createResultTuple(language, posixLib.setitimer(context.getPosixSupport(), which, delay, intervalTimeval)); + } catch (PosixException e) { + throw raiseItimerError(frame, inliningTarget, e, constructAndRaiseNode); + } catch (UnsupportedPosixFeatureException e) { + throw constructAndRaiseNode.get(inliningTarget).raiseOSErrorUnsupported(frame, e); } - PTuple resultTuple = GetitimerNode.createResultTuple(language, moduleData); - setitimer(moduleData, usDelay, usInterval); - return resultTuple; } - private static long toMicroseconds(VirtualFrame frame, Node inliningTarget, Object obj, PyTimeFromObjectNode timeFromObjectNode) { + private static Timeval toTimeval(VirtualFrame frame, Node inliningTarget, Object obj, PyTimeFromObjectNode timeFromObjectNode) { if (obj == PNone.NO_VALUE) { - return 0; + return new Timeval(0, 0); } - return timeFromObjectNode.execute(frame, inliningTarget, obj, RoundType.CEILING, SEC_TO_US); + long microseconds = timeFromObjectNode.execute(frame, inliningTarget, obj, RoundType.CEILING, SEC_TO_US); + return new Timeval(microseconds / SEC_TO_US, microseconds % SEC_TO_US); } - @TruffleBoundary - private void setitimer(ModuleData moduleData, long usDelay, long usInterval) { - if (moduleData.itimerFuture != null) { - moduleData.itimerFuture.cancel(false); - moduleData.itimerFuture = null; - moduleData.itimerInterval = 0; - } - if (usDelay == 0) { - return; - } - moduleData.itimerInterval = usInterval; - Runnable r = () -> Signals.raiseSignal("ALRM"); - ScheduledExecutorService itimerService = getItimerService(moduleData); - if (usInterval == 0) { - moduleData.itimerFuture = itimerService.schedule(r, usDelay, TimeUnit.MICROSECONDS); - } else { - moduleData.itimerFuture = itimerService.scheduleAtFixedRate(r, usDelay, usInterval, TimeUnit.MICROSECONDS); - } + private static PTuple createResultTuple(PythonLanguage language, Timeval[] timeval) { + return PFactory.createTuple(language, new Object[]{toSeconds(timeval[0]), toSeconds(timeval[1])}); } - @TruffleBoundary - private ScheduledExecutorService getItimerService(ModuleData moduleData) { - if (moduleData.itimerService == null) { - ScheduledExecutorService itimerService = Executors.newSingleThreadScheduledExecutor(runnable -> { - Thread t = Executors.defaultThreadFactory().newThread(runnable); - t.setDaemon(true); - return t; - }); - moduleData.itimerService = itimerService; - getContext().registerAtexitHook(ctx -> itimerService.shutdown()); - } - return moduleData.itimerService; + private static double toSeconds(Timeval timeval) { + return timeval.getSeconds() + timeval.getMicroseconds() / (double) SEC_TO_US; + } + + private static PException raiseItimerError(VirtualFrame frame, Node inliningTarget, PosixException e, PConstructAndRaiseNode.Lazy constructAndRaiseNode) { + throw constructAndRaiseNode.get(inliningTarget).executeWithArgsOnly(frame, PythonBuiltinClassType.SignalItimerError, new Object[]{e.getErrorCode(), e.getMessageAsTruffleString()}); } } - @Builtin(name = "getitimer", minNumOfPositionalArgs = 1, declaresExplicitSelf = true, parameterNames = {"$self", "which"}) + @Builtin(name = "getitimer", minNumOfPositionalArgs = 1, parameterNames = {"which"}) @ArgumentClinic(name = "which", conversion = ArgumentClinic.ClinicConversion.Int) @GenerateNodeFactory - abstract static class GetitimerNode extends PythonBinaryClinicBuiltinNode { + abstract static class GetitimerNode extends PythonUnaryClinicBuiltinNode { @Override protected ArgumentClinicProvider getArgumentClinic() { @@ -557,33 +530,19 @@ protected ArgumentClinicProvider getArgumentClinic() { } @Specialization - static Object doIt(VirtualFrame frame, PythonModule self, int which, + static Object doIt(VirtualFrame frame, int which, @Bind Node inliningTarget, + @Bind PythonContext context, + @CachedLibrary("context.getPosixSupport()") PosixSupportLibrary posixLib, @Cached PConstructAndRaiseNode.Lazy constructAndRaiseNode, @Bind PythonLanguage language) { - ModuleData moduleData = self.getModuleState(ModuleData.class); - if (which != ITIMER_REAL) { - throw constructAndRaiseNode.get(inliningTarget).raiseOSError(frame, OSErrorEnum.EINVAL); - } - return createResultTuple(language, moduleData); - } - - static PTuple createResultTuple(PythonLanguage language, ModuleData moduleData) { - long oldInterval = moduleData.itimerInterval; - long oldDelay = getOldDelay(moduleData); - return PFactory.createTuple(language, new Object[]{oldDelay / (double) SEC_TO_US, oldInterval / (double) SEC_TO_US}); - } - - @TruffleBoundary - static long getOldDelay(ModuleData moduleData) { - if (moduleData.itimerFuture == null) { - return 0; - } - long delay = moduleData.itimerFuture.getDelay(TimeUnit.MICROSECONDS); - if (delay < 0) { - return 0; + try { + return SetitimerNode.createResultTuple(language, posixLib.getitimer(context.getPosixSupport(), which)); + } catch (PosixException e) { + throw SetitimerNode.raiseItimerError(frame, inliningTarget, e, constructAndRaiseNode); + } catch (UnsupportedPosixFeatureException e) { + throw constructAndRaiseNode.get(inliningTarget).raiseOSErrorUnsupported(frame, e); } - return delay; } } @@ -593,10 +552,6 @@ private static final class ModuleData { final ConcurrentHashMap defaultSignalHandlers = new ConcurrentHashMap<>(); final ConcurrentLinkedDeque signalQueue = new ConcurrentLinkedDeque<>(); final Semaphore signalSema = new Semaphore(0); - ScheduledExecutorService itimerService; - ScheduledFuture itimerFuture; - long itimerInterval; - Signals.Alarm currentAlarm; } } @@ -631,46 +586,6 @@ final class Signals { } } - static final class Alarm { - final int seconds; - final long startMillis; - private Thread thread; - - public Alarm(int seconds) { - this.seconds = seconds; - startMillis = System.currentTimeMillis(); - } - - public void start() { - thread = new Thread(() -> { - try { - Thread.sleep((long) seconds * 1000); - sun.misc.Signal.raise(new sun.misc.Signal("ALRM")); - } catch (InterruptedException e) { - // Cancelled - } - }); - thread.start(); - } - - public boolean isRunning() { - return thread.isAlive(); - } - - public void cancel() { - thread.interrupt(); - } - - public int getRemainingSeconds() { - return seconds - (int) ((System.currentTimeMillis() - startMillis) / 1000); - } - } - - @TruffleBoundary - static void raiseSignal(String name) { - sun.misc.Signal.raise(new sun.misc.Signal(name)); - } - static class PythonSignalHandler implements sun.misc.SignalHandler { private final Runnable handler; diff --git a/graalpython/com.oracle.graal.python/src/com/oracle/graal/python/nodes/BuiltinNames.java b/graalpython/com.oracle.graal.python/src/com/oracle/graal/python/nodes/BuiltinNames.java index dfce98243a..70c6b83296 100644 --- a/graalpython/com.oracle.graal.python/src/com/oracle/graal/python/nodes/BuiltinNames.java +++ b/graalpython/com.oracle.graal.python/src/com/oracle/graal/python/nodes/BuiltinNames.java @@ -394,7 +394,11 @@ private static TruffleString tsLiteral(String s) { public static final TruffleString T_SYS = tsLiteral("sys"); - public static final TruffleString T__SIGNAL = tsLiteral("_signal"); + public static final String J_SIGNAL = "signal"; + public static final TruffleString T_SIGNAL = tsLiteral(J_SIGNAL); + + public static final String J__SIGNAL = "_signal"; + public static final TruffleString T__SIGNAL = tsLiteral(J__SIGNAL); public static final String J__WEAKREF = "_weakref"; public static final TruffleString T__WEAKREF = tsLiteral(J__WEAKREF); diff --git a/graalpython/com.oracle.graal.python/src/com/oracle/graal/python/runtime/EmulatedPosixSupport.java b/graalpython/com.oracle.graal.python/src/com/oracle/graal/python/runtime/EmulatedPosixSupport.java index 5768560336..ef92e82946 100644 --- a/graalpython/com.oracle.graal.python/src/com/oracle/graal/python/runtime/EmulatedPosixSupport.java +++ b/graalpython/com.oracle.graal.python/src/com/oracle/graal/python/runtime/EmulatedPosixSupport.java @@ -2020,6 +2020,24 @@ public void raise(int signal) { throw createUnsupportedFeature("raise"); } + @ExportMessage + @SuppressWarnings("static-method") + public int alarm(int seconds) { + throw createUnsupportedFeature("alarm"); + } + + @ExportMessage + @SuppressWarnings("static-method") + public Timeval[] getitimer(int which) { + throw createUnsupportedFeature("getitimer"); + } + + @ExportMessage + @SuppressWarnings("static-method") + public Timeval[] setitimer(int which, Timeval delay, Timeval interval) { + throw createUnsupportedFeature("setitimer"); + } + @ExportMessage @SuppressWarnings("static-method") public void signalSelf(int signal) { diff --git a/graalpython/com.oracle.graal.python/src/com/oracle/graal/python/runtime/LoggingPosixSupport.java b/graalpython/com.oracle.graal.python/src/com/oracle/graal/python/runtime/LoggingPosixSupport.java index 1a2535210e..64b64b8512 100644 --- a/graalpython/com.oracle.graal.python/src/com/oracle/graal/python/runtime/LoggingPosixSupport.java +++ b/graalpython/com.oracle.graal.python/src/com/oracle/graal/python/runtime/LoggingPosixSupport.java @@ -763,6 +763,39 @@ final void raise(int signal, } } + @ExportMessage + final int alarm(int seconds, + @CachedLibrary("this.delegate") PosixSupportLibrary lib) throws PosixException { + logEnter("alarm", "%d", seconds); + try { + return logExit("alarm", "%d", lib.alarm(delegate, seconds)); + } catch (PosixException e) { + throw logException("alarm", e); + } + } + + @ExportMessage + final Timeval[] getitimer(int which, + @CachedLibrary("this.delegate") PosixSupportLibrary lib) throws PosixException { + logEnter("getitimer", "%d", which); + try { + return logExit("getitimer", "%s", lib.getitimer(delegate, which)); + } catch (PosixException e) { + throw logException("getitimer", e); + } + } + + @ExportMessage + final Timeval[] setitimer(int which, Timeval delay, Timeval interval, + @CachedLibrary("this.delegate") PosixSupportLibrary lib) throws PosixException { + logEnter("setitimer", "%d, %s, %s", which, delay, interval); + try { + return logExit("setitimer", "%s", lib.setitimer(delegate, which, delay, interval)); + } catch (PosixException e) { + throw logException("setitimer", e); + } + } + @ExportMessage final void signalSelf(int signal, @CachedLibrary("this.delegate") PosixSupportLibrary lib) throws PosixException { diff --git a/graalpython/com.oracle.graal.python/src/com/oracle/graal/python/runtime/NFIPosixSupport.java b/graalpython/com.oracle.graal.python/src/com/oracle/graal/python/runtime/NFIPosixSupport.java index 2078e00c6c..b8113d1d53 100644 --- a/graalpython/com.oracle.graal.python/src/com/oracle/graal/python/runtime/NFIPosixSupport.java +++ b/graalpython/com.oracle.graal.python/src/com/oracle/graal/python/runtime/NFIPosixSupport.java @@ -231,6 +231,9 @@ private enum PosixNativeFunction { set_blocking("(sint32, sint32):sint32"), get_terminal_size("(sint32, [sint32]):sint32"), call_raise("(sint32):sint32"), + call_alarm("(sint32):sint32"), + call_getitimer("(sint32, [sint64]):sint32"), + call_setitimer("(sint32, [sint64], [sint64]):sint32"), signal_self("(sint32):sint32"), call_kill("(sint64, sint32):sint32"), call_killpg("(sint64, sint32):sint32"), @@ -1176,6 +1179,34 @@ public void raise(int signal, } } + @ExportMessage + public int alarm(int seconds, + @Shared("invoke") @Cached InvokeNativeFunction invokeNode) { + return invokeNode.callInt(this, PosixNativeFunction.call_alarm, seconds); + } + + @ExportMessage + public Timeval[] getitimer(int which, + @Shared("invoke") @Cached InvokeNativeFunction invokeNode) throws PosixException { + long[] currentValue = new long[4]; + int res = invokeNode.callInt(this, PosixNativeFunction.call_getitimer, which, currentValue); + if (res == -1) { + throw getErrnoAndThrowPosixException(invokeNode); + } + return unwrapTimeval(currentValue); + } + + @ExportMessage + public Timeval[] setitimer(int which, Timeval delay, Timeval interval, + @Shared("invoke") @Cached InvokeNativeFunction invokeNode) throws PosixException { + long[] oldValue = new long[4]; + int res = invokeNode.callInt(this, PosixNativeFunction.call_setitimer, which, wrapItimerval(delay, interval), oldValue); + if (res == -1) { + throw getErrnoAndThrowPosixException(invokeNode); + } + return unwrapTimeval(oldValue); + } + @ExportMessage public void signalSelf(int signal, @Shared("invoke") @Cached InvokeNativeFunction invokeNode) throws PosixException { @@ -2711,6 +2742,14 @@ private Object wrap(Timeval[] timeval) { } } + private static long[] wrapItimerval(Timeval delay, Timeval interval) { + return new long[]{delay.getSeconds(), delay.getMicroseconds(), interval.getSeconds(), interval.getMicroseconds()}; + } + + private static Timeval[] unwrapTimeval(long[] timeval) { + return new Timeval[]{new Timeval(timeval[0], timeval[1]), new Timeval(timeval[2], timeval[3])}; + } + private static TruffleString cStringToTruffleString(byte[] buf, TruffleString.FromByteArrayNode fromByteArrayNode, TruffleString.SwitchEncodingNode switchEncodingNode) { return createString(buf, 0, findZero(buf), true, fromByteArrayNode, switchEncodingNode); } diff --git a/graalpython/com.oracle.graal.python/src/com/oracle/graal/python/runtime/PosixSupportLibrary.java b/graalpython/com.oracle.graal.python/src/com/oracle/graal/python/runtime/PosixSupportLibrary.java index e7e3cdd623..b5e92d39d7 100644 --- a/graalpython/com.oracle.graal.python/src/com/oracle/graal/python/runtime/PosixSupportLibrary.java +++ b/graalpython/com.oracle.graal.python/src/com/oracle/graal/python/runtime/PosixSupportLibrary.java @@ -260,6 +260,12 @@ public abstract class PosixSupportLibrary extends Library { public abstract void raise(Object receiver, int signal) throws PosixException; + public abstract int alarm(Object receiver, int seconds) throws PosixException; + + public abstract Timeval[] getitimer(Object receiver, int which) throws PosixException; + + public abstract Timeval[] setitimer(Object receiver, int which, Timeval delay, Timeval interval) throws PosixException; + public abstract void signalSelf(Object receiver, int signal) throws PosixException; public abstract void kill(Object receiver, long pid, int signal) throws PosixException; diff --git a/graalpython/com.oracle.graal.python/src/com/oracle/graal/python/runtime/PreInitPosixSupport.java b/graalpython/com.oracle.graal.python/src/com/oracle/graal/python/runtime/PreInitPosixSupport.java index 55e0881374..fd9eb2073d 100644 --- a/graalpython/com.oracle.graal.python/src/com/oracle/graal/python/runtime/PreInitPosixSupport.java +++ b/graalpython/com.oracle.graal.python/src/com/oracle/graal/python/runtime/PreInitPosixSupport.java @@ -595,6 +595,27 @@ final void raise(int signal, nativeLib.raise(nativePosixSupport, signal); } + @ExportMessage + final int alarm(int seconds, + @CachedLibrary("this.nativePosixSupport") PosixSupportLibrary nativeLib) throws PosixException { + checkNotInPreInitialization(); + return nativeLib.alarm(nativePosixSupport, seconds); + } + + @ExportMessage + final Timeval[] getitimer(int which, + @CachedLibrary("this.nativePosixSupport") PosixSupportLibrary nativeLib) throws PosixException { + checkNotInPreInitialization(); + return nativeLib.getitimer(nativePosixSupport, which); + } + + @ExportMessage + final Timeval[] setitimer(int which, Timeval delay, Timeval interval, + @CachedLibrary("this.nativePosixSupport") PosixSupportLibrary nativeLib) throws PosixException { + checkNotInPreInitialization(); + return nativeLib.setitimer(nativePosixSupport, which, delay, interval); + } + @ExportMessage final void signalSelf(int signal, @CachedLibrary("this.nativePosixSupport") PosixSupportLibrary nativeLib) throws PosixException { diff --git a/graalpython/python-libposix/src/posix.c b/graalpython/python-libposix/src/posix.c index f1d47f3c19..5f4d23366f 100644 --- a/graalpython/python-libposix/src/posix.c +++ b/graalpython/python-libposix/src/posix.c @@ -608,6 +608,44 @@ int32_t call_raise(int32_t signal) { return raise(signal); } +int32_t call_alarm(int32_t seconds) { + return alarm(seconds); +} + +static void long_array_to_itimerval(int64_t *src, struct itimerval *dst) { + dst->it_value.tv_sec = src[0]; + dst->it_value.tv_usec = src[1]; + dst->it_interval.tv_sec = src[2]; + dst->it_interval.tv_usec = src[3]; +} + +static void itimerval_to_long_array(struct itimerval *src, int64_t *dst) { + dst[0] = src->it_value.tv_sec; + dst[1] = src->it_value.tv_usec; + dst[2] = src->it_interval.tv_sec; + dst[3] = src->it_interval.tv_usec; +} + +int32_t call_getitimer(int32_t which, int64_t *current_value) { + struct itimerval current; + int32_t result = getitimer(which, ¤t); + if (result == 0) { + itimerval_to_long_array(¤t, current_value); + } + return result; +} + +int32_t call_setitimer(int32_t which, int64_t *new_value, int64_t *old_value) { + struct itimerval new_timer; + struct itimerval old_timer; + long_array_to_itimerval(new_value, &new_timer); + int32_t result = setitimer(which, &new_timer, &old_timer); + if (result == 0) { + itimerval_to_long_array(&old_timer, old_value); + } + return result; +} + int32_t signal_self(int32_t signal) { switch (signal) { case SIGABRT: From c84ee51c0ee3b9bbfe151bc16402f5a293b7f1be Mon Sep 17 00:00:00 2001 From: Michael Simacek Date: Tue, 28 Apr 2026 18:16:07 +0200 Subject: [PATCH 4/5] Add changelog entry --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index ea47a7802b..f9d6ec288f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,6 +18,7 @@ language runtime. The main focus is on user-observable behavior of the engine. * Added support for specifying generics on foreign classes, and inheriting from such classes. Especially when using Java classes that support generics, this allows expressing the generic types in Python type annotations as well. * Added a new `java` backend for the `pyexpat` module that uses a Java XML parser instead of the native `expat` library. It can be useful when running without native access or multiple-context scenarios. This backend is the default when embedding and can be switched back to native `expat` by setting `python.PyExpatModuleBackend` option to `native`. Standalone distribution still defaults to native expat backend. * Add a new context option `python.UnicodeCharacterDatabaseNativeFallback` to control whether the ICU database may fall back to the native unicode character database from CPython for features and characters not supported by ICU. This requires native access to be enabled and is disabled by default for embeddings. +* Add a new context option `python.AllowSignalHandlers` to control whether Python code may install signal handlers. This is disabled by default for Java embedding and enabled in the standalone. * Add an experimental `python.InitializationEntropySource` option to control the entropy source used for initialization-only randomness such as hash secret generation and `random.Random(None)` seeding. This means embeddings and tests can select deterministic or externally provided initialization entropy without affecting cryptographically relevant APIs like `os.urandom()` or `random.SystemRandom()`. * Foreign temporal objects (dates, times, and timezones) are now given a Python class corresponding to their interop traits, i.e., `date`, `time`, `datetime`, or `tzinfo`. This allows any foreign objects with these traits to be used in place of the native Python types and Python methods available on these types work on the foreign types. * Make BouncyCastle an optional dependency for embedding use cases. BouncyCastle is only needed for legacy RSA, DSA, and EC privat keys versions 0 and 1. To support these from Python embeddings, BouncyCastle must now be explicitly enabled by adding the `org.graalvm.python:python-bouncycastle-support` Maven artifact. From f6322457b7637a13734116deb4024364e82dc05c Mon Sep 17 00:00:00 2001 From: Michael Simacek Date: Wed, 29 Apr 2026 17:21:15 +0200 Subject: [PATCH 5/5] Reintroduce emulated alarm and setitimer --- .../src/tests/test_signal.py | 80 ++++++++++- .../modules/SignalModuleBuiltins.java | 15 ++ .../python/runtime/EmulatedPosixSupport.java | 134 ++++++++++++++++-- 3 files changed, 219 insertions(+), 10 deletions(-) diff --git a/graalpython/com.oracle.graal.python.test/src/tests/test_signal.py b/graalpython/com.oracle.graal.python.test/src/tests/test_signal.py index c46b265b5b..a3fdde3fa6 100644 --- a/graalpython/com.oracle.graal.python.test/src/tests/test_signal.py +++ b/graalpython/com.oracle.graal.python.test/src/tests/test_signal.py @@ -1,4 +1,4 @@ -# Copyright (c) 2018, 2024, Oracle and/or its affiliates. All rights reserved. +# Copyright (c) 2018, 2026, Oracle and/or its affiliates. All rights reserved. # DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. # # The Universal Permissive License (UPL), Version 1.0 @@ -82,6 +82,84 @@ def handler(signum, frame): assert False, "Signal handler didn't trigger or propagate exception" +def test_itimer(): + assert signal.getitimer(signal.ITIMER_REAL) == (0.0, 0.0) + try: + old = signal.setitimer(signal.ITIMER_REAL, 1.0, 0.25) + assert old == (0.0, 0.0), old + current = signal.getitimer(signal.ITIMER_REAL) + assert 0.0 <= current[0] <= 1.0, current + assert current[1] == 0.25, current + old = signal.setitimer(signal.ITIMER_REAL, 0) + assert 0.0 <= old[0] <= 1.0, old + assert old[1] == 0.25, old + finally: + signal.setitimer(signal.ITIMER_REAL, 0) + + for func, args in ( + (signal.getitimer, (-1,)), + (signal.setitimer, (-1, 0)), + (signal.setitimer, (signal.ITIMER_REAL, -1)), + ): + try: + func(*args) + except signal.ItimerError as e: + assert e.errno == 22, e + else: + raise AssertionError(f"{func.__name__}{args} did not raise ItimerError") + + +def test_emulated_timers_use_current_handler(): + if sys.implementation.name != 'graalpy' or __graalpython__.posix_module_backend() != 'java': + return + + import time + + calls = [] + + def first(signum, frame): + calls.append(("first", signum, frame)) + + def second(signum, frame): + calls.append(("second", signum, frame)) + + def wait_for_call(timeout=2.5): + deadline = time.time() + timeout + while time.time() < deadline: + if calls: + return True + time.sleep(0.01) + return False + + old_handler = signal.signal(signal.SIGALRM, first) + try: + signal.alarm(1) + signal.signal(signal.SIGALRM, second) + assert wait_for_call(), "alarm did not trigger handler" + assert calls[0][0] == "second", calls + assert calls[0][1] == signal.SIGALRM, calls + assert calls[0][2].f_code.co_name + + calls.clear() + signal.signal(signal.SIGALRM, first) + signal.setitimer(signal.ITIMER_REAL, 0.05) + signal.signal(signal.SIGALRM, second) + assert wait_for_call(), "setitimer did not trigger handler" + assert calls[0][0] == "second", calls + assert calls[0][1] == signal.SIGALRM, calls + assert calls[0][2].f_code.co_name + + calls.clear() + signal.signal(signal.SIGALRM, signal.SIG_IGN) + signal.setitimer(signal.ITIMER_REAL, 0.05) + time.sleep(0.2) + assert calls == [] + finally: + signal.alarm(0) + signal.setitimer(signal.ITIMER_REAL, 0) + signal.signal(signal.SIGALRM, old_handler) + + def test_interrupt(): if sys.implementation.name == 'graalpy' and __graalpython__.posix_module_backend() == 'java': # Sending SIGINT does not work when using the Java backend for posix diff --git a/graalpython/com.oracle.graal.python/src/com/oracle/graal/python/builtins/modules/SignalModuleBuiltins.java b/graalpython/com.oracle.graal.python/src/com/oracle/graal/python/builtins/modules/SignalModuleBuiltins.java index c6fa8b6d04..ac53d06cfc 100644 --- a/graalpython/com.oracle.graal.python/src/com/oracle/graal/python/builtins/modules/SignalModuleBuiltins.java +++ b/graalpython/com.oracle.graal.python/src/com/oracle/graal/python/builtins/modules/SignalModuleBuiltins.java @@ -204,6 +204,21 @@ public static int signalFromName(PythonContext context, String name) { return mod.getModuleState(ModuleData.class).signals.getOrDefault(name, -1); } + @TruffleBoundary + public static void triggerEmulatedSignal(PythonContext context, String name) { + PythonModule mod = context.lookupBuiltinModule(T__SIGNAL); + ModuleData data = mod.getModuleState(ModuleData.class); + if (data == null) { + return; + } + int signum = data.signals.getOrDefault(name, -1); + Object handler = data.signalHandlers.get(signum); + if (handler != null) { + data.signalQueue.add(new SignalTriggerAction(handler, signum)); + data.signalSema.release(); + } + } + public static void resetSignalHandlers(PythonModule mod) { ModuleData data = mod.getModuleState(ModuleData.class); if (data != null) { diff --git a/graalpython/com.oracle.graal.python/src/com/oracle/graal/python/runtime/EmulatedPosixSupport.java b/graalpython/com.oracle.graal.python/src/com/oracle/graal/python/runtime/EmulatedPosixSupport.java index ef92e82946..cabf987a60 100644 --- a/graalpython/com.oracle.graal.python/src/com/oracle/graal/python/runtime/EmulatedPosixSupport.java +++ b/graalpython/com.oracle.graal.python/src/com/oracle/graal/python/runtime/EmulatedPosixSupport.java @@ -44,6 +44,7 @@ import static com.oracle.graal.python.annotations.PythonOS.PLATFORM_LINUX; import static com.oracle.graal.python.annotations.PythonOS.PLATFORM_WIN32; import static com.oracle.graal.python.builtins.modules.SignalModuleBuiltins.signalFromName; +import static com.oracle.graal.python.builtins.modules.SignalModuleBuiltins.triggerEmulatedSignal; import static com.oracle.graal.python.builtins.objects.thread.PThread.getThreadId; import static com.oracle.graal.python.nodes.StringLiterals.T_EMPTY_STRING; import static com.oracle.graal.python.nodes.StringLiterals.T_JAVA; @@ -209,6 +210,9 @@ import java.util.Map; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.ScheduledFuture; import java.util.concurrent.TimeUnit; import java.util.logging.Level; @@ -349,6 +353,10 @@ public final class EmulatedPosixSupport extends PosixResources { private final boolean withoutIOSocket; // Lazily parsed content of /etc/services. private Map> etcServices; + private ScheduledExecutorService itimerService; + private ScheduledFuture itimerFuture; + private long itimerInterval; + private Alarm currentAlarm; public EmulatedPosixSupport(PythonContext context) { super(context); @@ -2021,21 +2029,129 @@ public void raise(int signal) { } @ExportMessage - @SuppressWarnings("static-method") - public int alarm(int seconds) { - throw createUnsupportedFeature("alarm"); + @TruffleBoundary + public synchronized int alarm(int seconds) { + int remaining = 0; + if (currentAlarm != null && currentAlarm.isRunning()) { + remaining = currentAlarm.getRemainingSeconds(); + if (remaining < 0) { + remaining = 0; + } + currentAlarm.cancel(); + } + if (seconds > 0) { + currentAlarm = new Alarm(context, seconds); + currentAlarm.start(); + } else { + currentAlarm = null; + } + return remaining; } @ExportMessage - @SuppressWarnings("static-method") - public Timeval[] getitimer(int which) { - throw createUnsupportedFeature("getitimer"); + @TruffleBoundary + public synchronized Timeval[] getitimer(int which) throws PosixException { + if (which != 0) { + throw posixException(OSErrorEnum.EINVAL); + } + return currentItimer(); } @ExportMessage - @SuppressWarnings("static-method") - public Timeval[] setitimer(int which, Timeval delay, Timeval interval) { - throw createUnsupportedFeature("setitimer"); + @TruffleBoundary + public synchronized Timeval[] setitimer(int which, Timeval delay, Timeval interval) throws PosixException { + if (which != 0 || delay.getSeconds() < 0 || delay.getMicroseconds() < 0 || interval.getSeconds() < 0 || interval.getMicroseconds() < 0) { + throw posixException(OSErrorEnum.EINVAL); + } + Timeval[] oldValue = currentItimer(); + long usDelay = toMicroseconds(delay); + long usInterval = toMicroseconds(interval); + if (itimerFuture != null) { + itimerFuture.cancel(false); + itimerFuture = null; + itimerInterval = 0; + } + if (usDelay != 0) { + itimerInterval = usInterval; + Runnable raiseAlarm = () -> triggerEmulatedSignal(context, "ALRM"); + if (usInterval == 0) { + itimerFuture = getItimerService().schedule(raiseAlarm, usDelay, TimeUnit.MICROSECONDS); + } else { + itimerFuture = getItimerService().scheduleAtFixedRate(raiseAlarm, usDelay, usInterval, TimeUnit.MICROSECONDS); + } + } + return oldValue; + } + + private Timeval[] currentItimer() { + return new Timeval[]{fromMicroseconds(currentItimerDelay()), fromMicroseconds(itimerInterval)}; + } + + private long currentItimerDelay() { + if (itimerFuture == null) { + return 0; + } + long delay = itimerFuture.getDelay(TimeUnit.MICROSECONDS); + return Math.max(delay, 0); + } + + private ScheduledExecutorService getItimerService() { + if (itimerService == null) { + ScheduledExecutorService newItimerService = Executors.newSingleThreadScheduledExecutor(runnable -> { + Thread thread = Executors.defaultThreadFactory().newThread(runnable); + thread.setDaemon(true); + return thread; + }); + itimerService = newItimerService; + context.registerAtexitHook(ctx -> newItimerService.shutdown()); + } + return itimerService; + } + + private static long toMicroseconds(Timeval timeval) { + return TimeUnit.SECONDS.toMicros(timeval.getSeconds()) + timeval.getMicroseconds(); + } + + private static Timeval fromMicroseconds(long microseconds) { + return new Timeval(microseconds / TimeUnit.SECONDS.toMicros(1), microseconds % TimeUnit.SECONDS.toMicros(1)); + } + + private static final class Alarm { + private final PythonContext context; + private final int seconds; + private final long startMillis; + private Thread thread; + + Alarm(PythonContext context, int seconds) { + this.context = context; + this.seconds = seconds; + this.startMillis = System.currentTimeMillis(); + } + + void start() { + thread = new Thread(() -> { + try { + Thread.sleep(TimeUnit.SECONDS.toMillis(seconds)); + triggerEmulatedSignal(context, "ALRM"); + } catch (InterruptedException e) { + // Cancelled + } + }); + thread.setDaemon(true); + thread.start(); + } + + boolean isRunning() { + return thread.isAlive(); + } + + void cancel() { + thread.interrupt(); + } + + int getRemainingSeconds() { + return seconds - (int) TimeUnit.MILLISECONDS.toSeconds(System.currentTimeMillis() - startMillis); + } } @ExportMessage