Fixed Spigot agent attach

- Iris restores the JVM SIGQUIT handler when the server console replaced it
- The Java agent attaches on Spigot servers with a terminal console
- Tests cover restoring the handler after a console resets it
This commit is contained in:
Brian Neumann-Fopiano committed 2026-10-29 01:09:16 -04:00
1 parent 58ab3fcbbe
commit 21ab5baf27
3 files changed
+119

No files matched your search

@@ -58,6 +58,7 @@ import art.arcane.volmlib.util.director.help.DirectorMiniMenu;
import art.arcane.volmlib.util.localization.BukkitLanguageSwitcher; import art.arcane.volmlib.util.localization.BukkitLanguageSwitcher;
import art.arcane.iris.integration.MultiverseCoreLink; import art.arcane.iris.integration.MultiverseCoreLink;
import art.arcane.iris.platform.agent.Agent; import art.arcane.iris.platform.agent.Agent;
import art.arcane.iris.platform.agent.AttachSignal;
import art.arcane.iris.platform.bukkit.nms.INMS; import art.arcane.iris.platform.bukkit.nms.INMS;
import art.arcane.volmlib.nativelib.terrain.ServerShutdownBoundary; import art.arcane.volmlib.nativelib.terrain.ServerShutdownBoundary;
import art.arcane.iris.studio.view.BukkitGuiHost; import art.arcane.iris.studio.view.BukkitGuiHost;
@@ -579,6 +580,17 @@ public class Iris extends VolmitPlugin implements Listener, ReloadAware {
this::getIrisVersion, this::getIrisVersion,
this::getMCVersion)); this::getMCVersion));
SlimJar.load(); SlimJar.load();
restoreAttachSignal();
}
private static void restoreAttachSignal() {
try {
if (AttachSignal.restoreJvmHandler()) {
info("Restored the JVM SIGQUIT handler that the server console replaced");
}
} catch (RuntimeException failure) {
reportError("Could not restore the JVM SIGQUIT handler; the Iris agent may fail to attach.", failure);
}
} }
private static void bridgeLog(LogLevel level, String message) { private static void bridgeLog(LogLevel level, String message) {
@@ -0,0 +1,50 @@
package art.arcane.iris.platform.agent;
import java.lang.foreign.Arena;
import java.lang.foreign.FunctionDescriptor;
import java.lang.foreign.Linker;
import java.lang.foreign.MemorySegment;
import java.lang.foreign.ValueLayout;
import java.lang.invoke.MethodHandle;
import java.util.Locale;
public final class AttachSignal {
private static final int SIGQUIT = 3;
private static final int SIGTERM = 15;
private static final long ACTION_BYTES = 256;
private static final long SIG_IGN = 1L;
private AttachSignal() {
}
public static boolean restoreJvmHandler() {
if (System.getProperty("os.name", "").toLowerCase(Locale.ROOT).contains("win")) {
return false;
}
Linker linker = Linker.nativeLinker();
MethodHandle sigaction = linker.downcallHandle(
linker.defaultLookup().find("sigaction")
.orElseThrow(() -> new IllegalStateException("sigaction is unavailable")),
FunctionDescriptor.of(ValueLayout.JAVA_INT, ValueLayout.JAVA_INT, ValueLayout.ADDRESS, ValueLayout.ADDRESS));
try (Arena arena = Arena.ofConfined()) {
MemorySegment jvm = arena.allocate(ACTION_BYTES);
MemorySegment quit = arena.allocate(ACTION_BYTES);
if ((int) sigaction.invokeExact(SIGTERM, MemorySegment.NULL, jvm) != 0
|| (int) sigaction.invokeExact(SIGQUIT, MemorySegment.NULL, quit) != 0) {
throw new IllegalStateException("sigaction could not read the SIGTERM and SIGQUIT handlers");
}
long handler = jvm.get(ValueLayout.JAVA_LONG, 0);
if (handler == 0L || handler == SIG_IGN || quit.get(ValueLayout.JAVA_LONG, 0) == handler) {
return false;
}
if ((int) sigaction.invokeExact(SIGQUIT, jvm, MemorySegment.NULL) != 0) {
throw new IllegalStateException("sigaction could not restore the JVM SIGQUIT handler");
}
return true;
} catch (RuntimeException | Error failure) {
throw failure;
} catch (Throwable failure) {
throw new IllegalStateException("Unable to restore the JVM SIGQUIT handler", failure);
}
}
}
@@ -0,0 +1,57 @@
package art.arcane.iris.platform.agent;
import org.junit.Test;
import java.lang.foreign.Arena;
import java.lang.foreign.FunctionDescriptor;
import java.lang.foreign.Linker;
import java.lang.foreign.MemorySegment;
import java.lang.foreign.ValueLayout;
import java.lang.invoke.MethodHandle;
import java.util.Locale;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotEquals;
import static org.junit.Assert.assertTrue;
import static org.junit.Assume.assumeFalse;
public class AttachSignalTest {
private static final int SIGQUIT = 3;
private static final int SIGTERM = 15;
private static final long ACTION_BYTES = 256;
private static final MethodHandle SIGACTION = Linker.nativeLinker().downcallHandle(
Linker.nativeLinker().defaultLookup().find("sigaction").orElseThrow(),
FunctionDescriptor.of(ValueLayout.JAVA_INT, ValueLayout.JAVA_INT, ValueLayout.ADDRESS, ValueLayout.ADDRESS));
@Test
public void aConsoleThatResetsQuitGetsTheJvmHandlerBack() throws Throwable {
assumeFalse(System.getProperty("os.name").toLowerCase(Locale.ROOT).contains("win"));
long jvmHandler = handler(SIGTERM);
assertNotEquals(0L, jvmHandler);
try (Arena arena = Arena.ofConfined()) {
MemorySegment original = arena.allocate(ACTION_BYTES);
assertEquals(0, (int) SIGACTION.invokeExact(SIGQUIT, MemorySegment.NULL, original));
try {
MemorySegment reset = arena.allocate(ACTION_BYTES);
assertEquals(0, (int) SIGACTION.invokeExact(SIGQUIT, reset, MemorySegment.NULL));
assertEquals(0L, handler(SIGQUIT));
assertTrue(AttachSignal.restoreJvmHandler());
assertEquals(jvmHandler, handler(SIGQUIT));
assertFalse(AttachSignal.restoreJvmHandler());
} finally {
assertEquals(0, (int) SIGACTION.invokeExact(SIGQUIT, original, MemorySegment.NULL));
}
}
}
private static long handler(int signal) throws Throwable {
try (Arena arena = Arena.ofConfined()) {
MemorySegment action = arena.allocate(ACTION_BYTES);
assertEquals(0, (int) SIGACTION.invokeExact(signal, MemorySegment.NULL, action));
return action.get(ValueLayout.JAVA_LONG, 0);
}
}
}