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 4a0788fa73..c726d20192 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 @@ -183,3 +183,77 @@ def test_interrupt(): assert proc.wait() != 0 assert b'KeyboardInterrupt' in stderr, f"Unexpected stderr: {stderr}" assert b'Traceback' in stderr, f"Unexpected stderr: {stderr}" + + +@unittest.skipUnless(sys.platform.startswith("linux"), "needs POSIX signals and select.poll") +class WakeupFdTests(unittest.TestCase): + """signal.set_wakeup_fd must wake a native poll() that would otherwise never return. + + asyncio relies on this for SIGCHLD: with no pipes to a subprocess, the only + thing that can wake the event loop is the byte the signal handler writes to + the wakeup fd. Without it, the Python-level handler only runs when the main + thread leaves poll(), which only happens if the kernel delivers the signal + to that very thread. + """ + + def setUp(self): + import os + self.r, self.w = os.pipe() + os.set_blocking(self.w, False) + self.old_fd = signal.set_wakeup_fd(self.w) + self.old_handler = signal.signal(signal.SIGALRM, lambda *args: None) + + def tearDown(self): + import os + signal.signal(signal.SIGALRM, self.old_handler) + signal.set_wakeup_fd(self.old_fd) + os.close(self.r) + os.close(self.w) + + def test_set_wakeup_fd_result_and_validation(self): + import os + self.assertEqual(signal.set_wakeup_fd(self.w), self.w) + with self.assertRaisesRegex(ValueError, "invalid fd"): + signal.set_wakeup_fd(10 ** 6) + r, w = os.pipe() + try: + with self.assertRaisesRegex(ValueError, "non-blocking"): + signal.set_wakeup_fd(w) + finally: + os.close(r) + os.close(w) + self.assertEqual(signal.set_wakeup_fd(self.w), self.w) + + def test_wakeup_byte_wakes_blocking_poll(self): + import os + import select + poller = select.poll() + poller.register(self.r, select.POLLIN) + # Delivered asynchronously while the main thread is blocked in poll() + # with no timeout; the poll returns only if the wakeup byte is written. + signal.setitimer(signal.ITIMER_REAL, 0.2) + try: + events = poller.poll() + finally: + signal.setitimer(signal.ITIMER_REAL, 0) + self.assertEqual([(self.r, select.POLLIN)], events) + self.assertEqual(bytes([signal.SIGALRM]), os.read(self.r, 1)) + + def test_handler_runs_after_wakeup(self): + import select + import time + seen = [] + signal.signal(signal.SIGALRM, lambda signum, frame: seen.append(signum)) + poller = select.poll() + poller.register(self.r, select.POLLIN) + signal.setitimer(signal.ITIMER_REAL, 0.2) + try: + poller.poll() + # the handler runs once the main thread reaches a safepoint + for _ in range(100): + if seen: + break + time.sleep(0.01) + finally: + signal.setitimer(signal.ITIMER_REAL, 0) + self.assertEqual([signal.SIGALRM], seen) diff --git a/graalpython/com.oracle.graal.python.test/src/tests/unittest_tags/test_signal.txt b/graalpython/com.oracle.graal.python.test/src/tests/unittest_tags/test_signal.txt index 8e71d28367..b974682e7c 100644 --- a/graalpython/com.oracle.graal.python.test/src/tests/unittest_tags/test_signal.txt +++ b/graalpython/com.oracle.graal.python.test/src/tests/unittest_tags/test_signal.txt @@ -14,5 +14,15 @@ test.test_signal.RaiseSignalTest.test_handler @ darwin-arm64,linux-aarch64,linux test.test_signal.SiginterruptTest.test_siginterrupt_off @ darwin-arm64,linux-aarch64,linux-aarch64-github,linux-x86_64,linux-x86_64-github # Timeout !test.test_signal.StressTest.test_stress_modifying_handlers +test.test_signal.WakeupFDTests.test_invalid_call @ linux-aarch64,linux-aarch64-github,linux-x86_64,linux-x86_64-github +test.test_signal.WakeupFDTests.test_invalid_fd @ linux-aarch64,linux-aarch64-github,linux-x86_64,linux-x86_64-github +test.test_signal.WakeupFDTests.test_invalid_socket @ linux-aarch64,linux-aarch64-github,linux-x86_64,linux-x86_64-github +test.test_signal.WakeupFDTests.test_set_wakeup_fd_blocking @ linux-aarch64,linux-aarch64-github,linux-x86_64,linux-x86_64-github +test.test_signal.WakeupFDTests.test_set_wakeup_fd_result @ linux-aarch64,linux-aarch64-github,linux-x86_64,linux-x86_64-github +test.test_signal.WakeupFDTests.test_set_wakeup_fd_socket_result @ linux-aarch64,linux-aarch64-github,linux-x86_64,linux-x86_64-github +test.test_signal.WakeupSignalTests.test_signum @ linux-aarch64,linux-aarch64-github,linux-x86_64,linux-x86_64-github +test.test_signal.WakeupSignalTests.test_wakeup_fd_during @ linux-aarch64,linux-aarch64-github,linux-x86_64,linux-x86_64-github +test.test_signal.WakeupSignalTests.test_wakeup_fd_early @ linux-aarch64,linux-aarch64-github,linux-x86_64,linux-x86_64-github +test.test_signal.WakeupSignalTests.test_wakeup_write_error @ linux-aarch64,linux-aarch64-github,linux-x86_64,linux-x86_64-github test.test_signal.WindowsSignalTests.test_issue9324 @ win32-AMD64,win32-AMD64-github test.test_signal.WindowsSignalTests.test_valid_signals @ win32-AMD64,win32-AMD64-github diff --git a/graalpython/com.oracle.graal.python/src/com/oracle/graal/python/builtins/modules/SelectModuleBuiltins.java b/graalpython/com.oracle.graal.python/src/com/oracle/graal/python/builtins/modules/SelectModuleBuiltins.java index 9a621b1fe4..ae1865add1 100644 --- a/graalpython/com.oracle.graal.python/src/com/oracle/graal/python/builtins/modules/SelectModuleBuiltins.java +++ b/graalpython/com.oracle.graal.python/src/com/oracle/graal/python/builtins/modules/SelectModuleBuiltins.java @@ -168,6 +168,8 @@ static PTuple doGeneric(VirtualFrame frame, Object rlist, Object wlist, Object x } finally { gil.acquire(); } + // a signal (e.g. one whose wakeup byte woke us) may have a handler pending + SignalModuleBuiltins.runPendingSignalHandlers(inliningTarget, PythonContext.get(inliningTarget)); break; } catch (ChannelNotSelectableException e) { // GraalPython hack: if one of the channels is not selectable (can happen only 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 1d4a767c20..9b042d4093 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 @@ -52,6 +52,9 @@ import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentLinkedDeque; import java.util.concurrent.Semaphore; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicLong; +import java.util.concurrent.atomic.AtomicInteger; import com.oracle.graal.python.PythonLanguage; import com.oracle.graal.python.annotations.ArgumentClinic; @@ -72,6 +75,7 @@ import com.oracle.graal.python.nodes.ErrorMessages; import com.oracle.graal.python.nodes.PConstructAndRaiseNode; import com.oracle.graal.python.nodes.PRaiseNode; +import com.oracle.graal.python.nodes.WriteUnraisableNode; 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; @@ -96,6 +100,9 @@ import com.oracle.truffle.api.dsl.Cached; import com.oracle.truffle.api.dsl.GenerateNodeFactory; import com.oracle.truffle.api.dsl.NodeFactory; +import com.oracle.truffle.api.ThreadLocalAction; +import com.oracle.truffle.api.TruffleSafepoint; +import com.oracle.truffle.api.TruffleLogger; import com.oracle.truffle.api.dsl.Specialization; import com.oracle.truffle.api.frame.VirtualFrame; import com.oracle.truffle.api.nodes.Node; @@ -106,6 +113,8 @@ @CoreFunctions(defineModule = "_signal", isEager = true) public final class SignalModuleBuiltins extends PythonBuiltins { + + private static final TruffleLogger LOGGER = PythonLanguage.getLogger(SignalModuleBuiltins.class); private static final int ITIMER_REAL = 0; private static final int ITIMER_VIRTUAL = 1; private static final int ITIMER_PROF = 2; @@ -181,7 +190,7 @@ public void postInitialize(Python3Core core) { } context.registerAsyncAction(() -> { - SignalTriggerAction poll = moduleData.signalQueue.poll(); + AsyncHandler.AsyncAction poll = moduleData.signalQueue.poll(); if (PythonOptions.AUTOMATIC_ASYNC_ACTIONS) { try { while (poll == null) { @@ -202,6 +211,36 @@ public void postInitialize(Python3Core core) { } } + /** + * Runs Python signal handlers whose signal has already been received but whose handler has not + * run yet on the main thread. CPython runs pending handlers when a blocking call such as + * {@code select()} returns, so that a handler's exception interrupts the call; here the handler + * is submitted to the main thread by another thread, so this waits (bounded) for that to happen. + * Cheap when no signal is pending, which is the common case. + */ + @TruffleBoundary + public static void runPendingSignalHandlers(Node node, PythonContext context) { + if (Thread.currentThread() != context.getMainThread()) { + return; + } + PythonModule mod = context.lookupBuiltinModule(T__SIGNAL); + ModuleData data = mod == null ? null : mod.getModuleState(ModuleData.class); + if (data == null || data.pendingHandlers.get() == 0) { + return; + } + long deadline = System.nanoTime() + TimeUnit.MILLISECONDS.toNanos(100); + while (data.pendingHandlers.get() > 0 && System.nanoTime() < deadline) { + TruffleSafepoint.poll(node); + try { + Thread.sleep(1); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + return; + } + } + TruffleSafepoint.poll(node); + } + @TruffleBoundary public static int signalFromName(PythonContext context, String name) { PythonModule mod = context.lookupBuiltinModule(T__SIGNAL); @@ -218,8 +257,7 @@ public static void triggerEmulatedSignal(PythonContext context, String name) { 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(); + data.trip(context, new SignalTriggerAction(handler, signum, data.pendingHandlers)); } } @@ -242,14 +280,18 @@ public static void resetSignalHandlers(PythonModule mod) { private static class SignalTriggerAction extends AsyncHandler.AsyncPythonAction { private final Object callableObject; private final int signum; + private final AtomicInteger pendingHandlers; - SignalTriggerAction(Object callable, int signum) { + SignalTriggerAction(Object callable, int signum, AtomicInteger pendingHandlers) { this.callableObject = callable; this.signum = signum; + this.pendingHandlers = pendingHandlers; } @Override public Object callable() { + // called once, on the main thread, when the handler is about to run + pendingHandlers.decrementAndGet(); return callableObject; } @@ -399,12 +441,11 @@ private static Object signalHandlerBoundary(PythonModule self, Object handler, N static Object signal(Node raisingNode, int signum, Object handler, ModuleData data) { CompilerAsserts.neverPartOfCompilation(); SignalHandler oldHandler; - SignalTriggerAction signalTrigger = new SignalTriggerAction(handler, signum); + SignalTriggerAction signalTrigger = new SignalTriggerAction(handler, signum, data.pendingHandlers); + PythonContext context = PythonContext.get(null); try { - oldHandler = Signals.setSignalHandler(signum, () -> { - data.signalQueue.add(signalTrigger); - data.signalSema.release(); - }); + // runs on the JVM's signal dispatcher thread + oldHandler = Signals.setSignalHandler(signum, () -> data.trip(context, signalTrigger)); } catch (IllegalArgumentException e) { throw PRaiseNode.raiseStatic(raisingNode, PythonErrorType.ValueError, e); } @@ -431,14 +472,53 @@ private static Object signal(Node raisingNode, int signum, int id, ModuleData da } } - @Builtin(name = "set_wakeup_fd", minNumOfPositionalArgs = 1, numOfPositionalOnlyArgs = 1, parameterNames = {"fd", "warn_on_full_buffer"}) + @Builtin(name = "set_wakeup_fd", minNumOfPositionalArgs = 2, numOfPositionalOnlyArgs = 2, parameterNames = {"$self", "fd"}, keywordOnlyNames = {"warn_on_full_buffer"}, declaresExplicitSelf = true) + @ArgumentClinic(name = "fd", conversion = ArgumentClinic.ClinicConversion.Int) + @ArgumentClinic(name = "warn_on_full_buffer", conversion = ArgumentClinic.ClinicConversion.Boolean, defaultValue = "true") @GenerateNodeFactory - abstract static class SetWakeupFdNode extends PythonBuiltinNode { + abstract static class SetWakeupFdNode extends PythonTernaryClinicBuiltinNode { + private static final TruffleString T_FD_MUST_BE_NON_BLOCKING = tsLiteral("the fd %d must be in non-blocking mode"); + @Specialization - @SuppressWarnings("unused") - static int doGeneric(Object fd, Object warnOnFullBuffer) { - // TODO: implement - return -1; + static int doInt(VirtualFrame frame, PythonModule self, int fd, boolean warnOnFullBuffer, + @Bind Node inliningTarget, + @Bind PythonContext context, + @Cached PConstructAndRaiseNode.Lazy constructAndRaiseNode, + @Cached PRaiseNode raiseNode) { + if (Thread.currentThread() != context.getMainThread()) { + throw raiseNode.raise(inliningTarget, PythonErrorType.ValueError, ErrorMessages.S, "set_wakeup_fd only works in main thread of the main interpreter"); + } + if (fd != -1) { + try { + context.getPosixSupport().fstat(fd); + } catch (PosixException e) { + throw raiseNode.raise(inliningTarget, PythonErrorType.ValueError, ErrorMessages.S, "invalid fd"); + } + boolean blocking; + try { + blocking = context.getPosixSupport().getBlocking(fd); + } catch (PosixException e) { + throw constructAndRaiseNode.get(inliningTarget).raiseOSErrorFromPosixException(frame, e); + } + if (blocking) { + throw raiseNode.raise(inliningTarget, PythonErrorType.ValueError, T_FD_MUST_BE_NON_BLOCKING, fd); + } + } + return setWakeupFd(self, fd, warnOnFullBuffer); + } + + @TruffleBoundary + private static int setWakeupFd(PythonModule self, int fd, boolean warnOnFullBuffer) { + ModuleData data = self.getModuleState(ModuleData.class); + int oldFd = data.wakeupFd; + data.wakeupFd = fd; + data.wakeupWarnOnFullBuffer = warnOnFullBuffer; + return oldFd; + } + + @Override + protected ArgumentClinicProvider getArgumentClinic() { + return SignalModuleBuiltinsClinicProviders.SetWakeupFdNodeClinicProviderGen.INSTANCE; } } @@ -472,14 +552,52 @@ static PNone doInt(VirtualFrame frame, int signum, @Bind PythonContext context, @Bind Node inliningTarget, @Cached PConstructAndRaiseNode.Lazy constructAndRaiseNode) { + long receivedBefore = receivedBeforeRaise(context, signum); try { context.getPosixSupport().raise(signum); } catch (PosixException e) { throw constructAndRaiseNode.get(inliningTarget).raiseOSErrorFromPosixException(frame, e); } + if (receivedBefore >= 0) { + waitForHandler(inliningTarget, context, receivedBefore); + } return PNone.NONE; } + /** + * @return the number of signals received so far if a Python handler is installed for + * {@code signum}, -1 otherwise + */ + @TruffleBoundary + private static long receivedBeforeRaise(PythonContext context, int signum) { + ModuleData data = context.lookupBuiltinModule(T__SIGNAL).getModuleState(ModuleData.class); + return data.signalHandlers.containsKey(signum) ? data.received.get() : -1; + } + + /** + * CPython runs the Python handler of a signal raised in the process before + * {@code raise_signal} returns. The JVM dispatches signals on a separate thread, so wait + * (bounded) until the signal arrived there and its handler ran at a safepoint here; the + * handler's exception, if any, propagates out of {@link TruffleSafepoint#poll}. + */ + @TruffleBoundary + private static void waitForHandler(Node node, PythonContext context, long receivedBefore) { + ModuleData data = context.lookupBuiltinModule(T__SIGNAL).getModuleState(ModuleData.class); + long deadline = System.nanoTime() + TimeUnit.SECONDS.toNanos(2); + while (System.nanoTime() < deadline) { + TruffleSafepoint.poll(node); + if (data.received.get() > receivedBefore && data.pendingHandlers.get() == 0) { + return; + } + try { + Thread.sleep(1); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + return; + } + } + } + @Override protected ArgumentClinicProvider getArgumentClinic() { return SignalModuleBuiltinsClinicProviders.RaiseSignalNodeClinicProviderGen.INSTANCE; @@ -565,8 +683,68 @@ private static final class ModuleData { final Map signals = new HashMap<>(); final ConcurrentHashMap signalHandlers = new ConcurrentHashMap<>(); final ConcurrentHashMap defaultSignalHandlers = new ConcurrentHashMap<>(); - final ConcurrentLinkedDeque signalQueue = new ConcurrentLinkedDeque<>(); + final ConcurrentLinkedDeque signalQueue = new ConcurrentLinkedDeque<>(); final Semaphore signalSema = new Semaphore(0); + /** + * The fd set with {@code signal.set_wakeup_fd}, or -1. Written from the thread that + * receives the signal, see {@link #trip(PythonContext, SignalTriggerAction)}. + */ + volatile int wakeupFd = -1; + volatile boolean wakeupWarnOnFullBuffer = true; + /** Signals received by the Java-level handlers, see {@link RaiseSignalNode}. */ + final AtomicLong received = new AtomicLong(); + /** Python handlers queued but not yet run on the main thread. */ + final AtomicInteger pendingHandlers = new AtomicInteger(); + + /** + * Queues the Python-level handler for execution as an async action and, as CPython's C + * signal handler does, writes the signal number to the wakeup fd. The Python handler only + * runs once the main thread reaches a safepoint, which does not happen while it is blocked + * in a native call such as {@code poll()}: the wakeup byte is what makes such a call return + * (asyncio relies on it to wake its event loop for SIGCHLD). + */ + void trip(PythonContext context, SignalTriggerAction trigger) { + received.incrementAndGet(); + pendingHandlers.incrementAndGet(); + // The wakeup byte goes first, as in CPython's trip_signal(): a failed write is + // reported from the main thread before the handler runs. + int fd = wakeupFd; + if (fd != -1) { + int errno = context.getPosixSupport().writeSignalWakeupByte(fd, (byte) trigger.signum); + if (errno != 0) { + boolean fullBuffer = errno == OSErrorEnum.EAGAIN.getNumber() || errno == OSErrorEnum.EWOULDBLOCK.getNumber(); + if (!fullBuffer || wakeupWarnOnFullBuffer) { + signalQueue.add(new WakeupWriteErrorAction(errno)); + signalSema.release(); + } + } + } + signalQueue.add(trigger); + signalSema.release(); + } + } + + /** + * Reports a failed write to the wakeup fd through {@code sys.unraisablehook}, from the main + * thread (the error cannot be raised in the thread that received the signal). + */ + private static final class WakeupWriteErrorAction implements AsyncHandler.AsyncAction { + private static final TruffleString T_WAKEUP_WRITE_ERROR = tsLiteral("Exception ignored when trying to write to the signal wakeup fd"); + private final int errno; + + WakeupWriteErrorAction(int errno) { + this.errno = errno; + } + + @Override + public void execute(PythonContext context, ThreadLocalAction.Access access) { + try { + TruffleString message = context.getPosixSupport().strerror(errno); + throw PConstructAndRaiseNode.getUncached().raiseOSError(null, errno, message, PNone.NONE); + } catch (PException e) { + WriteUnraisableNode.getUncached().execute(e.getUnreifiedException(), T_WAKEUP_WRITE_ERROR, PNone.NONE); + } + } } } diff --git a/graalpython/com.oracle.graal.python/src/com/oracle/graal/python/builtins/objects/select/PollBuiltins.java b/graalpython/com.oracle.graal.python/src/com/oracle/graal/python/builtins/objects/select/PollBuiltins.java index 97bec5dc30..8227095d23 100644 --- a/graalpython/com.oracle.graal.python/src/com/oracle/graal/python/builtins/objects/select/PollBuiltins.java +++ b/graalpython/com.oracle.graal.python/src/com/oracle/graal/python/builtins/objects/select/PollBuiltins.java @@ -54,6 +54,7 @@ import com.oracle.graal.python.builtins.CoreFunctions; import com.oracle.graal.python.builtins.PythonBuiltinClassType; import com.oracle.graal.python.builtins.PythonBuiltins; +import com.oracle.graal.python.builtins.modules.SignalModuleBuiltins; import com.oracle.graal.python.builtins.objects.PNone; import com.oracle.graal.python.builtins.objects.exception.OSErrorEnum; import com.oracle.graal.python.builtins.objects.list.PList; @@ -194,6 +195,8 @@ static PList poll(VirtualFrame frame, PPoll self, Object timeoutObject, } finally { gil.acquire(); } + // a signal (e.g. one whose wakeup byte woke us) may have a handler pending + SignalModuleBuiltins.runPendingSignalHandlers(inliningTarget, PythonContext.get(inliningTarget)); break; } catch (PosixException e) { if (!e.hasErrno(OSErrorEnum.EINTR)) { 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 17f02f7a88..de4e1626bd 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 @@ -183,6 +183,12 @@ public final Buffer read(int fd, long length) throws PosixException { } } + @Override + @TruffleBoundary + public int writeSignalWakeupByte(int fd, byte value) { + return delegate.writeSignalWakeupByte(fd, value); + } + @Override @TruffleBoundary public final long write(int fd, Buffer data) throws PosixException { diff --git a/graalpython/com.oracle.graal.python/src/com/oracle/graal/python/runtime/NativePosixSupport.java b/graalpython/com.oracle.graal.python/src/com/oracle/graal/python/runtime/NativePosixSupport.java index b7da1d6cc2..5514ae7371 100644 --- a/graalpython/com.oracle.graal.python/src/com/oracle/graal/python/runtime/NativePosixSupport.java +++ b/graalpython/com.oracle.graal.python/src/com/oracle/graal/python/runtime/NativePosixSupport.java @@ -3948,4 +3948,16 @@ private static void checkBounds(byte[] buf, int offset, int length) { private int getErrno() { return posixNativeFunctionInvoker.get_errno(); } + + @Override + @TruffleBoundary + public int writeSignalWakeupByte(int fd, byte value) { + long nativeBuffer = NativeMemory.mallocByteArrayOrNull(1); + try { + NativeMemory.writeByteArrayElements(nativeBuffer, 0, new byte[]{value}, 0, 1); + return posixNativeFunctionInvoker.call_write(fd, nativeBuffer, 1) < 0 ? getErrno() : 0; + } finally { + NativeMemory.free(nativeBuffer); + } + } } diff --git a/graalpython/com.oracle.graal.python/src/com/oracle/graal/python/runtime/PosixSupport.java b/graalpython/com.oracle.graal.python/src/com/oracle/graal/python/runtime/PosixSupport.java index 97a5f718e1..0d7e9630ab 100644 --- a/graalpython/com.oracle.graal.python/src/com/oracle/graal/python/runtime/PosixSupport.java +++ b/graalpython/com.oracle.graal.python/src/com/oracle/graal/python/runtime/PosixSupport.java @@ -661,6 +661,26 @@ public void setEnv(@SuppressWarnings("unused") Env env) { // nop } + /** + * Writes a single byte to {@code fd} for {@code signal.set_wakeup_fd}. Unlike + * {@link #write(int, Buffer)}, this is safe to call from a thread that has not entered the + * context (the JVM's signal dispatcher thread): it does not create Python-level exceptions or + * {@link com.oracle.truffle.api.strings.TruffleString}s from native memory. + * + * @return 0 on success, the errno otherwise + */ + @TruffleBoundary + public int writeSignalWakeupByte(int fd, byte value) { + try { + write(fd, Buffer.wrap(new byte[]{value})); + return 0; + } catch (PosixErrnoException e) { + return e.getErrorCode(); + } catch (PosixException e) { + return OSErrorEnum.EIO.getNumber(); + } + } + public static PosixSupport get(Node node) { return PythonContext.get(node).getPosixSupport(); } 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 993e875763..b7727dab58 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 @@ -190,6 +190,13 @@ public final Buffer read(int fd, long length) throws PosixException { return nativePosixSupport.read(fd, length); } + @Override + @TruffleBoundary + public int writeSignalWakeupByte(int fd, byte value) { + checkNotInPreInitialization(); + return nativePosixSupport.writeSignalWakeupByte(fd, value); + } + @Override @TruffleBoundary public final long write(int fd, Buffer data) throws PosixException {