summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorGabriel Schneider <[email protected]>2026-09-28 15:02:31 -0300
committerGabriel Schneider <[email protected]>2026-10-01 00:12:15 -0300
commit8d93016e923565e2e8b73aa7b549dd65b5d18a8c (patch)
tree2d0324ea3642404c421ba8b667996ded08f72ba8 /src
parentd70f0029f0808e0a3724150a081015e0f943dfb5 (diff)
downloadpardes-8d93016e923565e2e8b73aa7b549dd65b5d18a8c.tar.gz
pardes-8d93016e923565e2e8b73aa7b549dd65b5d18a8c.zip
Kill signals a command only while it runs, and only the pane it was meant for
A signal effect carried only a slot, so a Kill followed in the same step by the pane's closing and its slot's reuse signalled the new pane's child; and a command pane whose child had exited and been reaped kept a process group id another process could take. The effect now carries the pane's serial, checked as it is performed, and a host signals no command whose exit it has recorded; the core already sends Kill only to a command it has not been told is done. Co-Authored-By: Claude Opus 5.5 <[email protected]>
Diffstat (limited to 'src')
-rw-r--r--src/builtins.zig2
-rw-r--r--src/detached/server.zig3
-rw-r--r--src/gui/gui.zig3
-rw-r--r--src/macos.zig3
-rw-r--r--src/ninep/pty.zig2
-rw-r--r--src/pardes.zig23
-rw-r--r--src/tty/tty.zig4
7 files changed, 32 insertions, 8 deletions
diff --git a/src/builtins.zig b/src/builtins.zig
index 4815a114..5273cf37 100644
--- a/src/builtins.zig
+++ b/src/builtins.zig
@@ -440,7 +440,7 @@ pub const Kill = struct {
if (std.mem.eql(u8, w, first)) break;
} else continue;
}
- c.p.emit(.{ .signal_pty = .{ .pane = @intCast(id), .sig = .term } });
+ c.p.emit(.{ .signal_pty = .{ .pane = @intCast(id), .serial = pane.serial, .sig = .term } });
stopped = true;
continue;
};
diff --git a/src/detached/server.zig b/src/detached/server.zig
index 202eafb3..5bf2384b 100644
--- a/src/detached/server.zig
+++ b/src/detached/server.zig
@@ -523,7 +523,8 @@ pub const Session = struct {
const s = of(ctx);
if (pane >= s.ptys.len) return;
const pt = s.ptys[pane];
- if (pt.fd < 0) return;
+ // A command whose exit is recorded is reaped: its group may be another's.
+ if (pt.fd < 0 or pt.cmd.exited) return;
host_io.signalTty(pt.pid, pt.fd, sig);
}
diff --git a/src/gui/gui.zig b/src/gui/gui.zig
index 175db9e2..46bd5ffc 100644
--- a/src/gui/gui.zig
+++ b/src/gui/gui.zig
@@ -4246,7 +4246,8 @@ fn ptyResize(ctx: ?*anyopaque, pane: u8, cols: u16, rows: u16) void {
fn ptySignal(ctx: ?*anyopaque, pane: u8, sig: pardes.PtySignal) void {
const s = shellOf(ctx);
- if (s.ptys[pane]) |pt| if (pt.fd >= 0) host_io.signalTty(pt.pid, pt.fd, sig);
+ // A command whose exit is recorded is reaped: its group may be another's.
+ if (s.ptys[pane]) |pt| if (pt.fd >= 0 and !pt.cmd.exited) host_io.signalTty(pt.pid, pt.fd, sig);
}
fn ttyTaken(ctx: ?*anyopaque, pane: u8) bool {
diff --git a/src/macos.zig b/src/macos.zig
index 1327b064..d6ceb535 100644
--- a/src/macos.zig
+++ b/src/macos.zig
@@ -2481,7 +2481,8 @@ fn ptyResize(ctx: ?*anyopaque, pane: u8, cols: u16, rows: u16) void {
fn ptySignal(ctx: ?*anyopaque, pane: u8, sig: pardes.PtySignal) void {
const st = hostState(ctx);
- if (st.ptys[pane]) |pt| host_io.signalTty(pt.pid, pt.file.handle, sig);
+ // A command whose exit is recorded is reaped: its group may be another's.
+ if (st.ptys[pane]) |pt| if (!pt.cmd.exited) host_io.signalTty(pt.pid, pt.file.handle, sig);
}
fn ttyTaken(ctx: ?*anyopaque, pane: u8) bool {
diff --git a/src/ninep/pty.zig b/src/ninep/pty.zig
index 6065dab2..88dafdfc 100644
--- a/src/ninep/pty.zig
+++ b/src/ninep/pty.zig
@@ -65,7 +65,7 @@ fn verb(p: *Pardes, id: usize, line: []const u8, apply: bool) bool {
const which = signalNamed(words.next() orelse return false) orelse return false;
if (words.next() != null) return false;
if (!apply) return true;
- p.emit(.{ .signal_pty = .{ .pane = @intCast(id), .sig = which } });
+ p.emit(.{ .signal_pty = .{ .pane = @intCast(id), .serial = pane.serial, .sig = which } });
},
.exec => {
if (words.next() != null) return false;
diff --git a/src/pardes.zig b/src/pardes.zig
index 1539637c..5ba95be3 100644
--- a/src/pardes.zig
+++ b/src/pardes.zig
@@ -1468,6 +1468,22 @@ test "a command pane shows how its command ended, and the next command there run
var want: [64]u8 = undefined;
try std.testing.expect(std.mem.indexOf(u8, log.bytes, try std.fmt.bufPrint(&want, "run {d} make\n", .{pane.serial})) != null);
try std.testing.expect(std.mem.indexOf(u8, log.bytes, try std.fmt.bufPrint(&want, "exit {d} 2\n", .{pane.serial})) != null);
+ // A signal for a pane is not performed on whoever takes its slot.
+ const Count = struct {
+ var signals: usize = 0;
+ const vtable: Host.VTable = .{ .pty_signal = signal };
+ fn signal(_: ?*anyopaque, _: u8, _: PtySignal) void {
+ signals += 1;
+ }
+ };
+ const host_before = p.host;
+ p.host = .{ .vtable = &Count.vtable };
+ try std.testing.expect(p.executeBuiltinLine(0, "Kill make"));
+ try p.removePane(dst, null);
+ _ = try p.newCommand(dst, "", "other");
+ while (p.nextEffect()) |effect| if (effect == .signal_pty) p.perform(effect);
+ try std.testing.expectEqual(@as(usize, 0), Count.signals);
+ p.host = host_before;
// A command line longer than a command pane takes is refused.
const long = "x" ** (exec.command_max + 1);
try std.testing.expectEqual(@as(?usize, null), exec.execute(p, 1, long));
@@ -3187,7 +3203,8 @@ pub const Effect = union(enum) {
spawn: struct { pane: u8, serial: u32, cwd: Buf(effect_path_cap) },
write: struct { pane: u8, bytes: Buf(64) },
resize_pty: struct { pane: u8, cols: u16, rows: u16 },
- signal_pty: struct { pane: u8, sig: PtySignal },
+ /// For the pane whose serial is `serial`, as `spawn` is (perform).
+ signal_pty: struct { pane: u8, serial: u32, sig: PtySignal },
/// The terminal pane in this slot is gone: the shell hangs up its pty
/// and ends the shell, rather than leave it running with nobody to read.
close_pty: struct { pane: u8 },
@@ -4665,7 +4682,9 @@ pub const Pardes = struct {
// screen, and the bytes are dropped rather than transcribed.
.write => |w| if (v.pty_write) |f| f(p.host.ctx, w.pane, w.bytes.slice()),
.resize_pty => |r| if (v.pty_resize) |f| f(p.host.ctx, r.pane, r.cols, r.rows),
- .signal_pty => |s| if (v.pty_signal) |f| f(p.host.ctx, s.pane, s.sig),
+ // A signal for a pane that closed, its slot retaken in the same
+ // step, would reach another pane's child.
+ .signal_pty => |s| if (if (p.panes[s.pane]) |pane| pane.serial != s.serial else true) {} else if (v.pty_signal) |f| f(p.host.ctx, s.pane, s.sig),
.close_pty => |c| if (v.close_pty) |f| f(p.host.ctx, c.pane),
.open_link => |u| if (v.open_link) |f|
f(p.host.ctx, u.slice())
diff --git a/src/tty/tty.zig b/src/tty/tty.zig
index 8e3809b0..14417cd0 100644
--- a/src/tty/tty.zig
+++ b/src/tty/tty.zig
@@ -1250,7 +1250,9 @@ const Shell = struct {
fn ptySignal(ctx: ?*anyopaque, pane: u8, sig: pardes.PtySignal) void {
const s = of(ctx);
- if (s.ptys[pane]) |pt| host_io.signalTty(pt.pid, pt.file.handle, sig);
+ // A command whose exit is recorded is reaped: its group may be
+ // another's by now.
+ if (s.ptys[pane]) |pt| if (!pt.cmd.exited) host_io.signalTty(pt.pid, pt.file.handle, sig);
}
/// The pane is gone: hang its pty up, which the kernel passes on to the