From c0ce81f125225a3ddc16f7edc48ba0e43a1ed11b Mon Sep 17 00:00:00 2001 From: Gregor Lohaus Date: Tue, 2 Jun 2026 09:35:41 +0200 Subject: [PATCH] shutdown hook --- .../java/com/gregor/jprototerm/Compositor.java | 12 ++++++++++++ src/main/java/com/gregor/jprototerm/Main.java | 6 ++++++ src/main/java/com/gregor/jprototerm/Tab.java | 10 ++++++++++ .../java/com/gregor/jprototerm/TerminalPane.java | 15 ++++++++++++++- 4 files changed, 42 insertions(+), 1 deletion(-) diff --git a/src/main/java/com/gregor/jprototerm/Compositor.java b/src/main/java/com/gregor/jprototerm/Compositor.java index 24eac4c..6c7a5a8 100644 --- a/src/main/java/com/gregor/jprototerm/Compositor.java +++ b/src/main/java/com/gregor/jprototerm/Compositor.java @@ -236,6 +236,18 @@ public final class Compositor { tabs.clear(); } + /** + * Signals and reaps every pane's shell process across all tabs, without tearing down render + * state. Intended for a JVM shutdown hook (SIGTERM/SIGINT/SIGHUP), so child shells get the + * configured close signal instead of being orphaned when jprototerm itself is killed. Safe to + * call off the FX thread and idempotent; see {@link TerminalPane#terminateSession()}. + */ + public void terminateSessions() { + for (Tab tab : List.copyOf(tabs)) { + tab.terminateSessions(); + } + } + private Tab currentTab() { return tabs.get(currentTabIndex); } diff --git a/src/main/java/com/gregor/jprototerm/Main.java b/src/main/java/com/gregor/jprototerm/Main.java index db67345..569c8df 100644 --- a/src/main/java/com/gregor/jprototerm/Main.java +++ b/src/main/java/com/gregor/jprototerm/Main.java @@ -41,6 +41,12 @@ public final class Main extends Application { compositor.close(); Platform.exit(); }); + // If jprototerm itself is killed (SIGTERM/SIGINT/SIGHUP, e.g. a logout or `kill`), the JVM + // runs shutdown hooks before exiting. Send each pane's configured close signal here so the + // child shells are terminated rather than orphaned. Only the ptys are touched (not ghostty's + // native state), so this is safe to run concurrently with the still-live render loop. A + // SIGKILL of jprototerm bypasses hooks entirely; nothing can help there. + Runtime.getRuntime().addShutdownHook(new Thread(compositor::terminateSessions, "shell-cleanup")); StackPane root = new StackPane(compositor.canvas(), compositor.imageOverlay()); compositor.canvas().widthProperty().bind(root.widthProperty()); diff --git a/src/main/java/com/gregor/jprototerm/Tab.java b/src/main/java/com/gregor/jprototerm/Tab.java index c56fdde..ef9cb5c 100644 --- a/src/main/java/com/gregor/jprototerm/Tab.java +++ b/src/main/java/com/gregor/jprototerm/Tab.java @@ -423,4 +423,14 @@ final class Tab implements AutoCloseable { tiled.clear(); floating.clear(); } + + /** + * Signals and reaps every pane's shell process without tearing down render state. Safe to call + * off the FX thread (see {@link TerminalPane#terminateSession()}); iterates snapshots so a + * concurrent close on the FX thread can't trigger a {@link java.util.ConcurrentModificationException}. + */ + public void terminateSessions() { + List.copyOf(tiled).forEach(TerminalPane::terminateSession); + List.copyOf(floating).forEach(TerminalPane::terminateSession); + } } diff --git a/src/main/java/com/gregor/jprototerm/TerminalPane.java b/src/main/java/com/gregor/jprototerm/TerminalPane.java index 8224da8..f79fe3e 100644 --- a/src/main/java/com/gregor/jprototerm/TerminalPane.java +++ b/src/main/java/com/gregor/jprototerm/TerminalPane.java @@ -39,7 +39,7 @@ public final class TerminalPane implements AutoCloseable, RenderTarget { // tracking meaningful: update() accumulates dirty since the last resetDirty(). private final RenderState renderState = new RenderState(); private RenderStateSnapshot cachedSnapshot; - private ShellSession session; + private volatile ShellSession session; // Run once (on the FX thread) when this pane's process exits on its own, so the owning tab can // remove it. Set by the Tab that creates the pane; null until then. private Runnable onExit; @@ -380,4 +380,17 @@ public final class TerminalPane implements AutoCloseable, RenderTarget { renderState.close(); terminal.close(); } + + /** + * Signals and reaps just the shell process, leaving the render/native state untouched. Unlike + * {@link #close()} this is safe to call off the FX thread — notably from a JVM shutdown hook, + * which runs concurrently with the live render loop — because it only touches the pty (a child + * process and fd), not ghostty's terminal handles. Idempotent; the OS reclaims the rest on exit. + */ + public void terminateSession() { + ShellSession current = session; + if (current != null) { + current.close(); + } + } }