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. 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.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/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 82c1a457ec..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 @@ -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 @@ -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,14 +74,17 @@ 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; 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.Timeval; +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 +98,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; @@ -109,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); @@ -154,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]; @@ -188,7 +191,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); @@ -201,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) { @@ -266,31 +284,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 @@ -351,7 +361,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); } @@ -448,9 +463,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; } @@ -460,10 +484,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() { @@ -471,68 +495,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() { @@ -540,33 +545,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; } } @@ -576,10 +567,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; } } @@ -614,46 +601,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 88ec3a30f9..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); @@ -2014,6 +2022,138 @@ public void kill(long pid, int signal, } } + @ExportMessage + @SuppressWarnings("static-method") + public void raise(int signal) { + throw createUnsupportedFeature("raise"); + } + + @ExportMessage + @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 + @TruffleBoundary + public synchronized Timeval[] getitimer(int which) throws PosixException { + if (which != 0) { + throw posixException(OSErrorEnum.EINVAL); + } + return currentItimer(); + } + + @ExportMessage + @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 @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..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 @@ -752,6 +752,50 @@ 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 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 1bf99341f3..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 @@ -230,6 +230,10 @@ private enum PosixNativeFunction { get_blocking("(sint32):sint32"), 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"), @@ -1166,6 +1170,43 @@ 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 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 { @@ -2701,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 5ad6aaac3e..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 @@ -258,6 +258,14 @@ 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 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 9b36dc9fd0..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 @@ -588,6 +588,34 @@ 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 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/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<>(""); diff --git a/graalpython/python-libposix/src/posix.c b/graalpython/python-libposix/src/posix.c index 5d6d863e95..5f4d23366f 100644 --- a/graalpython/python-libposix/src/posix.c +++ b/graalpython/python-libposix/src/posix.c @@ -604,6 +604,48 @@ 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 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: