summaryrefslogtreecommitdiff
path: root/src/host_io.zig
diff options
context:
space:
mode:
authorGabriel Schneider <[email protected]>2026-09-28 12:06:13 -0300
committerGabriel Schneider <[email protected]>2026-10-01 00:12:14 -0300
commit8b18331f5e3f6c346b992019163ec3f5b6e6b721 (patch)
treef6e0236af8b0ac0428078d5ba512df10a1f3f43d /src/host_io.zig
parent990c2e1b184e9cb1fddd8eb1b05a472cfb113dde (diff)
downloadpardes-8b18331f5e3f6c346b992019163ec3f5b6e6b721.tar.gz
pardes-8b18331f5e3f6c346b992019163ec3f5b6e6b721.zip
Kill asks the host to signal at once and says when a shell has no job to signal
Kill queued a signal effect for later, so the job it saw running by its marks could have ended, and another started, before the signal went; and with job control off the job shares the shell's process group, so the host skipped it and Kill reported nothing. Kill now calls the host's kill_job while it holds the turn, and when the only job is the shell's own group says 'Kill: no job to signal', which also fails a write of Kill to ctl. Co-Authored-By: Claude Opus 5.5 <[email protected]>
Diffstat (limited to 'src/host_io.zig')
-rw-r--r--src/host_io.zig35
1 files changed, 26 insertions, 9 deletions
diff --git a/src/host_io.zig b/src/host_io.zig
index 81d784c5..6009c00f 100644
--- a/src/host_io.zig
+++ b/src/host_io.zig
@@ -22,6 +22,9 @@ pub const Host = struct {
pty_signal: ?*const fn (ctx: ?*anyopaque, pane: u8, sig: pardes.PtySignal) void = null,
close_pty: ?*const fn (ctx: ?*anyopaque, pane: u8) void = null,
tty_taken: ?*const fn (ctx: ?*anyopaque, pane: u8) bool = null,
+ /// Answered at once, not queued as an effect: Kill asks while the
+ /// job it saw running is still the one in the tty (host_io.killJob).
+ kill_job: ?*const fn (ctx: ?*anyopaque, pane: u8) bool = null,
gpio_toggle: ?*const fn (ctx: ?*anyopaque, pin: u16, was: *u8, now: *u8) bool = null,
write_file: ?*const fn (ctx: ?*anyopaque, pane: u8, path: []const u8, bytes: []const u8) void = null,
write_dump: ?*const fn (ctx: ?*anyopaque, bytes: []const u8) void = null,
@@ -1366,14 +1369,8 @@ pub fn signalTty(shell_pid: libc.pid_t, master_fd: c_int, which: pardes.PtySigna
.hup => libc.SIG.HUP,
.quit => libc.SIG.QUIT,
.kill => libc.SIG.KILL,
- .command => libc.SIG.TERM,
};
const fg = tcgetpgrp(master_fd);
- // The shell leads its own group: a job it runs has another.
- if (which == .command) {
- if (fg > 0 and fg != shell_pid) _ = libc.kill(-fg, sig);
- return;
- }
if (fg > 0) {
_ = libc.kill(-fg, sig);
return;
@@ -1381,6 +1378,16 @@ pub fn signalTty(shell_pid: libc.pid_t, master_fd: c_int, which: pardes.PtySigna
if (shell_pid > 0) _ = libc.kill(shell_pid, sig);
}
+/// Kill: SIGTERM to the job in the tty's foreground, never to the shell.
+/// The shell leads its own group, and a job it runs has another; with job
+/// control off (`set +m`, a shell run as `sh -c`) the job shares the shell's
+/// group, and there is no job to signal apart from it: false.
+pub fn killJob(shell_pid: libc.pid_t, master_fd: c_int) bool {
+ const fg = tcgetpgrp(master_fd);
+ if (fg <= 0 or fg == shell_pid) return false;
+ return libc.kill(-fg, libc.SIG.TERM) == 0;
+}
+
fn readProc(path: [*:0]const u8, buf: []u8) ?[]const u8 {
const fd = libc.open(path, .{ .ACCMODE = .RDONLY });
if (fd < 0) return null;
@@ -1675,23 +1682,33 @@ test "an idle prompt is free, a foreground job takes the tty, and Ctrl-C hands i
try std.testing.expect(sh.waitText(test_prompt, 10_000));
}
-test "Kill's signal stops the foreground job and never the shell" {
+test "Kill stops the foreground job, never the shell, and finds none with job control off" {
if (comptime !tty_probe_platform) return error.SkipZigTest;
var sh = TestShell.start() orelse return error.SkipZigTest;
defer sh.stop();
// At the prompt the shell is the foreground: there is nothing to stop.
- signalTty(sh.pid, sh.master, .command);
+ try std.testing.expect(!killJob(sh.pid, sh.master));
sleepMs(100);
try std.testing.expectEqual(@as(libc.pid_t, 0), libc.waitpid(sh.pid, null, libc.W.NOHANG));
sh.send("sleep 30\n");
try std.testing.expect(sh.waitTaken(true, 10_000));
sh.forget();
- signalTty(sh.pid, sh.master, .command);
+ try std.testing.expect(killJob(sh.pid, sh.master));
try std.testing.expect(sh.waitTaken(false, 10_000));
try std.testing.expect(sh.waitText(test_prompt, 10_000));
try std.testing.expectEqual(@as(libc.pid_t, 0), libc.waitpid(sh.pid, null, libc.W.NOHANG));
+
+ // Job control off: the job runs in the shell's own group.
+ sh.forget();
+ sh.send("set +m; sleep 30\n");
+ sleepMs(300);
+ try std.testing.expect(!killJob(sh.pid, sh.master));
+ sh.forget();
+ sh.send("\x03");
+ try std.testing.expect(sh.waitText(test_prompt, 10_000));
+ try std.testing.expectEqual(@as(libc.pid_t, 0), libc.waitpid(sh.pid, null, libc.W.NOHANG));
}
test "a retired shell that ignores the hangup is killed and reaped, not left a zombie" {