configurable kill signals
This commit is contained in:
@@ -3,6 +3,9 @@ columns = 100
|
|||||||
rows = 30
|
rows = 30
|
||||||
max_scrollback = 100000
|
max_scrollback = 100000
|
||||||
shell = "/bin/bash"
|
shell = "/bin/bash"
|
||||||
|
# Signal sent to a pane's shell process when the pane is closed (e.g. ALT+X).
|
||||||
|
# Use SIGKILL to force-kill instead of asking the shell to terminate.
|
||||||
|
close_signal = "SIGTERM"
|
||||||
font_family = "JetBrainsMono Nerd Font"
|
font_family = "JetBrainsMono Nerd Font"
|
||||||
font_size = 15
|
font_size = 15
|
||||||
|
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ public record AppConfig(
|
|||||||
double windowHeight,
|
double windowHeight,
|
||||||
boolean kittyGraphics,
|
boolean kittyGraphics,
|
||||||
String scrollbackEditorCommand,
|
String scrollbackEditorCommand,
|
||||||
|
String closeSignal,
|
||||||
Map<String, String> envOverride,
|
Map<String, String> envOverride,
|
||||||
Map<String, KeyBinding> keybindings
|
Map<String, KeyBinding> keybindings
|
||||||
) {
|
) {
|
||||||
@@ -71,6 +72,7 @@ public record AppConfig(
|
|||||||
doubleValue(document, "window.height", defaults.windowHeight),
|
doubleValue(document, "window.height", defaults.windowHeight),
|
||||||
booleanValue(document, "kitty_graphics.enabled", defaults.kittyGraphics),
|
booleanValue(document, "kitty_graphics.enabled", defaults.kittyGraphics),
|
||||||
stringValue(document, "scrollback.editor_command", defaults.scrollbackEditorCommand),
|
stringValue(document, "scrollback.editor_command", defaults.scrollbackEditorCommand),
|
||||||
|
closeSignalValue(document, defaults.closeSignal),
|
||||||
envOverride(document, defaults.envOverride),
|
envOverride(document, defaults.envOverride),
|
||||||
keybindings(document, defaults)
|
keybindings(document, defaults)
|
||||||
);
|
);
|
||||||
@@ -92,6 +94,7 @@ public record AppConfig(
|
|||||||
760.0,
|
760.0,
|
||||||
true,
|
true,
|
||||||
defaultScrollbackEditorCommand(),
|
defaultScrollbackEditorCommand(),
|
||||||
|
"SIGTERM",
|
||||||
Map.of(),
|
Map.of(),
|
||||||
Map.ofEntries(
|
Map.ofEntries(
|
||||||
Map.entry("navigate_left", KeyBinding.parse("ALT+H")),
|
Map.entry("navigate_left", KeyBinding.parse("ALT+H")),
|
||||||
@@ -125,11 +128,22 @@ public record AppConfig(
|
|||||||
windowHeight,
|
windowHeight,
|
||||||
kittyGraphics,
|
kittyGraphics,
|
||||||
scrollbackEditorCommand,
|
scrollbackEditorCommand,
|
||||||
|
closeSignal,
|
||||||
envOverride,
|
envOverride,
|
||||||
keybindings
|
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() {
|
public void save() {
|
||||||
save(configPath(), this);
|
save(configPath(), this);
|
||||||
}
|
}
|
||||||
@@ -156,6 +170,23 @@ public record AppConfig(
|
|||||||
return editor.trim() + " {file}";
|
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) {
|
private static Map<String, KeyBinding> keybindings(TomlTable table, AppConfig defaults) {
|
||||||
Map<String, KeyBinding> parsed = new LinkedHashMap<>();
|
Map<String, KeyBinding> parsed = new LinkedHashMap<>();
|
||||||
for (String key : KEYBINDING_KEYS) {
|
for (String key : KEYBINDING_KEYS) {
|
||||||
@@ -193,6 +224,7 @@ public record AppConfig(
|
|||||||
builder.append("rows = ").append(rows).append('\n');
|
builder.append("rows = ").append(rows).append('\n');
|
||||||
builder.append("max_scrollback = ").append(maxScrollback).append('\n');
|
builder.append("max_scrollback = ").append(maxScrollback).append('\n');
|
||||||
builder.append("shell = ").append(quotedList(shell)).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_family = ").append(quoted(fontFamily)).append('\n');
|
||||||
builder.append("font_size = ").append(trimDouble(fontSize)).append("\n\n");
|
builder.append("font_size = ").append(trimDouble(fontSize)).append("\n\n");
|
||||||
builder.append("[window]\n");
|
builder.append("[window]\n");
|
||||||
|
|||||||
@@ -63,7 +63,10 @@ public final class LinuxPty implements AutoCloseable {
|
|||||||
private static final long TIOCSWINSZ = 0x5414L;
|
private static final long TIOCSWINSZ = 0x5414L;
|
||||||
private static final short POSIX_SPAWN_SETSID = 0x80;
|
private static final short POSIX_SPAWN_SETSID = 0x80;
|
||||||
private static final int SIGHUP = 1;
|
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 SIGKILL = 9;
|
||||||
|
private static final int SIGTERM = 15;
|
||||||
private static final int WNOHANG = 1;
|
private static final int WNOHANG = 1;
|
||||||
|
|
||||||
// struct winsize { unsigned short ws_row, ws_col, ws_xpixel, ws_ypixel; }
|
// 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 Object writeLock = new Object();
|
||||||
private final int masterFd;
|
private final int masterFd;
|
||||||
private final int pid;
|
private final int pid;
|
||||||
|
private final int closeSignal;
|
||||||
private volatile boolean closed;
|
private volatile boolean closed;
|
||||||
|
|
||||||
private LinuxPty(int masterFd, int pid) {
|
private LinuxPty(int masterFd, int pid, int closeSignal) {
|
||||||
this.masterFd = masterFd;
|
this.masterFd = masterFd;
|
||||||
this.pid = pid;
|
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 argv command and arguments (e.g. {@code {"/bin/zsh", "-i"}})
|
||||||
* @param environment environment for the child, as KEY=VALUE pairs
|
* @param environment environment for the child, as KEY=VALUE pairs
|
||||||
* @param workingDirectory directory the child starts in, or {@code null} to inherit
|
* @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();
|
Arena setup = Arena.ofConfined();
|
||||||
try {
|
try {
|
||||||
int master = check(callInt(POSIX_OPENPT, O_RDWR | O_NOCTTY), "posix_openpt");
|
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) {
|
if (rc != 0) {
|
||||||
throw new IllegalStateException("posix_spawnp failed for " + argv[0] + " (rc=" + rc + ")");
|
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 {
|
} finally {
|
||||||
callInt(ATTR_DESTROY, attr);
|
callInt(ATTR_DESTROY, attr);
|
||||||
callInt(FA_DESTROY, actions);
|
callInt(FA_DESTROY, actions);
|
||||||
@@ -249,7 +279,7 @@ public final class LinuxPty implements AutoCloseable {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
closed = true;
|
closed = true;
|
||||||
callKill(pid, SIGHUP);
|
callKill(pid, closeSignal);
|
||||||
callInt(CLOSE, masterFd);
|
callInt(CLOSE, masterFd);
|
||||||
reap();
|
reap();
|
||||||
arena.close();
|
arena.close();
|
||||||
|
|||||||
@@ -27,9 +27,10 @@ public final class ShellSession implements AutoCloseable {
|
|||||||
* config, not assumed here.
|
* config, not assumed here.
|
||||||
*/
|
*/
|
||||||
public static ShellSession start(List<String> shellCommand, Map<String, String> envOverride, TerminalPane pane,
|
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 {
|
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) {
|
} catch (RuntimeException ex) {
|
||||||
pane.write("failed to start shell: " + ex.getMessage() + "\r\n");
|
pane.write("failed to start shell: " + ex.getMessage() + "\r\n");
|
||||||
throw new IllegalStateException("Could not start shell " + String.join(" ", shellCommand), ex);
|
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.
|
* flags. {@code command} must not be null.
|
||||||
*/
|
*/
|
||||||
public static ShellSession startCommand(Map<String, String> envOverride, TerminalPane pane,
|
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 {
|
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) {
|
} catch (RuntimeException ex) {
|
||||||
pane.write("failed to run command: " + ex.getMessage() + "\r\n");
|
pane.write("failed to run command: " + ex.getMessage() + "\r\n");
|
||||||
throw new IllegalStateException("Could not run command: " + command, ex);
|
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,
|
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());
|
Map<String, String> environment = new HashMap<>(System.getenv());
|
||||||
environment.put("TERM", "xterm-kitty");
|
environment.put("TERM", "xterm-kitty");
|
||||||
environment.put("COLORTERM", "truecolor");
|
environment.put("COLORTERM", "truecolor");
|
||||||
@@ -65,7 +67,8 @@ public final class ShellSession implements AutoCloseable {
|
|||||||
LinuxPty pty = LinuxPty.spawn(
|
LinuxPty pty = LinuxPty.spawn(
|
||||||
argv,
|
argv,
|
||||||
environment,
|
environment,
|
||||||
workingDirectory != null ? workingDirectory : System.getProperty("user.home"));
|
workingDirectory != null ? workingDirectory : System.getProperty("user.home"),
|
||||||
|
closeSignal);
|
||||||
ShellSession session = new ShellSession(pty);
|
ShellSession session = new ShellSession(pty);
|
||||||
session.resize(columns, rows);
|
session.resize(columns, rows);
|
||||||
return session;
|
return session;
|
||||||
|
|||||||
@@ -81,7 +81,7 @@ public final class TerminalPane implements AutoCloseable, RenderTarget {
|
|||||||
double widthPx, double heightPx, String workingDirectory) {
|
double widthPx, double heightPx, String workingDirectory) {
|
||||||
TerminalPane pane = newPane(config, metrics, onContentChange, widthPx, heightPx);
|
TerminalPane pane = newPane(config, metrics, onContentChange, widthPx, heightPx);
|
||||||
pane.attach(ShellSession.start(config.shell(), config.envOverride(), pane, pane.columns, pane.rows,
|
pane.attach(ShellSession.start(config.shell(), config.envOverride(), pane, pane.columns, pane.rows,
|
||||||
workingDirectory));
|
workingDirectory, config.closeSignalNumber()));
|
||||||
return pane;
|
return pane;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -94,7 +94,7 @@ public final class TerminalPane implements AutoCloseable, RenderTarget {
|
|||||||
double widthPx, double heightPx, String workingDirectory, String command) {
|
double widthPx, double heightPx, String workingDirectory, String command) {
|
||||||
TerminalPane pane = newPane(config, metrics, onContentChange, widthPx, heightPx);
|
TerminalPane pane = newPane(config, metrics, onContentChange, widthPx, heightPx);
|
||||||
pane.attach(ShellSession.startCommand(config.envOverride(), pane, pane.columns, pane.rows,
|
pane.attach(ShellSession.startCommand(config.envOverride(), pane, pane.columns, pane.rows,
|
||||||
workingDirectory, command));
|
workingDirectory, command, config.closeSignalNumber()));
|
||||||
return pane;
|
return pane;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user