diff options
| author | Gabriel Schneider <[email protected]> | 2026-09-29 00:40:03 -0300 |
|---|---|---|
| committer | Gabriel Schneider <[email protected]> | 2026-10-01 00:12:15 -0300 |
| commit | 6732ce6edcf1562d3e126ed02f96466ab07c498e (patch) | |
| tree | 77c68f4357b335e67b34f19e248f1ea00240e0eb /src | |
| parent | fad6bf0cc7cc2cb579d9ad03147a7014906f6b92 (diff) | |
| download | pardes-6732ce6edcf1562d3e126ed02f96466ab07c498e.tar.gz pardes-6732ce6edcf1562d3e126ed02f96466ab07c498e.zip | |
A desktop session holds 64 panes, and every route that finds none free says so
Sixteen panes was far too few for a real session. MAX_PANES is now 64 on a
desktop and stays 16 on the board; the 48 more slots cost about 3.7 KB each
in fixed tables (the core 1.33 to 1.45 MB, its surface 19 to 72 KB), and a
pane itself (84 KB and its terminal) is allocated only when it opens. At the
cap, pane/new fails with `pane/new: no pane slots: 64 max` and an err
record rather than a bare ENFILE, a 9P look or exec that would open a pane
fails its write with the same words and reads back empty, and New, Tty,
Tutor and Alt-n say it on the message row (their ctl writes fail with it).
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Diffstat (limited to 'src')
| -rw-r--r-- | src/Messages.zig | 12 | ||||
| -rw-r--r-- | src/builtins.zig | 2 | ||||
| -rw-r--r-- | src/dump.zig | 2 | ||||
| -rw-r--r-- | src/exec.zig | 4 | ||||
| -rw-r--r-- | src/fs.zig | 4 | ||||
| -rw-r--r-- | src/ninep/ctl.zig | 7 | ||||
| -rw-r--r-- | src/ninep/tree.zig | 31 | ||||
| -rw-r--r-- | src/pardes.zig | 12 |
8 files changed, 65 insertions, 9 deletions
diff --git a/src/Messages.zig b/src/Messages.zig index 8b6c795d..7d284e6b 100644 --- a/src/Messages.zig +++ b/src/Messages.zig @@ -445,10 +445,22 @@ pub fn messageLog(m: *const Messages, i: usize) ?*const LoggedMessage { } /// `<operation>: <the error in words>`: FileNotFound reads "file not found". +/// What a route that would open a pane says when every slot is taken. +pub const no_pane_slots = "no pane slots: {d} max"; + pub fn reportError(p: *Pardes, id: usize, operation: []const u8, err: anyerror) void { var buf: [256]u8 = undefined; var w = std.Io.Writer.fixed(&buf); w.print("{s}: ", .{operation}) catch {}; + // Every route that opens a pane says the cap the same way. + if (err == error.NoPaneSlots) { + w.print(no_pane_slots, .{pardes.MAX_PANES}) catch {}; + // A 9P look or exec that would open the pane fails with it too. + p.fs.no_pane_slot = true; + @memcpy(p.fs.ename[0..w.buffered().len], w.buffered()); + p.fs.no_pane_slot_len = @intCast(w.buffered().len); + return reportFailure(p, id, w.buffered()); + } for (@errorName(err), 0..) |c, i| { if (std.ascii.isUpper(c) and i > 0) w.writeByte(' ') catch break; w.writeByte(std.ascii.toLower(c)) catch break; diff --git a/src/builtins.zig b/src/builtins.zig index 5b1b4dcb..32e3cd84 100644 --- a/src/builtins.zig +++ b/src/builtins.zig @@ -1102,7 +1102,7 @@ pub const Joincol = struct { pub const Tutor = struct { pub const scope: Scope = .session; pub fn run(c: Ctx) void { - const free = c.p.freeSlot() orelse return; + const free = c.p.freeSlot() orelse return c.p.reportError(c.id, "Tutor", error.NoPaneSlots); _ = c.p.openTutorView(free) catch return; exec.placeNew(c.p, c.id, c.id, free, .doc); // a doc like any other } diff --git a/src/dump.zig b/src/dump.zig index 77e2c2ba..61aeabbf 100644 --- a/src/dump.zig +++ b/src/dump.zig @@ -73,7 +73,7 @@ pub fn defaultDirectory(buf: []u8) ?[]const u8 { pub const magic = "pardes-dump"; pub const version: u32 = 1; -pub const max_panes: usize = 16; +pub const max_panes: usize = MAX_PANES; pub const max_cols: usize = 6; /// Output arguments are typed in the same bounded one-line tag storage. Keep /// the schema limit named independently so a dump reader can validate it diff --git a/src/exec.zig b/src/exec.zig index 2f279d8e..d4480e00 100644 --- a/src/exec.zig +++ b/src/exec.zig @@ -312,7 +312,7 @@ pub fn spawnV9fsTty(p: *Pardes, from: usize) void { fn spawnTtyWithMount(p: *Pardes, from: usize, v9fs: bool) ?*Pane { const src = p.panes[from] orelse return null; const free = p.freeSlot() orelse { - p.reportFailure(from, "Tty: no pane free"); + p.reportError(from, "Tty", error.NoPaneSlots); return null; }; const nt = p.newShell(free, Pardes.paneDir(src)) catch |err| { @@ -609,7 +609,7 @@ fn runCommand(p: *Pardes, from: usize, line: []const u8) ?usize { return id; } const free = p.freeSlot() orelse { - p.reportFailure(from, "exec: no pane free for the command"); + p.reportError(from, "exec", error.NoPaneSlots); return null; }; const pane = p.newCommand(free, dir, line) catch |err| { @@ -1274,6 +1274,10 @@ pub const Namespace = struct { /// 0 the workspace's, a column's its index + 1; and whether the newline /// that ended its last write is held back. header_rewrite: ?u32 = null, + /// A look or exec a 9P write made found every pane slot taken; the + /// write fails with what was said, kept at the head of `ename`. + no_pane_slot: bool = false, + no_pane_slot_len: u16 = 0, header_held: bool = false, late_failure: [256]u8 = undefined, late_failure_len: u16 = 0, diff --git a/src/ninep/ctl.zig b/src/ninep/ctl.zig index 11de3c1c..325fc8c8 100644 --- a/src/ninep/ctl.zig +++ b/src/ninep/ctl.zig @@ -138,7 +138,14 @@ pub fn command(p: *Pardes, req: Req, serial: ?u32, exec: bool) Reply { if (!apply) continue; const id = if (serial) |s| p.paneBySerial(s) orelse break else p.active; if (p.panes[id] == null) return Reply.fail(req.tag, E.NOENT); + p.fs.no_pane_slot = false; run(p, id, exec, text); + // A pane it would have opened had no slot: the write fails, and + // a look reads back nothing. + if (p.fs.no_pane_slot) { + p.fs.results_len = 0; + return tree.failText(req.tag, E.IO, p.fs.ename[0..p.fs.no_pane_slot_len]); + } } } return .{ .tag = req.tag, .written = @intCast(req.data.len) }; diff --git a/src/ninep/tree.zig b/src/ninep/tree.zig index 3be75960..c4ade1f7 100644 --- a/src/ninep/tree.zig +++ b/src/ninep/tree.zig @@ -664,7 +664,7 @@ fn open(p: *Pardes, req: Req, target: Target) Reply { // pane it made, so reading it twice answers the same one and // closing it leaves the pane alone. .new => { - const slot = p.freeSlot() orelse return Reply.fail(req.tag, E.NFILE); + const slot = p.freeSlot() orelse return failText(req.tag, E.NFILE, std.fmt.bufPrint(&p.fs.ename, "pane/new: " ++ pardes.Messages.no_pane_slots, .{pardes.MAX_PANES}) catch "no pane slots"); p.newScratchFor(p.active, .fs); const made = p.panes[slot] orelse return Reply.fail(req.tag, E.NFILE); return .{ .tag = req.tag, .handle = made.serial }; @@ -1284,3 +1284,32 @@ test "editor paths resolve to the same nodes the wire serves" { try testing.expect(resolveSelf(p, "pane/new/body") == null); try testing.expectEqual(before, p.next_serial); } + +test "at the pane cap, pane/new, look and New each say so, and look reads back empty" { + const p = try th.withFile(testing.allocator, "x\n"); + defer p.deinit(); + const new = @intFromEnum(TopFile.new); + while (p.freeSlot() != null) try testing.expectEqual(Status.ok, call(p, .{ .tag = 1, .op = .open, .node = new }).reply.status); + var want: [64]u8 = undefined; + const said = try std.fmt.bufPrint(&want, "no pane slots: {d} max", .{pardes.MAX_PANES}); + const refused = call(p, .{ .tag = 2, .op = .open, .node = new }); + try testing.expectEqual(E.NFILE, refused.errno()); + try testing.expect(std.mem.endsWith(u8, refused.reply.ename, said)); + try testing.expect(th.logHas(p, said)); + // A look that would open a pane fails its write, and says the same. + var tmp = testing.tmpDir(.{}); + defer tmp.cleanup(); + try tmp.dir.writeFile(testing.io, .{ .sub_path = "f.txt", .data = "f\n" }); + var dir_buf: [4096]u8 = undefined; + const dir = dir_buf[0..try tmp.dir.realPath(testing.io, &dir_buf)]; + var line: [4200]u8 = undefined; + const look_node = @intFromEnum(TopFile.look); + const looked = th.wr(p, look_node, try std.fmt.bufPrint(&line, "{s}/f.txt\n", .{dir})); + try testing.expect(looked.errno() != 0); + try testing.expect(std.mem.indexOf(u8, looked.reply.ename, said) != null); + try testing.expectEqualStrings("", th.rd(p, look_node, 0, 64).bytes); + // New from a pane's ctl fails as well. + const ctl_node = Node.of(p.panes[0].?.serial, .ctl); + const made = th.wr(p, ctl_node, "New\n"); + try testing.expect(std.mem.indexOf(u8, made.reply.ename, said) != null); +} diff --git a/src/pardes.zig b/src/pardes.zig index 8dbe6744..a6a088de 100644 --- a/src/pardes.zig +++ b/src/pardes.zig @@ -250,7 +250,11 @@ pub const pdf_raster_policy: PdfRasterPolicy = switch (platform) { pub const Pane = panes.Pane; const Text = panes.Text; -pub const MAX_PANES = 16; +/// Pane slots: 64 on a desktop, where a session keeps many files and +/// terminals open; the board keeps its 16. A slot costs its share of the +/// fixed tables (MAX_PANES-sized arrays in the core and its surface) whether +/// or not a pane is in it; a pane itself is allocated when it opens. +pub const MAX_PANES = if (platform == .esp32p4) 16 else 64; pub const Layer = @import("Layer.zig"); pub const RowMetrics = Layer.Metrics; pub const TagHit = Layer.TagHit; @@ -4797,7 +4801,7 @@ pub const Pardes = struct { /// A scratch for New (`.scratch`) or 9P's pane/new (`.fs`). pub fn newScratchFor(p: *Pardes, from_id: usize, kind: exec.NewPane) void { - const free = p.freeSlot() orelse return; + const free = p.freeSlot() orelse return p.reportError(from_id, "New", error.NoPaneSlots); _ = p.newScratch(from_id, free) catch return; exec.placeNew(p, from_id, from_id, free, kind); } @@ -5305,7 +5309,7 @@ pub const Pardes = struct { p.raw_hover_intent = false; look.cancelLookHover(p); const reply = ctlfs.handle(p, req); - if (reply.status == .err and (req.op == .write or req.op == .setattr)) ctlfs.events.noteError(p, req, reply); + if (reply.status == .err and (req.op == .write or req.op == .setattr or (req.op == .open and reply.errno == ctlfs.E.NFILE))) ctlfs.events.noteError(p, req, reply); // The request was a whole step of its own, so it settles the way a // step does: the cursor and scroll reconciled, the scripted panes // told, and the panes it made announced to /log now rather than at @@ -5665,7 +5669,7 @@ pub const Pardes = struct { // global window ops (any mode): Alt-n new terminal below, Alt-c move // the active terminal into a fresh column if (hit(key, config.new_shell_below)) { - const free = p.freeSlot() orelse return; + const free = p.freeSlot() orelse return p.reportError(p.active, "Tty", error.NoPaneSlots); const nt = p.newShell(free, "") catch return; nt.greet = true; // From a tag, as a 9P new is: acme's t->w == nil. |
