diff --git a/adapters/bukkit/plugin/src/main/java/art/arcane/iris/Iris.java b/adapters/bukkit/plugin/src/main/java/art/arcane/iris/Iris.java index ee0c35397..c099cdbe5 100644 --- a/adapters/bukkit/plugin/src/main/java/art/arcane/iris/Iris.java +++ b/adapters/bukkit/plugin/src/main/java/art/arcane/iris/Iris.java @@ -58,6 +58,7 @@ import art.arcane.volmlib.util.director.help.DirectorMiniMenu; import art.arcane.volmlib.util.localization.BukkitLanguageSwitcher; import art.arcane.iris.integration.MultiverseCoreLink; 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.volmlib.nativelib.terrain.ServerShutdownBoundary; import art.arcane.iris.studio.view.BukkitGuiHost; @@ -579,6 +580,17 @@ public class Iris extends VolmitPlugin implements Listener, ReloadAware { this::getIrisVersion, this::getMCVersion)); 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) { diff --git a/adapters/bukkit/plugin/src/main/java/art/arcane/iris/platform/agent/AttachSignal.java b/adapters/bukkit/plugin/src/main/java/art/arcane/iris/platform/agent/AttachSignal.java new file mode 100644 index 000000000..e636c6737 --- /dev/null +++ b/adapters/bukkit/plugin/src/main/java/art/arcane/iris/platform/agent/AttachSignal.java @@ -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); + } + } +} diff --git a/adapters/bukkit/plugin/src/test/java/art/arcane/iris/platform/agent/AttachSignalTest.java b/adapters/bukkit/plugin/src/test/java/art/arcane/iris/platform/agent/AttachSignalTest.java new file mode 100644 index 000000000..b45d427da --- /dev/null +++ b/adapters/bukkit/plugin/src/test/java/art/arcane/iris/platform/agent/AttachSignalTest.java @@ -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); + } + } +}