diff options
| author | Gabriel Schneider <[email protected]> | 2026-09-29 15:47:52 -0300 |
|---|---|---|
| committer | Gabriel Schneider <[email protected]> | 2026-10-01 00:12:16 -0300 |
| commit | c2e58c827bd8d2497c86b2eca23ac096cb972296 (patch) | |
| tree | b9e46bee4978e97952d377b6e21f20338dc08d2c /src | |
| parent | 9babe3a63ca06b4b0248d1d21ccb74687c5b188d (diff) | |
| download | pardes-c2e58c827bd8d2497c86b2eca23ac096cb972296.tar.gz pardes-c2e58c827bd8d2497c86b2eca23ac096cb972296.zip | |
A ctl or exec line with no newline runs with its write and fails it, unless it needs more
A last line with no newline was held to the close, so printf bogus > ctl answered 0 and failed only in the log, Edit ,s/zzz/y/ too. A write shorter than its Twrite is the whole of what was written (acme takes each write whole): its tail runs then and a failure is the write's. It is held only when the parser says it needs more -- an open { block, an a/c/i text -- or when the write filled its Twrite and may go on (the listener tells the core the room, msize less its header).
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Diffstat (limited to 'src')
| -rw-r--r-- | src/9p_io.zig | 2 | ||||
| -rw-r--r-- | src/fs.zig | 4 | ||||
| -rw-r--r-- | src/ninep/pane.zig | 10 | ||||
| -rw-r--r-- | src/ninep/tree.zig | 41 |
4 files changed, 53 insertions, 4 deletions
diff --git a/src/9p_io.zig b/src/9p_io.zig index 91240b57..7f490f81 100644 --- a/src/9p_io.zig +++ b/src/9p_io.zig @@ -261,6 +261,7 @@ pub const Listener = struct { return conn.reply(&later, ""); } const core = l.core; + core.fs.write_room = if (conn.engine.protocol.msize > 24) conn.engine.protocol.msize - 24 else 0; const epoch = pardes.turn.epoch; const restores = pardes.turn.restores; // Asked first: a release that runs a held line has none after. @@ -506,6 +507,7 @@ pub const Listener = struct { /// Answers one QUIC request, on the editor's thread. fn step(l: *Listener, srv: *Srv, req: pardes.ctlfs.Req) void { + l.core.fs.write_room = if (srv.protocol.msize > 24) srv.protocol.msize - 24 else 0; const reply = l.core.serveFs(req); srv.reply(&reply, l.core.fsPayload(reply)); if (req.op == .release) l.collectOs(); @@ -1326,6 +1326,10 @@ pub const Namespace = struct { /// A 9P write asked a language server something: the answer count it /// waits to change (`turn.lsp_answers`), so its failure is the write's. lsp_answer_at: ?u64 = null, + /// The most a Twrite of the request being served can carry (msize less + /// its header), 0 when unknown: a write shorter than it is the whole of + /// what its client wrote, one that fills it may be cut and go on. + write_room: u32 = 0, /// The serial of the pane the root's exec or look last ran a line at /// (the keyboard's), 0 before one: its err record names that pane. ran_at: u32 = 0, diff --git a/src/ninep/pane.zig b/src/ninep/pane.zig index 9f85a75f..d760bebd 100644 --- a/src/ninep/pane.zig +++ b/src/ninep/pane.zig @@ -1153,13 +1153,19 @@ test "a name cut across writes is one name, applied once at its newline or its c const node = Node.of(serial, .name); const h = call(p, .{ .tag = 1, .op = .open, .node = node, .omode = 1 }).reply.handle; try testing.expect(h != 0); + // A piece that fills its Twrite goes on in the next. + p.fs.write_room = "/tmp/pardes-na".len; _ = call(p, .{ .tag = 2, .op = .write, .node = node, .handle = h, .data = "/tmp/pardes-na" }); + p.fs.write_room = 0; try testing.expectEqualStrings("/test.txt", nameOf(p.panes[p.paneBySerial(serial).?].?)); _ = call(p, .{ .tag = 2, .op = .write, .node = node, .handle = h, .data = "me.txt\n" }); try testing.expectEqualStrings("/tmp/pardes-name.txt", nameOf(p.panes[p.paneBySerial(serial).?].?)); - // No newline: the close applies it. - _ = call(p, .{ .tag = 2, .op = .write, .node = node, .handle = h, .data = "/tmp/pardes-closed.txt" }); _ = call(p, .{ .tag = 3, .op = .release, .node = node, .handle = h, .opened = true }); + // No newline, the whole write: applied at once, on its own open. + const h3 = call(p, .{ .tag = 1, .op = .open, .node = node, .omode = 1 }).reply.handle; + _ = call(p, .{ .tag = 2, .op = .write, .node = node, .handle = h3, .data = "/tmp/pardes-closed.txt" }); + try testing.expectEqualStrings("/tmp/pardes-closed.txt", nameOf(p.panes[p.paneBySerial(serial).?].?)); + _ = call(p, .{ .tag = 3, .op = .release, .node = node, .handle = h3, .opened = true }); try testing.expectEqualStrings("/tmp/pardes-closed.txt", nameOf(p.panes[p.paneBySerial(serial).?].?)); } diff --git a/src/ninep/tree.zig b/src/ninep/tree.zig index d6f4211b..0960f00b 100644 --- a/src/ninep/tree.zig +++ b/src/ninep/tree.zig @@ -1073,7 +1073,18 @@ fn write(p: *Pardes, req: Req, target: Target) Reply { // to fail unseen at the close. if (resultsFile(target)) for (req.data) |c| if ((c < ' ' and c != '\t' and c != '\n' and c != '\r') or c == 0x7f) return failText(req.tag, E.INVAL, ctl.e_control); o.pending.appendSlice(p.gpa, req.data) catch return Reply.fail(req.tag, E.NOMEM); - const end = ctl.completeEnd(p, o.pending.items); + var end = ctl.completeEnd(p, o.pending.items); + // A tail with no newline is a whole line when the write is the whole of + // what its client wrote (shorter than a Twrite can carry): it runs now, + // and its failure is this write's, as acme takes each write whole. Held + // only when it is not whole yet -- an open `{` block, an `a`/`c`/`i` + // text awaiting its `.` -- or the write filled its Twrite and may go on. + const cut = p.fs.write_room != 0 and req.data.len >= p.fs.write_room; + if (end < o.pending.items.len and !cut) { + o.pending.append(p.gpa, '\n') catch return Reply.fail(req.tag, E.NOMEM); + const whole = ctl.completeEnd(p, o.pending.items); + if (whole == o.pending.items.len) end = whole else o.pending.shrinkRetainingCapacity(o.pending.items.len - 1); + } if (end == 0) { if (o.pending.items.len <= pending_cap) return .{ .tag = req.tag, .written = @intCast(req.data.len) }; o.pending.clearRetainingCapacity(); @@ -1580,8 +1591,12 @@ test "a command line cut across writes runs once whole, and the last runs at rel const exec = @intFromEnum(TopFile.exec); const h = call(p, .{ .tag = 1, .op = .open, .node = exec, .omode = 1 }).reply.handle; try testing.expect(h != 0); - for ([_][]const u8{ "Msg he", "llo\nMsg wor", "ld\nMsg tail" }) |piece| + // Each piece fills its Twrite (a client's one write cut at msize). + for ([_][]const u8{ "Msg he", "llo\nMsg wor", "ld\nMsg tail" }) |piece| { + p.fs.write_room = @intCast(piece.len); try testing.expectEqual(Status.ok, call(p, .{ .tag = 2, .op = .write, .node = exec, .handle = h, .data = piece }).reply.status); + } + p.fs.write_room = 0; try testing.expect(th.logHas(p, " hello\n") and th.logHas(p, " world\n")); try testing.expect(!th.logHas(p, " he\n") and !th.logHas(p, " tail\n")); _ = call(p, .{ .tag = 3, .op = .release, .node = exec, .handle = h, .opened = true }); @@ -1596,6 +1611,28 @@ test "a command line cut across writes runs once whole, and the last runs at rel try testing.expectEqualStrings("x\none\ntwo\n", p.panes[0].?.file.?.content); } +test "a write with no newline, whole in its Twrite, runs then and fails the write; one needing more waits" { + const p = try th.withFile(testing.allocator, "abc\n"); + defer p.deinit(); + const serial = serialOf(p); + const ctl_node = Node.of(serial, .ctl); + const c = call(p, .{ .tag = 1, .op = .open, .node = ctl_node, .omode = 1 }).reply.handle; + p.fs.write_room = 8192; + defer p.fs.write_room = 0; + try testing.expectEqual(Status.err, call(p, .{ .tag = 2, .op = .write, .node = ctl_node, .handle = c, .data = "bogus" }).reply.status); + try testing.expectEqual(Status.err, call(p, .{ .tag = 3, .op = .write, .node = ctl_node, .handle = c, .data = "Edit ,s/zzz/y/" }).reply.status); + // An `a` needs its text and `.`: held, not run, not failed. + try testing.expectEqual(Status.ok, call(p, .{ .tag = 4, .op = .write, .node = ctl_node, .handle = c, .data = "Edit $a" }).reply.status); + try testing.expectEqual(Status.ok, call(p, .{ .tag = 5, .op = .write, .node = ctl_node, .handle = c, .data = "\nmore\n.\n" }).reply.status); + try testing.expectEqualStrings("abc\nmore\n", p.panes[0].?.file.?.content); + _ = call(p, .{ .tag = 6, .op = .release, .node = ctl_node, .handle = c, .opened = true }); + // The root's ctl too. + const root_ctl = @intFromEnum(TopFile.ctl); + const r = call(p, .{ .tag = 7, .op = .open, .node = root_ctl, .omode = 1 }).reply.handle; + try testing.expectEqual(Status.err, call(p, .{ .tag = 8, .op = .write, .node = root_ctl, .handle = r, .data = "bogus" }).reply.status); + _ = call(p, .{ .tag = 9, .op = .release, .node = root_ctl, .handle = r, .opened = true }); +} + test "look and exec read back what their own open's write touched; a fresh open reads the session's last" { const p = try th.withFile(testing.allocator, "x\n"); defer p.deinit(); |
