-
Notifications
You must be signed in to change notification settings - Fork 243
feat: modernize signal handling via Panama FFM sigaction() #1750
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 2 commits
6e0fd88
4ce73fa
9402976
8e38d02
e2d632b
c5343d3
87bb61a
45f3ce6
9b04757
6247f1d
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,395 @@ | ||
| /* | ||
| * Copyright (c) the original author(s). | ||
| * | ||
| * This software is distributable under the BSD license. See the terms of the | ||
| * BSD license in the documentation provided with this software. | ||
| * | ||
| * https://opensource.org/licenses/BSD-3-Clause | ||
| */ | ||
| package org.jline.terminal.impl.ffm; | ||
|
|
||
| import java.lang.foreign.*; | ||
| import java.lang.invoke.MethodHandle; | ||
| import java.lang.invoke.MethodHandles; | ||
| import java.lang.invoke.MethodType; | ||
| import java.lang.invoke.VarHandle; | ||
| import java.util.Map; | ||
| import java.util.Optional; | ||
| import java.util.concurrent.ConcurrentHashMap; | ||
| import java.util.concurrent.atomic.AtomicIntegerArray; | ||
| import java.util.concurrent.locks.LockSupport; | ||
| import java.util.logging.Level; | ||
| import java.util.logging.Logger; | ||
|
|
||
| /** | ||
| * Native signal handling via POSIX {@code sigaction()} using the Foreign Function & Memory API. | ||
| * | ||
| * <p>This class replaces the reflection-based {@code sun.misc.Signal} approach with direct | ||
| * {@code sigaction()} calls, providing:</p> | ||
| * <ul> | ||
| * <li>No dependency on internal JVM APIs ({@code sun.misc.Signal})</li> | ||
| * <li>{@code SA_RESTART} flag to automatically restart interrupted system calls</li> | ||
| * <li>Arena-scoped handler lifetime</li> | ||
| * </ul> | ||
| * | ||
| * <p>Signal safety is achieved by having the native signal handler only set an atomic flag. | ||
| * A daemon dispatcher thread polls these flags and invokes the registered Java handlers | ||
| * in a safe context.</p> | ||
| */ | ||
| @SuppressWarnings("restricted") | ||
| class FfmSignalHandler { | ||
|
|
||
| private static final Logger logger = Logger.getLogger("org.jline"); | ||
|
|
||
| // --- Struct field name constants (shared across platform layouts) --- | ||
| private static final String SA_HANDLER = "sa_handler"; | ||
| private static final String SA_MASK = "sa_mask"; | ||
| private static final String SA_FLAGS = "sa_flags"; | ||
|
|
||
| // --- Platform-specific signal constants --- | ||
| private static final int SIGHUP; | ||
| private static final int SIGINT; | ||
| private static final int SIGQUIT; | ||
| private static final int SIGTERM; | ||
| private static final int SIGTSTP; | ||
| private static final int SIGCONT; | ||
| private static final int SIGINFO; | ||
| private static final int SIGWINCH; | ||
| private static final int SA_RESTART; | ||
|
|
||
| // --- sigaction struct layout and field accessors --- | ||
| private static final GroupLayout sigactionLayout; | ||
| private static final VarHandle sa_handler_vh; | ||
| private static final VarHandle sa_flags_vh; | ||
|
|
||
| // --- FFM method handle for sigaction() --- | ||
| private static final MethodHandle sigaction_mh; | ||
|
|
||
| // --- Shared upcall stub for all signals --- | ||
| private static final MemorySegment upcallStub; | ||
|
|
||
| // --- Whether FFM signal handling is available on this platform --- | ||
| private static final boolean available; | ||
|
Check failure on line 72 in terminal-ffm/src/main/java/org/jline/terminal/impl/ffm/FfmSignalHandler.java
|
||
|
|
||
| static { | ||
| boolean avail = false; | ||
| GroupLayout layout = null; | ||
| VarHandle saHandler = null; | ||
| VarHandle saFlags = null; | ||
| MethodHandle sigaction = null; | ||
| MemorySegment stub = null; | ||
|
|
||
| int sighup = -1; | ||
| int sigint = -1; | ||
| int sigquit = -1; | ||
| int sigterm = -1; | ||
| int sigtstp = -1; | ||
| int sigcont = -1; | ||
| int siginfo = -1; | ||
| int sigwinch = -1; | ||
| int saRestart = 0; | ||
|
|
||
| try { | ||
| String osName = System.getProperty("os.name"); | ||
|
|
||
| if (osName.startsWith("Mac") || osName.startsWith("Darwin")) { | ||
| sighup = 1; | ||
| sigint = 2; | ||
| sigquit = 3; | ||
| sigterm = 15; | ||
| sigtstp = 18; | ||
| sigcont = 19; | ||
| siginfo = 29; | ||
| sigwinch = 28; | ||
| saRestart = 0x0002; | ||
|
|
||
| layout = MemoryLayout.structLayout( | ||
| ValueLayout.ADDRESS.withName(SA_HANDLER), | ||
| ValueLayout.JAVA_INT.withName(SA_MASK), | ||
| ValueLayout.JAVA_INT.withName(SA_FLAGS)); | ||
| } else if (osName.startsWith("Linux")) { | ||
| sighup = 1; | ||
| sigint = 2; | ||
| sigquit = 3; | ||
| sigterm = 15; | ||
| sigtstp = 20; | ||
| sigcont = 18; | ||
| siginfo = -1; | ||
| sigwinch = 28; | ||
| saRestart = 0x10000000; | ||
|
|
||
| layout = MemoryLayout.structLayout( | ||
| ValueLayout.ADDRESS.withName(SA_HANDLER), | ||
| MemoryLayout.sequenceLayout(128, ValueLayout.JAVA_BYTE).withName(SA_MASK), | ||
| ValueLayout.JAVA_INT.withName(SA_FLAGS), | ||
| MemoryLayout.paddingLayout(4), | ||
| ValueLayout.ADDRESS.withName("sa_restorer")); | ||
| } else if (osName.startsWith("FreeBSD")) { | ||
| sighup = 1; | ||
| sigint = 2; | ||
| sigquit = 3; | ||
| sigterm = 15; | ||
| sigtstp = 18; | ||
| sigcont = 19; | ||
| siginfo = 29; | ||
| sigwinch = 28; | ||
| saRestart = 0x0002; | ||
|
|
||
| layout = MemoryLayout.structLayout( | ||
| ValueLayout.ADDRESS.withName(SA_HANDLER), | ||
| ValueLayout.JAVA_INT.withName(SA_FLAGS), | ||
| MemoryLayout.sequenceLayout(16, ValueLayout.JAVA_BYTE).withName(SA_MASK), | ||
| MemoryLayout.paddingLayout(4)); | ||
| } | ||
|
|
||
| if (layout != null) { | ||
| saHandler = | ||
| FfmTerminalProvider.lookupVarHandle(layout, MemoryLayout.PathElement.groupElement(SA_HANDLER)); | ||
| saFlags = FfmTerminalProvider.lookupVarHandle(layout, MemoryLayout.PathElement.groupElement(SA_FLAGS)); | ||
|
|
||
| Linker linker = Linker.nativeLinker(); | ||
| SymbolLookup lookup = SymbolLookup.loaderLookup().or(linker.defaultLookup()); | ||
|
|
||
| Optional<MemorySegment> sigactionAddr = lookup.find("sigaction"); | ||
| if (sigactionAddr.isPresent()) { | ||
| sigaction = linker.downcallHandle( | ||
| sigactionAddr.get(), | ||
| FunctionDescriptor.of( | ||
| ValueLayout.JAVA_INT, | ||
| ValueLayout.JAVA_INT, | ||
| ValueLayout.ADDRESS, | ||
| ValueLayout.ADDRESS)); | ||
|
|
||
| stub = linker.upcallStub( | ||
| MethodHandles.lookup() | ||
| .findStatic( | ||
| FfmSignalHandler.class, | ||
| "signalReceived", | ||
| MethodType.methodType(void.class, int.class)), | ||
| FunctionDescriptor.ofVoid(ValueLayout.JAVA_INT), | ||
| Arena.global()); | ||
|
|
||
| avail = true; | ||
| } | ||
| } | ||
| } catch (Exception | LinkageError t) { | ||
| logger.log(Level.FINE, "FFM signal handler not available", t); | ||
| } | ||
|
|
||
| SIGHUP = sighup; | ||
| SIGINT = sigint; | ||
| SIGQUIT = sigquit; | ||
| SIGTERM = sigterm; | ||
| SIGTSTP = sigtstp; | ||
| SIGCONT = sigcont; | ||
| SIGINFO = siginfo; | ||
| SIGWINCH = sigwinch; | ||
| SA_RESTART = saRestart; | ||
|
|
||
| sigactionLayout = layout; | ||
| sa_handler_vh = saHandler; | ||
| sa_flags_vh = saFlags; | ||
| sigaction_mh = sigaction; | ||
| upcallStub = stub; | ||
| available = avail; | ||
| } | ||
|
|
||
| // --- Signal dispatch infrastructure --- | ||
|
|
||
| /** Atomic flags: pendingSignals[signum] == 1 means a signal is pending dispatch. */ | ||
| private static final AtomicIntegerArray pendingSignals = new AtomicIntegerArray(64); | ||
|
|
||
| /** Registered Java handlers, keyed by signal number. */ | ||
| private static final Map<Integer, Runnable> handlers = new ConcurrentHashMap<>(); | ||
|
|
||
| /** Dispatcher thread (started lazily on first registration). */ | ||
| private static volatile Thread dispatcherThread; | ||
|
|
||
| /** | ||
| * Token returned by {@link #register} for later use with {@link #unregister}. | ||
| */ | ||
| record Registration(int signum, MemorySegment oldAction) {} | ||
coderabbitai[bot] marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
|
|
||
| // --- Public API --- | ||
|
|
||
| /** | ||
| * Returns whether FFM-based signal handling is available on this platform. | ||
| */ | ||
| static boolean isAvailable() { | ||
| return available; | ||
| } | ||
|
|
||
| /** | ||
| * Registers a signal handler via {@code sigaction()} with {@code SA_RESTART}. | ||
| * | ||
| * @param name signal name (e.g. "WINCH", "INT") | ||
| * @param handler the Java callback | ||
| * @return a {@link Registration} token, or {@code null} if the signal is unsupported | ||
| */ | ||
| static Object register(String name, Runnable handler) { | ||
| if (!available) { | ||
| return null; | ||
| } | ||
| int signum = signalNumber(name); | ||
| if (signum < 0) { | ||
| return null; | ||
| } | ||
|
|
||
| ensureDispatcherStarted(); | ||
| handlers.put(signum, handler); | ||
|
|
||
| try { | ||
| MemorySegment oldAct = Arena.global().allocate(sigactionLayout); | ||
| MemorySegment newAct = Arena.global().allocate(sigactionLayout); | ||
| sa_handler_vh.set(newAct, upcallStub); | ||
| sa_flags_vh.set(newAct, SA_RESTART); | ||
|
|
||
| int res = (int) sigaction_mh.invoke(signum, newAct, oldAct); | ||
| if (res != 0) { | ||
| logger.log(Level.FINE, "sigaction() failed for signal {0} (signum={1})", new Object[] {name, signum}); | ||
| handlers.remove(signum); | ||
| return null; | ||
| } | ||
| return new Registration(signum, oldAct); | ||
| } catch (Throwable t) { | ||
| logger.log(Level.FINE, "Error registering FFM signal handler for {0}", name); | ||
| logger.log(Level.FINE, "Exception details", t); | ||
|
Check failure on line 256 in terminal-ffm/src/main/java/org/jline/terminal/impl/ffm/FfmSignalHandler.java
|
||
| handlers.remove(signum); | ||
coderabbitai[bot] marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| return null; | ||
coderabbitai[bot] marked this conversation as resolved.
Show resolved
Hide resolved
Comment on lines
+269
to
+277
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Reconcile dispatcher lifecycle in the If post-install logic throws, this path restores handlers/native disposition but does not re-evaluate dispatcher state. That can leave dispatcher state inconsistent with Suggested adjustment } catch (Throwable t) {
logger.log(Level.FINE, "Error registering FFM signal handler for {0}", name);
logger.log(Level.FINE, EXCEPTION_DETAILS, t);
restoreHandler(signum, previousHandler);
+ if (previousHandler != null) {
+ ensureDispatcherStarted();
+ } else {
+ stopDispatcherIfIdle();
+ }
arena.close();
return null;
}🤖 Prompt for AI Agents |
||
| } | ||
| } | ||
|
|
||
| /** | ||
| * Registers the default (SIG_DFL) handler for the specified signal. | ||
| * | ||
| * @param name signal name | ||
| * @return a {@link Registration} token, or {@code null} if the signal is unsupported | ||
| */ | ||
| static Object registerDefault(String name) { | ||
| if (!available) { | ||
| return null; | ||
| } | ||
| int signum = signalNumber(name); | ||
| if (signum < 0) { | ||
| return null; | ||
| } | ||
|
|
||
| handlers.remove(signum); | ||
|
|
||
| try { | ||
| MemorySegment oldAct = Arena.global().allocate(sigactionLayout); | ||
| MemorySegment newAct = Arena.global().allocate(sigactionLayout); | ||
| // sa_handler = SIG_DFL (0) — already zero from allocate() | ||
| // sa_flags and sa_mask also zero | ||
|
|
||
| int res = (int) sigaction_mh.invoke(signum, newAct, oldAct); | ||
| if (res != 0) { | ||
| logger.log(Level.FINE, "sigaction(SIG_DFL) failed for signal {0}", name); | ||
| return null; | ||
| } | ||
| return new Registration(signum, oldAct); | ||
| } catch (Throwable t) { | ||
| logger.log(Level.FINE, "Error registering default handler for {0}", name); | ||
| logger.log(Level.FINE, "Exception details", t); | ||
| return null; | ||
| } | ||
| } | ||
|
|
||
| /** | ||
| * Restores the previous signal handler that was in place before registration. | ||
| * | ||
| * @param name signal name | ||
| * @param previous the token returned by {@link #register} or {@link #registerDefault} | ||
| */ | ||
| static void unregister(String name, Object previous) { | ||
| if (!(previous instanceof Registration reg)) { | ||
| return; | ||
| } | ||
|
|
||
| handlers.remove(reg.signum()); | ||
|
|
||
| try { | ||
| int res = (int) sigaction_mh.invoke(reg.signum(), reg.oldAction(), MemorySegment.NULL); | ||
| if (res != 0) { | ||
| logger.log(Level.FINE, "sigaction() restore failed for signal {0}", name); | ||
| } | ||
| } catch (Throwable t) { | ||
| logger.log(Level.FINE, "Error unregistering FFM signal handler for {0}", name); | ||
| logger.log(Level.FINE, "Exception details", t); | ||
| } | ||
| } | ||
|
|
||
| // --- Signal upcall target (called from native signal context) --- | ||
|
|
||
| /** | ||
| * Called from the native signal handler via the upcall stub. | ||
| * Sets an atomic flag; the dispatcher thread will invoke the Java handler. | ||
| */ | ||
| static void signalReceived(int signum) { | ||
| if (signum >= 0 && signum < pendingSignals.length()) { | ||
| pendingSignals.set(signum, 1); | ||
| } | ||
| } | ||
|
|
||
| // --- Dispatcher thread --- | ||
|
|
||
| private static synchronized void ensureDispatcherStarted() { | ||
| if (dispatcherThread != null) { | ||
| return; | ||
| } | ||
| Thread t = new Thread(FfmSignalHandler::dispatchLoop, "JLine-signal-dispatcher"); | ||
| t.setDaemon(true); | ||
| t.start(); | ||
| dispatcherThread = t; | ||
| } | ||
|
|
||
| /** | ||
| * Polls pending signal flags and dispatches to registered Java handlers. | ||
| * Runs on a daemon thread; exits when interrupted. | ||
| */ | ||
| private static void dispatchLoop() { | ||
| while (!Thread.interrupted()) { | ||
| boolean anyPending = false; | ||
| for (int i = 0; i < pendingSignals.length(); i++) { | ||
| if (pendingSignals.compareAndSet(i, 1, 0)) { | ||
| anyPending = true; | ||
| dispatchSignal(i); | ||
| } | ||
| } | ||
| if (!anyPending) { | ||
| LockSupport.parkNanos(1_000_000L); // 1 ms | ||
| } | ||
| } | ||
| } | ||
|
|
||
| /** | ||
| * Dispatches a single pending signal to its registered handler. | ||
| */ | ||
| private static void dispatchSignal(int signum) { | ||
| Runnable handler = handlers.get(signum); | ||
| if (handler != null) { | ||
| try { | ||
| handler.run(); | ||
| } catch (Exception e) { | ||
| logger.log(Level.WARNING, "Error in signal handler for signal {0}", signum); | ||
| logger.log(Level.WARNING, "Exception details", e); | ||
| } | ||
| } | ||
| } | ||
|
|
||
| // --- Signal name → number mapping --- | ||
|
|
||
| private static int signalNumber(String name) { | ||
| return switch (name) { | ||
| case "HUP" -> SIGHUP; | ||
| case "INT" -> SIGINT; | ||
| case "QUIT" -> SIGQUIT; | ||
| case "TERM" -> SIGTERM; | ||
| case "TSTP" -> SIGTSTP; | ||
| case "CONT" -> SIGCONT; | ||
| case "INFO" -> SIGINFO; | ||
| case "WINCH" -> SIGWINCH; | ||
| default -> -1; | ||
| }; | ||
| } | ||
| } | ||
Uh oh!
There was an error while loading. Please reload this page.