summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGabriel Schneider <[email protected]>2026-10-01 09:58:15 -0300
committerGabriel Schneider <[email protected]>2026-10-01 10:36:27 -0300
commit70881bb4edd15fe918ee5fed4ed3e8922bc7fc6a (patch)
tree18d80a3172d054cd152f78f56d82150f914b81f2
parentcaa8ed077e8b9b67d18f18b3ec46891bd8d310a7 (diff)
downloadpardes-70881bb4edd15fe918ee5fed4ed3e8922bc7fc6a.tar.gz
pardes-70881bb4edd15fe918ee5fed4ed3e8922bc7fc6a.zip
A command or terminal pane refused for room is refused before anything is logged or answered of it, and a serial is never handed out twice
A command pane with no room logged `new N` and `run N ls`, was answered to its exec, then went with `del N` and no `exit N`; and sync gave N back (`next_serial -= 1`) to the next pane made, so a script holding N found another pane under it. Tty did the same. Both now take a refused placement back at once, before the run is logged or the serial answered (the pane was never announced, so it leaves no record), and fail the write with no room. The decrement is gone: gaps are fine, and a serial once said names one pane for good. Co-Authored-By: Claude Opus 5.5 <[email protected]>
-rw-r--r--src/exec.zig23
-rw-r--r--src/ninep/ctl.zig30
-rw-r--r--src/ninep/tree.zig5
-rw-r--r--src/pardes.zig10
4 files changed, 59 insertions, 9 deletions
diff --git a/src/exec.zig b/src/exec.zig
index ca48bed1..080d0aea 100644
--- a/src/exec.zig
+++ b/src/exec.zig
@@ -413,9 +413,28 @@ fn spawnTtyWithMount(p: *Pardes, from: usize, v9fs: bool) ?*Pane {
nt.v9fs_on_spawn = v9fs;
nt.body.mode = .tty;
placeNew(p, from, from, free, .shell);
+ if (refusedPlacement(p, free)) {
+ p.reportError(from, "Tty", error.NoPaneRoom);
+ return null;
+ }
return nt;
}
+/// A pane placeNew found no room for: taken back now, before anything is
+/// logged of it or answered with its serial (it was never announced), so
+/// no `new`/`del` pair and no serial a script was told of is left behind.
+fn refusedPlacement(p: *Pardes, id: usize) bool {
+ if (!p.unplaced.isSet(id)) return false;
+ p.unplaced.unset(id);
+ p.removePane(id, null) catch {};
+ // As sync does for a refused pane: none it squeezed stays under its
+ // minimum.
+ for (0..p.ncol) |c| if (!layout.columnAtMinimums(p, c)) {
+ _ = layout.shareColumn(p, c);
+ };
+ return true;
+}
+
/// Whether a shell would be started in `dir` and `dir` is not there: asked
/// only of a host that starts shells, and only of a local path.
pub fn missingDir(p: *Pardes, dir: []const u8) bool {
@@ -811,6 +830,10 @@ fn runCommand(p: *Pardes, from: usize, line: []const u8) ?usize {
pane.command_from = winid(p, src);
echoCommand(p, pane, line);
placeNew(p, from, from, free, .command);
+ if (refusedPlacement(p, free)) {
+ p.reportError(from, "exec", error.NoPaneRoom);
+ return null;
+ }
noteRun(p, pane, "run", line);
return free;
}
diff --git a/src/ninep/ctl.zig b/src/ninep/ctl.zig
index 40ffebc8..16fbabb4 100644
--- a/src/ninep/ctl.zig
+++ b/src/ninep/ctl.zig
@@ -4379,3 +4379,33 @@ test "a Mount dial with a network it does not know is a bad dial address, EINVAL
try testing.expectEqual(E.INVAL, r.errno());
try testing.expectStringStartsWith(r.reply.ename, "Mount: bad dial address: nowhere!x");
}
+
+test "a command or a terminal refused for room is refused before anything is logged of it, and its serial is never handed out again" {
+ const p = try withFile(testing.allocator, "x\n");
+ defer p.deinit();
+ const exec = Node.of(serialOf(p), .exec);
+ var buf: [32]u8 = undefined;
+ for ([_][]const u8{ "ls\n", "Tty\n" }) |line| {
+ // Each a pane of its own until the column has no room for one more.
+ var before = p.next_serial;
+ var tries: usize = 0;
+ while (tries < pardes.MAX_PANES) : (tries += 1) {
+ before = p.next_serial;
+ const r = wr(p, exec, line);
+ p.sync();
+ if (r.reply.status == .err) break;
+ } else return error.NeverRefused;
+ // Nothing said of the refused one: no new, no run, no del.
+ for (before + 1..p.next_serial + 1) |n| {
+ try testing.expect(!th.logHas(p, try std.fmt.bufPrint(&buf, "new {d} ", .{n})));
+ try testing.expect(!th.logHas(p, try std.fmt.bufPrint(&buf, "run {d} ", .{n})));
+ try testing.expect(!th.logHas(p, try std.fmt.bufPrint(&buf, "del {d} ", .{n})));
+ }
+ // And the next pane made takes a serial past it.
+ const spent = p.next_serial;
+ p.update(.{ .resize = .{ .cols = 200, .rows = 200 } });
+ const made = try th.newPane(p);
+ try testing.expect(made > spent);
+ p.update(.{ .resize = .{ .cols = 80, .rows = 24 } });
+ }
+}
diff --git a/src/ninep/tree.zig b/src/ninep/tree.zig
index e6c139c1..40543ecf 100644
--- a/src/ninep/tree.zig
+++ b/src/ninep/tree.zig
@@ -1905,10 +1905,11 @@ test "no placement leaves a pane shorter than its tag and two rows; a full colum
} else return error.NeverRefused;
try testing.expectEqual(E.NOSPC, refused.errno());
try testing.expect(std.mem.indexOf(u8, refused.reply.ename, "no space for a pane in that column") != null);
- // The refused pane was never there: no serial spent on it.
+ // The refused pane was never there; its serial is not handed out again
+ // (gaps are fine: a serial once said names one pane for good).
const before = p.next_serial;
try testing.expectEqual(E.NOSPC, call(p, .{ .tag = 1, .op = .open, .node = new }).errno());
- try testing.expectEqual(before, p.next_serial);
+ try testing.expectEqual(before + 1, p.next_serial);
p.sync();
var count: usize = 0;
for (p.panes) |slot| count += @intFromBool(slot != null);
diff --git a/src/pardes.zig b/src/pardes.zig
index 0690b7eb..4e578791 100644
--- a/src/pardes.zig
+++ b/src/pardes.zig
@@ -7119,13 +7119,9 @@ pub const Pardes = struct {
var refused = false;
while (p.unplaced.findFirstSet()) |id| {
p.unplaced.unset(id);
- if (p.panes[id]) |pane| {
- // Refused, it was never there: the serial it took is the
- // next pane's, as a refused Newcol's column serial is.
- const newest = pane.serial == p.next_serial;
- p.removePane(id, null) catch {};
- if (newest and p.panes[id] == null) p.next_serial -= 1;
- }
+ // Refused, it was never there. Its serial is not handed out
+ // again: one logged or answered names this pane for good.
+ if (p.panes[id] != null) p.removePane(id, null) catch {};
refused = true;
}
// A refused pane gone, no pane it squeezed stays under its minimum.