configurable kill signals

This commit is contained in:
2026-06-02 09:31:14 +02:00
parent c2ccd056af
commit dcb70243aa
5 changed files with 80 additions and 12 deletions

View File

@@ -29,6 +29,7 @@ public record AppConfig(
double windowHeight,
boolean kittyGraphics,
String scrollbackEditorCommand,
String closeSignal,
Map<String, String> envOverride,
Map<String, KeyBinding> keybindings
) {
@@ -71,6 +72,7 @@ public record AppConfig(
doubleValue(document, "window.height", defaults.windowHeight),
booleanValue(document, "kitty_graphics.enabled", defaults.kittyGraphics),
stringValue(document, "scrollback.editor_command", defaults.scrollbackEditorCommand),
closeSignalValue(document, defaults.closeSignal),
envOverride(document, defaults.envOverride),
keybindings(document, defaults)
);
@@ -92,6 +94,7 @@ public record AppConfig(
760.0,
true,
defaultScrollbackEditorCommand(),
"SIGTERM",
Map.of(),
Map.ofEntries(
Map.entry("navigate_left", KeyBinding.parse("ALT+H")),
@@ -125,11 +128,22 @@ public record AppConfig(
windowHeight,
kittyGraphics,
scrollbackEditorCommand,
closeSignal,
envOverride,
keybindings
);
}
/**
* The {@link #closeSignal} as a Linux signal number, sent to a pane's shell process when the
* pane is closed (e.g. via the close-pane key). Falls back to SIGTERM (15) if the configured
* name is somehow unresolvable.
*/
public int closeSignalNumber() {
int number = LinuxPty.signalNumber(closeSignal);
return number < 0 ? 15 : number;
}
public void save() {
save(configPath(), this);
}
@@ -156,6 +170,23 @@ public record AppConfig(
return editor.trim() + " {file}";
}
/**
* Reads {@code terminal.close_signal}, normalising it to a canonical {@code SIG*} name. An
* unknown or unset value keeps {@code fallback} so a typo can't leave a pane unkillable.
*/
private static String closeSignalValue(TomlTable table, String fallback) {
String value = stringValue(table, "terminal.close_signal", null);
if (value == null) {
return fallback;
}
if (LinuxPty.signalNumber(value) < 0) {
System.err.println("Unknown terminal.close_signal '" + value + "', using " + fallback);
return fallback;
}
String normalized = value.trim().toUpperCase(java.util.Locale.ROOT);
return normalized.startsWith("SIG") ? normalized : "SIG" + normalized;
}
private static Map<String, KeyBinding> keybindings(TomlTable table, AppConfig defaults) {
Map<String, KeyBinding> parsed = new LinkedHashMap<>();
for (String key : KEYBINDING_KEYS) {
@@ -193,6 +224,7 @@ public record AppConfig(
builder.append("rows = ").append(rows).append('\n');
builder.append("max_scrollback = ").append(maxScrollback).append('\n');
builder.append("shell = ").append(quotedList(shell)).append('\n');
builder.append("close_signal = ").append(quoted(closeSignal)).append('\n');
builder.append("font_family = ").append(quoted(fontFamily)).append('\n');
builder.append("font_size = ").append(trimDouble(fontSize)).append("\n\n");
builder.append("[window]\n");

View File

@@ -63,7 +63,10 @@ public final class LinuxPty implements AutoCloseable {
private static final long TIOCSWINSZ = 0x5414L;
private static final short POSIX_SPAWN_SETSID = 0x80;
private static final int SIGHUP = 1;
private static final int SIGINT = 2;
private static final int SIGQUIT = 3;
private static final int SIGKILL = 9;
private static final int SIGTERM = 15;
private static final int WNOHANG = 1;
// struct winsize { unsigned short ws_row, ws_col, ws_xpixel, ws_ypixel; }
@@ -105,11 +108,36 @@ public final class LinuxPty implements AutoCloseable {
private final Object writeLock = new Object();
private final int masterFd;
private final int pid;
private final int closeSignal;
private volatile boolean closed;
private LinuxPty(int masterFd, int pid) {
private LinuxPty(int masterFd, int pid, int closeSignal) {
this.masterFd = masterFd;
this.pid = pid;
this.closeSignal = closeSignal;
}
/**
* Resolves a signal name (e.g. {@code "SIGTERM"}, {@code "TERM"}, {@code "SIGKILL"}) to its
* Linux signal number, or {@code -1} if the name is not one we recognise. Case-insensitive and
* tolerant of a missing {@code SIG} prefix.
*/
public static int signalNumber(String name) {
if (name == null) {
return -1;
}
String normalized = name.trim().toUpperCase(java.util.Locale.ROOT);
if (normalized.startsWith("SIG")) {
normalized = normalized.substring(3);
}
return switch (normalized) {
case "HUP" -> SIGHUP;
case "INT" -> SIGINT;
case "QUIT" -> SIGQUIT;
case "KILL" -> SIGKILL;
case "TERM" -> SIGTERM;
default -> -1;
};
}
/**
@@ -118,8 +146,10 @@ public final class LinuxPty implements AutoCloseable {
* @param argv command and arguments (e.g. {@code {"/bin/zsh", "-i"}})
* @param environment environment for the child, as KEY=VALUE pairs
* @param workingDirectory directory the child starts in, or {@code null} to inherit
* @param closeSignal signal number sent to the child on {@link #close()} (e.g. SIGTERM)
*/
public static LinuxPty spawn(String[] argv, Map<String, String> environment, String workingDirectory) {
public static LinuxPty spawn(String[] argv, Map<String, String> environment, String workingDirectory,
int closeSignal) {
Arena setup = Arena.ofConfined();
try {
int master = check(callInt(POSIX_OPENPT, O_RDWR | O_NOCTTY), "posix_openpt");
@@ -158,7 +188,7 @@ public final class LinuxPty implements AutoCloseable {
if (rc != 0) {
throw new IllegalStateException("posix_spawnp failed for " + argv[0] + " (rc=" + rc + ")");
}
return new LinuxPty(master, pidOut.get(C_INT, 0));
return new LinuxPty(master, pidOut.get(C_INT, 0), closeSignal);
} finally {
callInt(ATTR_DESTROY, attr);
callInt(FA_DESTROY, actions);
@@ -249,7 +279,7 @@ public final class LinuxPty implements AutoCloseable {
return;
}
closed = true;
callKill(pid, SIGHUP);
callKill(pid, closeSignal);
callInt(CLOSE, masterFd);
reap();
arena.close();

View File

@@ -27,9 +27,10 @@ public final class ShellSession implements AutoCloseable {
* config, not assumed here.
*/
public static ShellSession start(List<String> shellCommand, Map<String, String> envOverride, TerminalPane pane,
int columns, int rows, String workingDirectory) {
int columns, int rows, String workingDirectory, int closeSignal) {
try {
return spawn(shellCommand.toArray(new String[0]), envOverride, columns, rows, workingDirectory);
return spawn(shellCommand.toArray(new String[0]), envOverride, columns, rows, workingDirectory,
closeSignal);
} catch (RuntimeException ex) {
pane.write("failed to start shell: " + ex.getMessage() + "\r\n");
throw new IllegalStateException("Could not start shell " + String.join(" ", shellCommand), ex);
@@ -45,9 +46,10 @@ public final class ShellSession implements AutoCloseable {
* flags. {@code command} must not be null.
*/
public static ShellSession startCommand(Map<String, String> envOverride, TerminalPane pane,
int columns, int rows, String workingDirectory, String command) {
int columns, int rows, String workingDirectory, String command, int closeSignal) {
try {
return spawn(new String[] {"/bin/sh", "-c", command}, envOverride, columns, rows, workingDirectory);
return spawn(new String[] {"/bin/sh", "-c", command}, envOverride, columns, rows, workingDirectory,
closeSignal);
} catch (RuntimeException ex) {
pane.write("failed to run command: " + ex.getMessage() + "\r\n");
throw new IllegalStateException("Could not run command: " + command, ex);
@@ -55,7 +57,7 @@ public final class ShellSession implements AutoCloseable {
}
private static ShellSession spawn(String[] argv, Map<String, String> envOverride,
int columns, int rows, String workingDirectory) {
int columns, int rows, String workingDirectory, int closeSignal) {
Map<String, String> environment = new HashMap<>(System.getenv());
environment.put("TERM", "xterm-kitty");
environment.put("COLORTERM", "truecolor");
@@ -65,7 +67,8 @@ public final class ShellSession implements AutoCloseable {
LinuxPty pty = LinuxPty.spawn(
argv,
environment,
workingDirectory != null ? workingDirectory : System.getProperty("user.home"));
workingDirectory != null ? workingDirectory : System.getProperty("user.home"),
closeSignal);
ShellSession session = new ShellSession(pty);
session.resize(columns, rows);
return session;

View File

@@ -81,7 +81,7 @@ public final class TerminalPane implements AutoCloseable, RenderTarget {
double widthPx, double heightPx, String workingDirectory) {
TerminalPane pane = newPane(config, metrics, onContentChange, widthPx, heightPx);
pane.attach(ShellSession.start(config.shell(), config.envOverride(), pane, pane.columns, pane.rows,
workingDirectory));
workingDirectory, config.closeSignalNumber()));
return pane;
}
@@ -94,7 +94,7 @@ public final class TerminalPane implements AutoCloseable, RenderTarget {
double widthPx, double heightPx, String workingDirectory, String command) {
TerminalPane pane = newPane(config, metrics, onContentChange, widthPx, heightPx);
pane.attach(ShellSession.startCommand(config.envOverride(), pane, pane.columns, pane.rows,
workingDirectory, command));
workingDirectory, command, config.closeSignalNumber()));
return pane;
}