diff options
| author | Gabriel Schneider <[email protected]> | 2026-09-28 12:06:13 -0300 |
|---|---|---|
| committer | Gabriel Schneider <[email protected]> | 2026-10-01 00:12:14 -0300 |
| commit | 8b18331f5e3f6c346b992019163ec3f5b6e6b721 (patch) | |
| tree | f6e0236af8b0ac0428078d5ba512df10a1f3f43d | |
| parent | 990c2e1b184e9cb1fddd8eb1b05a472cfb113dde (diff) | |
| download | pardes-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]>
| -rw-r--r-- | docs/fs.md | 5 | ||||
| -rw-r--r-- | src/Messages.zig | 5 | ||||
| -rw-r--r-- | src/builtins.zig | 36 | ||||
| -rw-r--r-- | src/detached/server.zig | 9 | ||||
| -rw-r--r-- | src/gui/gui.zig | 8 | ||||
| -rw-r--r-- | src/host_io.zig | 35 | ||||
| -rw-r--r-- | src/macos.zig | 7 | ||||
| -rw-r--r-- | src/pardes.zig | 8 | ||||
| -rw-r--r-- | src/tty/tty.zig | 7 |
9 files changed, 101 insertions, 19 deletions
@@ -112,7 +112,10 @@ started, and `Kill make ls`, those whose line begins with one of the words. A command pardes started is a line it typed into a terminal (a word written to `exec`, a middle click on one, a `pty/run`), from its shell's start mark (C) to its end mark (D); Kill sends its foreground job SIGTERM -- acme posts the -"kill" note, which ends a process -- and never signals the shell itself -- +"kill" note, which ends a process -- and never signals the shell itself; +in a shell running without job control (`set +m`) the job shares the +shell's group, so there is none to signal: Kill says `Kill: no job to +signal`, and a write of it to `ctl` fails with that -- and reads every setting there is, one a line, in the words a write of it takes (`Verbose on`, `WindowOpacity 70`, `PanelSlide off`, `DumpDir` bare for the default directory, `LocationsConfig ...`), so writing what it reads diff --git a/src/Messages.zig b/src/Messages.zig index f4ea1c3e..72b3b63c 100644 --- a/src/Messages.zig +++ b/src/Messages.zig @@ -388,6 +388,11 @@ pub fn messageLog(m: *const Messages, i: usize) ?*const LoggedMessage { pub fn reportError(p: *Pardes, id: usize, operation: []const u8, err: anyerror) void { var buf: [256]u8 = undefined; const text = std.fmt.bufPrint(&buf, "{s}: {s}", .{ operation, @errorName(err) }) catch operation; + reportFailure(p, id, text); +} + +/// reportError with the words already chosen. +pub fn reportFailure(p: *Pardes, id: usize, text: []const u8) void { // A builtin a ctl write runs: its first error is also the write's. if (p.fs.no_prompt and p.fs.failure_len == 0) { const n = @min(text.len, p.fs.failure.len); diff --git a/src/builtins.zig b/src/builtins.zig index 0154e707..93eae8ec 100644 --- a/src/builtins.zig +++ b/src/builtins.zig @@ -235,16 +235,23 @@ test "Kill stops the commands pardes typed, bare all and by name those that matc const prompt = "\x1b]133;A;cl=line;aid=pardes\x07$ \x1b]133;B;aid=pardes\x07"; p.update(.{ .output = .{ .pane = @intCast(term), .bytes = prompt } }); while (p.nextEffect()) |_| {} + // The host the core asks: how many it was asked to stop, and whether + // their shells run jobs in groups of their own. const killed = struct { + var asked: usize = 0; + var job_control = true; + const vtable: pardes.Host.VTable = .{ .kill_job = answer }; + fn answer(_: ?*anyopaque, _: u8) bool { + asked += 1; + return job_control; + } fn count(pp: *Pardes, line: []const u8) usize { + asked = 0; _ = pp.executeBuiltinLine(pp.active, line); - var n: usize = 0; - while (pp.nextEffect()) |e| if (e == .signal_pty and e.signal_pty.sig == .command) { - n += 1; - }; - return n; + return asked; } }; + p.host = .{ .vtable = &killed.vtable }; // Nothing running: nothing to stop, and never the shell. try std.testing.expectEqual(@as(usize, 0), killed.count(p, "Kill")); _ = exec.execute(p, term, "make -j8"); @@ -257,6 +264,15 @@ test "Kill stops the commands pardes typed, bare all and by name those that matc try std.testing.expectEqual(@as(usize, 1), killed.count(p, "Kill ls make")); try std.testing.expectEqual(@as(usize, 1), killed.count(p, "Kill")); try std.testing.expect(!p.quit); + // With job control off there is no job apart from the shell: said, and + // over ctl the write fails with it. + killed.job_control = false; + p.fs.no_prompt = true; + try std.testing.expectEqual(@as(usize, 1), killed.count(p, "Kill")); + try std.testing.expectStringStartsWith(p.fs.failure[0..p.fs.failure_len], "Kill: no job to signal"); + p.fs.no_prompt = false; + p.fs.failure_len = 0; + killed.job_control = true; // Ended (a D mark): nothing to stop again. p.update(.{ .output = .{ .pane = @intCast(term), .bytes = "\x1b]133;D;143;aid=pardes\x07" ++ prompt } }); while (p.nextEffect()) |_| {} @@ -362,6 +378,7 @@ pub const Kill = struct { pub fn run(c: Ctx) void { const names = std.mem.trim(u8, c.arg orelse "", " \t\r\n"); var stopped = false; + var jobless = false; for (c.p.panes, 0..) |slot, id| { const pane = slot orelse continue; if (!panes.Terminal.commandRunning(pane)) continue; @@ -372,10 +389,13 @@ pub const Kill = struct { if (std.mem.eql(u8, w, sent.word[0..sent.len])) break; } else continue; } - c.p.emit(.{ .signal_pty = .{ .pane = @intCast(id), .sig = .command } }); - stopped = true; + // Asked of the host now, not queued: the job the marks say is + // running is the one signalled, not whatever runs by then. + if (c.p.hostKillJob(id)) stopped = true else jobless = true; } - if (!stopped and names.len > 0) c.p.reportError(c.id, "Kill", error.NoSuchCommand); + if (stopped) return; + if (jobless) return c.p.reportFailure(c.id, "Kill: no job to signal (its shell runs it without job control)"); + if (names.len > 0) c.p.reportError(c.id, "Kill", error.NoSuchCommand); } }; diff --git a/src/detached/server.zig b/src/detached/server.zig index 0cf1729f..5c0e1487 100644 --- a/src/detached/server.zig +++ b/src/detached/server.zig @@ -353,6 +353,7 @@ pub const Session = struct { .pty_signal = ptySignal, .close_pty = closePaneShell, .tty_taken = ttyTaken, + .kill_job = killJob, .write_file = writeFile, .write_dump = writeDump, .watch_file = watchFile, @@ -521,6 +522,14 @@ pub const Session = struct { return host_io.ttyTaken(pt.pid, pt.fd); } + fn killJob(ctx: ?*anyopaque, pane: u8) bool { + const s = of(ctx); + if (pane >= s.ptys.len) return false; + const pt = s.ptys[pane]; + if (pt.fd < 0) return false; + return host_io.killJob(pt.pid, pt.fd); + } + fn writeFile(ctx: ?*anyopaque, pane: u8, path: []const u8, bytes: []const u8) void { const s = of(ctx); filesystem.write(s.core, path, bytes) catch |err| diff --git a/src/gui/gui.zig b/src/gui/gui.zig index 2369dd28..6978284b 100644 --- a/src/gui/gui.zig +++ b/src/gui/gui.zig @@ -3841,6 +3841,7 @@ const Shell = struct { .pty_signal = ptySignal, .close_pty = closePaneShell, .tty_taken = ttyTaken, + .kill_job = killJob, .write_file = writeFile, .write_dump = writeDump, .watch_file = watchFile, @@ -4196,6 +4197,13 @@ fn ttyTaken(ctx: ?*anyopaque, pane: u8) bool { return host_io.ttyTaken(pt.pid, pt.fd); } +fn killJob(ctx: ?*anyopaque, pane: u8) bool { + const s = shellOf(ctx); + const pt = s.ptys[pane] orelse return false; + if (pt.fd < 0) return false; + return host_io.killJob(pt.pid, pt.fd); +} + fn writeFile(ctx: ?*anyopaque, pane: u8, path: []const u8, bytes: []const u8) void { const s = shellOf(ctx); filesystem.write(s.core, path, bytes) catch |err| 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" { diff --git a/src/macos.zig b/src/macos.zig index ebe8193b..62858887 100644 --- a/src/macos.zig +++ b/src/macos.zig @@ -2312,6 +2312,7 @@ const vtable: pardes.Host.VTable = .{ .pty_signal = ptySignal, .close_pty = closePty, .tty_taken = ttyTaken, + .kill_job = killJob, .write_file = writeFile, .write_dump = writeDump, .watch_file = watchFile, @@ -2435,6 +2436,12 @@ fn ttyTaken(ctx: ?*anyopaque, pane: u8) bool { return host_io.ttyTaken(pt.pid, pt.file.handle); } +fn killJob(ctx: ?*anyopaque, pane: u8) bool { + const st = hostState(ctx); + const pt = st.ptys[pane] orelse return false; + return host_io.killJob(pt.pid, pt.file.handle); +} + fn writeFile(ctx: ?*anyopaque, pane: u8, path: []const u8, bytes: []const u8) void { const st = hostState(ctx); filesystem.write(st.core, path, bytes) catch |err| diff --git a/src/pardes.zig b/src/pardes.zig index 59556649..6e78380a 100644 --- a/src/pardes.zig +++ b/src/pardes.zig @@ -3111,7 +3111,7 @@ pub const PendingWrite = struct { bytes: []u8, off: usize = 0 }; /// `command` is Kill's: SIGTERM to the terminal's foreground job, and never /// to the shell itself when the shell is in the foreground. -pub const PtySignal = enum(u8) { int, term, hup, quit, kill, command }; +pub const PtySignal = enum(u8) { int, term, hup, quit, kill }; /// IO the core wants done. Payloads are inline (fixed buffers): effects are /// queued values with no lifetime ties back into the core. @@ -3928,6 +3928,7 @@ pub const Pardes = struct { pub const setStatus = Messages.setStatus; pub const setMessage = Messages.setMessage; pub const reportError = Messages.reportError; + pub const reportFailure = Messages.reportFailure; /// Running a builtin line, called from everywhere as /// `p.executeBuiltinLine(...)`; it lives with the rest of exec in exec.zig. @@ -4510,6 +4511,11 @@ pub const Pardes = struct { return f(p.host.ctx, @intCast(id)); } + pub fn hostKillJob(p: *const Pardes, id: usize) bool { + const f = p.host.vtable.kill_job orelse return false; + return f(p.host.ctx, @intCast(id)); + } + fn hostWriteFile(p: *Pardes, pane: u8, path: []const u8, bytes: []const u8) void { if (p.host.vtable.write_file) |f| return f(p.host.ctx, pane, path, bytes); // The in-process filesystem reports the same way a real host does, so diff --git a/src/tty/tty.zig b/src/tty/tty.zig index 128a7ead..32f9138f 100644 --- a/src/tty/tty.zig +++ b/src/tty/tty.zig @@ -926,6 +926,7 @@ const Shell = struct { .pty_signal = ptySignal, .close_pty = closePty, .tty_taken = ttyTaken, + .kill_job = killJob, .write_file = writeFile, .write_dump = writeDump, .watch_file = watchFile, @@ -1244,6 +1245,12 @@ const Shell = struct { return host_io.ttyTaken(pt.pid, pt.file.handle); } + fn killJob(ctx: ?*anyopaque, pane: u8) bool { + const s = of(ctx); + const pt = s.ptys[pane] orelse return false; + return host_io.killJob(pt.pid, pt.file.handle); + } + fn writeFile(ctx: ?*anyopaque, pane: u8, path: []const u8, bytes: []const u8) void { const s = of(ctx); filesystem.write(s.core, path, bytes) catch |err| |
