summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/Messages.zig12
-rw-r--r--src/builtins.zig2
-rw-r--r--src/dump.zig2
-rw-r--r--src/exec.zig4
-rw-r--r--src/fs.zig4
-rw-r--r--src/ninep/ctl.zig7
-rw-r--r--src/ninep/tree.zig31
-rw-r--r--src/pardes.zig12
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| {
diff --git a/src/fs.zig b/src/fs.zig
index 21ccc94e..88ffb4a3 100644
--- a/src/fs.zig
+++ b/src/fs.zig
@@ -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.