diff options
| -rw-r--r-- | build.zig.zon | 4 | ||||
| -rw-r--r-- | docs/cloud9.md | 6 | ||||
| -rw-r--r-- | docs/fs.md | 23 | ||||
| -rw-r--r-- | src/9p_io.zig | 123 | ||||
| -rw-r--r-- | src/fs-help.txt | 2 | ||||
| -rw-r--r-- | src/ninep/ctl.zig | 14 | ||||
| -rw-r--r-- | src/ninep/events.zig | 4 | ||||
| -rw-r--r-- | src/ninep/pane.zig | 10 | ||||
| -rw-r--r-- | src/ninep/pty.zig | 21 | ||||
| -rw-r--r-- | src/ninep/tree.zig | 15 | ||||
| -rw-r--r-- | src/pardes.zig | 3 |
11 files changed, 152 insertions, 73 deletions
diff --git a/build.zig.zon b/build.zig.zon index f0167c34..165e9b54 100644 --- a/build.zig.zon +++ b/build.zig.zon @@ -9,8 +9,8 @@ // read-only HTTPS URL is what a manifest can carry. Re-pin with // `zig fetch --save=cloud9 git+https://git.sr.ht/~gbrls/cloud9#<commit>`. .cloud9 = .{ - .url = "git+https://git.sr.ht/~gbrls/cloud9#82d8152cd15ea3ddd238caec2072bc2a1327a578", - .hash = "cloud9-0.1.0-yt86qs2zHQAlKvzjBVAZ0ghQwRwWwRtvfE2r-6zUES4X", + .url = "git+https://git.sr.ht/~gbrls/cloud9#cca47d63c3ba774cb4de6f76603ba6edc5b475fe", + .hash = "cloud9-0.1.0-yt86qobpHQDOvgZw-CSB4UHvMmtpp-N4IG0XyTbJlS2y", }, // ZLS as a LIBRARY, not a language server: src/lsp_zls.zig imports the // `zls` module its build.zig publishes and calls the analyser in diff --git a/docs/cloud9.md b/docs/cloud9.md index 8d4b6b44..7365bf65 100644 --- a/docs/cloud9.md +++ b/docs/cloud9.md @@ -23,9 +23,9 @@ is parked with `Status.again` -- the engine parks reads, writes, opens, truncations, clunks and removes -- and retried by `Runner.wakeAll` when the editor next rests. A read with nothing yet parks the same way, but is never retried for news: the core holds it (`ctlfs.hold`) and `answerHeld` answers -it on its connection through `Conn.reply`'s engine, under the connection's -lock and only while the engine still holds that tag parked, since cloud9 does -not tell the backend about a Tflush. `src/9p_quic.zig` selects the existing `pardes-9p` ALPN for +it through the ticket `Conn.hold` gave its park, with `Conn.answerWith`, +which makes the answer only while that very park still waits (not flushed, +its fid not clunked, not out being retried) and under the same lock. `src/9p_quic.zig` selects the existing `pardes-9p` ALPN for cloud9's optional OpenSSL transport; QUIC still runs on the poll loop in `src/9p_io.zig` on the editor's thread, since cloud9's QUIC adapter is nonblocking-descriptor based rather than `std.Io` based. @@ -161,7 +161,10 @@ it: a `lock` while another open holds it waits until that open writes `unlock` or closes (or the pane does), and nothing else is refused for it -- not a write to any other file, not the person at the keyboard. It belongs to the open that wrote it, so only that open's `unlock` is taken; a write on an -open that cannot write (or the editor's own, on none) cannot lock. +open that cannot write (or the editor's own, on none) cannot lock. From a +shell the lock needs an open held across the edit, since `echo lock > ctl` +closes, and so unlocks, at once: `exec 3>ctl; echo lock >&3; ...edits...; +exec 3>&-`. The three range files `addr`, `dot` and `limit` each read the pair of offsets they also accept, so copying one onto another is all that acme's `addr=dot`, @@ -207,11 +210,15 @@ A read with nothing to give yet -- a following `log`, `event`, `pty/data`, a `pty/run` before its answer -- is held, the way factotum holds its log's reads (security/auth/factotum/log.c) and acme an event read: the core keeps it, and whoever next has news for it (a record, output, a run's answer, the pane -closing, which answers "No such file or directory") answers it on the connection it -came on as the turn is given up. Nothing else parked is retried for it. A -read the client flushed meanwhile is dropped unanswered, so no record is -spent on it. An open waits with one read at a time, as acme's window keeps -one `eventx`. QUIC connections still retry their parked reads each tick. +closing, which answers acme's "window shut down") answers it on the +connection it came on as the turn is given up. Nothing else parked is +retried for it. The core keeps the ticket cloud9 gave the park +(`Conn.hold`) and answers only while that very park waits, so a read the +client flushed or whose fid it clunked meanwhile is dropped unanswered, no +record is spent on it, and a tag the client reuses is never answered with +what was meant for the old one. An open waits with one read at a time, as +acme's window keeps one `eventx`; a second read on it meanwhile fails with +"file in use". QUIC connections still retry their parked reads each tick. `pty/run` runs one line at a terminal's prompt and answers how it ended, on the same open (factotum's `rpc` shape): write the line, then read `exit N` @@ -226,7 +233,9 @@ printed before its PROMPT_COMMAND lands in the next run's output. Only its last 64 KiB are kept, from a line start, and the header then reads `exit N cut M` (M bytes left out). `exit N cut`, with no count, says the output's start is not there to read: it scrolled out of the history, the command -erased it (`clear`, a reset), a start or end mark came on the alternate +erased the screen (`clear`, `watch`, a full-screen program's redraw, a +reset -- so such a command's output may read as cut), a start or end mark +came on the alternate screen, or the command printed more than 8192 rows, of which only the last are read so that the answer costs the editor a bounded amount. `exit ?` is a command whose end mark carried no status, which is not a success; `error out diff --git a/src/9p_io.zig b/src/9p_io.zig index 7c366aa8..7b0976bc 100644 --- a/src/9p_io.zig +++ b/src/9p_io.zig @@ -228,10 +228,29 @@ pub const Listener = struct { const reply = core.serveFs(req); if (req.op == .release and quiet) l.collectOs(); // A read with nothing yet stays parked in the engine, and the core - // keeps it to answer when what it waits on has something. - if (reply.status == .again and req.op == .read) if (pardes.ctlfs.openOf(core, req)) |o| { - o.held = .{ .asker = conn, .req = req }; - }; + // keeps its ticket to answer it when what it waits on has something. + if (reply.status == .again and req.op == .read) { + // Only an open's record can hold a read; one that waits with + // none would sit parked until some unrelated write parks. + const o = pardes.ctlfs.openOf(core, req) orelse { + log.err("a read of node {x} waits with no open record to hold it", .{req.node}); + std.debug.assert(false); + return conn.reply(&reply, ""); + }; + // One read waits on an open at a time, as acme's window keeps + // one `eventx`; a second is refused rather than left parked + // where nothing would ever answer it. The first asked again by + // a retry is the same request, and holds its place. + if (o.held) |held| if (held.req.tag != req.tag) { + const other: *Runner.Conn = @ptrCast(@alignCast(held.asker)); + if (other.waiting(held.ticket)) + return conn.reply(&pardes.ctlfs.failText(req.tag, pardes.ctlfs.E.BUSY, pardes.ctlfs.e_in_use), ""); + }; + o.held = null; + const ticket = conn.hold(&reply) orelse return; + o.held = .{ .asker = conn, .req = req, .ticket = ticket }; + return; + } if (!pardes.ctlfs.changesPane(req)) return conn.reply(&reply, core.fsPayload(reply)); // The editor draws the change and performs what it asked for; when // it asked for something -- a save, a shell, a watch -- the answer @@ -253,10 +272,11 @@ pub const Listener = struct { /// With the turn, as it is given up: answers each read the core holds /// whose file now has something, on the connection that asked, with no - /// retry of anything else parked there. Only a read its engine still - /// holds parked is answered: Tflush drops one without a word to the - /// backend, and a `retry` takes one out to ask again, and a record spent - /// on either would be lost to the reader that comes next. + /// retry of anything else parked there. Only while its ticket still + /// waits, asked in the same hold of the engine as the answer: Tflush + /// and clunk drop a park without a word to the backend, a `retry` takes + /// one out to ask again (and that asking answers it), and a record spent + /// on any of them would be lost to the reader that comes next. fn answerHeld(ctx: ?*anyopaque) void { if (comptime !supported) return; const l = of(ctx); @@ -267,27 +287,22 @@ pub const Listener = struct { for (&core.fs.opens) |*o| { const held = o.held orelse continue; const conn: *Runner.Conn = @ptrCast(@alignCast(held.asker)); - conn.lock(); - const parked: ?bool = for (conn.engine.slots) |sl| { - if (sl.used and sl.req.tag == held.req.tag) break sl.parked; - } else null; - // Gone (flushed, answered, hung up) it is forgotten; out being - // asked again, the asking answers it. - if (parked != true) { - if (parked == null) o.held = null; - conn.unlock(); - continue; - } - const reply = core.serveFs(held.req); - if (reply.status != .again) { - o.held = null; - conn.engine.reply(&reply, core.fsPayload(reply)); - } - conn.unlock(); - conn.flush(); + if (!conn.answerWith(held.ticket, Held{ .core = core, .req = held.req }, Held.make)) o.held = null; } } + /// A held read asked again, to make its answer while its park waits. + const Held = struct { + core: *pardes.Pardes, + req: pardes.ctlfs.Req, + + fn make(h: Held) ?Runner.Conn.Answer { + const reply = h.core.serveFs(h.req); + if (reply.status == .again) return null; + return .{ .reply = reply, .bytes = h.core.fsPayload(reply) }; + } + }; + /// The turn was given up quiet with a request parked: every connection /// retries what it parked. fn wakeParked(ctx: ?*anyopaque) void { @@ -1407,41 +1422,65 @@ test "a held read is answered when the log has news, and a flushed one spends no _ = try s.ask(.{ .write = .{ .fid = 1, .offset = 0, .data = "follow\n" } }, &remote); const heldNow = struct { - fn check(core: *pardes.Pardes) !void { + /// Waits until the core holds `n` reads. + fn count(core: *pardes.Pardes, n: usize) !void { const deadline = Client.nowMs() + Client.budget_ms; while (Client.nowMs() < deadline) { pardes.turn.wake(); - const any = for (core.fs.opens) |o| { - if (o.held != null) break true; - } else false; + var got: usize = 0; + for (core.fs.opens) |o| got += @intFromBool(o.held != null); pardes.turn.rest(); - if (any) return; + if (got == n) return; Client.nap(1); } return error.Timeout; } }; + const serial = p.panes[0].?.serial; + var event_path: [32]u8 = undefined; + var event_names: [3][]const u8 = .{ "pane", try std.fmt.bufPrint(&event_path, "{d}", .{serial}), "event" }; + _ = try s.ask(.{ .walk = .{ .fid = 0, .newfid = 2, .names = &event_names } }, &remote); + _ = try s.ask(.{ .open = .{ .fid = 2, .mode = ninep.oread } }, &remote); // Flushed while held, a read is gone from the engine without the core - // hearing of it; the record logged next must reach the read after it. + // hearing of it. Its tag, asked again at once for a read of the pane's + // event, must not be answered with the log's next record, and that + // record must reach the log's next read. const flushed = try s.cl.submit(.{ .read = .{ .fid = 1, .offset = 0, .count = 4096 } }); try s.flush(); - try heldNow.check(p); + try heldNow.count(p, 1); _ = try s.cl.submit(.{ .flush = .{ .oldtag = flushed } }); const interrupted = try s.settle(); try testing.expectEqual(flushed, interrupted.tag); try testing.expect(interrupted.result == .fail); try testing.expect((try s.settle()).result == .flush); + const reused = try s.cl.submit(.{ .read = .{ .fid = 2, .offset = 0, .count = 4096 } }); + try testing.expectEqual(flushed, reused); + try s.flush(); + try heldNow.count(p, 2); pardes.turn.wake(); p.setMessage(0, "after the flush"); pardes.turn.rest(); + try heldNow.count(p, 1); + pardes.turn.wake(); + p.fs.origin = 'K'; + _ = pardes.ctlfs.events.noteAction(p, 0, .body_exec, 0, 4, 0, "held"); + pardes.turn.rest(); + const clicked = try s.settle(); + try testing.expectEqual(reused, clicked.tag); + try testing.expectEqualStrings("KX0 4 0 4 held\n", clicked.result.read); try testing.expect(std.mem.indexOf(u8, (try s.ask(.{ .read = .{ .fid = 1, .offset = 0, .count = 4096 } }, &remote)).read, "after the flush") != null); // Held, a read is answered by the record that arrives, on its own - // connection, with no retry of every parked request asked for. + // connection, with no retry of every parked request asked for; a + // second read on the same open meanwhile is refused, not orphaned. const waiting = try s.cl.submit(.{ .read = .{ .fid = 1, .offset = 0, .count = 4096 } }); try s.flush(); - try heldNow.check(p); + try heldNow.count(p, 1); + const second = try s.cl.submit(.{ .read = .{ .fid = 1, .offset = 0, .count = 4096 } }); + const refused = try s.settle(); + try testing.expectEqual(second, refused.tag); + try testing.expectEqualStrings(pardes.ctlfs.e_in_use, refused.result.fail); pardes.turn.wake(); p.setMessage(0, "while held"); const retry_all = pardes.turn.parked; @@ -1450,13 +1489,15 @@ test "a held read is answered when the log has news, and a flushed one spends no const answered = try s.settle(); try testing.expectEqual(waiting, answered.tag); try testing.expect(std.mem.indexOf(u8, answered.result.read, "while held") != null); + + // Clunked, the log's held read is interrupted by the engine and the + // record it would have had is spent on nobody. + _ = try s.cl.submit(.{ .read = .{ .fid = 1, .offset = 0, .count = 4096 } }); + try s.flush(); + try heldNow.count(p, 1); s.drop(1); - pardes.turn.wake(); - const left = for (p.fs.opens) |o| { - if (o.held != null) break true; - } else false; - pardes.turn.rest(); - try testing.expect(!left); + s.drop(2); + try heldNow.count(p, 0); } extern "c" fn setenv(name: [*:0]const u8, value: [*:0]const u8, overwrite: c_int) c_int; diff --git a/src/fs-help.txt b/src/fs-help.txt index 302e6538..4eecad41 100644 --- a/src/fs-help.txt +++ b/src/fs-help.txt @@ -42,4 +42,4 @@ Pitfalls, one each: Truncating tag clears the part you may edit; truncating dot or addr empties it. A terminal's body is a history snapshot frozen per open; pty/data is the live stream. A failing command is reported in the editor and in log, not as a write error; a bad line fails the write. - pane/<n>/ctl: acme's status line; takes get (reload), lock/unlock (a second lock waits for close/unlock). + pane/<n>/ctl: acme's status line; takes get (reload), lock/unlock on a held fd (exec 3>ctl; echo lock >&3). diff --git a/src/ninep/ctl.zig b/src/ninep/ctl.zig index 83b1196f..a3c1ab9e 100644 --- a/src/ninep/ctl.zig +++ b/src/ninep/ctl.zig @@ -192,9 +192,15 @@ pub fn readPane(p: *Pardes, req: Req, pane: *Pane) Reply { /// the keyboard. It belongs to the open that wrote it, which alone may /// `unlock`, and closing that open or the pane gives it up. pub fn writePane(p: *Pardes, req: Req, pane: *Pane) Reply { - // The record of this open is what holds the lock; a write that came on - // no writable open (the editor's own) has none. - const mine: ?u8 = if (tree.openOf(p, req)) |o| @intCast(o - &p.fs.opens[0]) else null; + // This open's handle is what holds the lock; a write that came on no + // writable open (the editor's own) has none. + const mine: ?u32 = if (tree.openOf(p, req)) |o| (if (o.what == .ctl) req.handle else null) else null; + // The pane keeps the holder's handle, which names that open only on + // this pane's ctl node; one that no longer does holds nothing. + if (pane.fs.lock) |h| { + const o = tree.openOf(p, .{ .tag = 0, .op = .write, .node = tree.Node.of(pane.serial, .ctl), .handle = h }); + if (o == null or o.?.what != .ctl) pane.fs.lock = null; + } const other = pane.fs.lock != null and pane.fs.lock != mine; var asked = false; // Checked whole before anything applies, so a write that must wait for @@ -337,7 +343,7 @@ test "a second lock waits until the holder unlocks or closes, and binds nobody e try testing.expect(pardes.turn.parked); try testing.expectEqual(Status.ok, w.ctl(p, ctl_node, a, "lock\nunlock\nlock\n").reply.status); try testing.expectEqual(E.INVAL, w.ctl(p, ctl_node, a, "unlock\nunlock\n").errno()); - try testing.expectEqual(@as(?u8, @intCast(a - 1)), p.panes[0].?.fs.lock); + try testing.expectEqual(@as(?u32, a), p.panes[0].?.fs.lock); // The lock lives and dies with the pane: closing it wakes whoever waits. const other = try th.newPane(p); diff --git a/src/ninep/events.zig b/src/ninep/events.zig index cb31e2aa..8d5ba16c 100644 --- a/src/ninep/events.zig +++ b/src/ninep/events.zig @@ -608,6 +608,10 @@ test "a pane deleted while its event file is open leaves no suppression behind" _ = wr(p, Node.of(serial, .exec), "Del\n"); try testing.expect(p.paneBySerial(serial) == null); try testing.expectEqual(@as(u16, 0), p.fs.listeners); + // The reader's next read hears what acme says of a window gone under it. + const shut = call(p, .{ .tag = 19, .op = .read, .node = event, .handle = a.reply.handle, .size = 64 }); + try testing.expectEqual(E.IO, shut.errno()); + try testing.expectEqualStrings(tree.e_shut_down, shut.reply.ename); _ = call(p, .{ .tag = 20, .op = .release, .node = event, .handle = a.reply.handle }); _ = call(p, .{ .tag = 21, .op = .release, .node = event, .handle = b.reply.handle }); diff --git a/src/ninep/pane.zig b/src/ninep/pane.zig index 3d7834c4..24398445 100644 --- a/src/ninep/pane.zig +++ b/src/ninep/pane.zig @@ -43,11 +43,11 @@ pub const State = struct { /// An open reads pty/data, so output queues for it; a second is refused. pty_reader: bool = false, pty_out: events.Queue = .{}, - /// The open record (tree.zig) of the pty/run waiting on this shell's - /// current command. - run: ?u8 = null, - /// The open record of the ctl open that wrote `lock` (ctl.zig). - lock: ?u8 = null, + /// The handle of the pty/run open waiting on this shell's current + /// command, good only through `tree.openOf` on this pane's run node. + run: ?u32 = null, + /// The handle of the ctl open that wrote `lock` (ctl.zig), the same way. + lock: ?u32 = null, /// The host started a shell it could not teach to mark its prompts. unmarked: bool = false, /// Installed during this update; /log hears about it once the update ends diff --git a/src/ninep/pty.zig b/src/ninep/pty.zig index 936f9b73..49f52c82 100644 --- a/src/ninep/pty.zig +++ b/src/ninep/pty.zig @@ -164,7 +164,7 @@ pub fn writeRun(p: *Pardes, req: Req, id: usize, pane: *Pane) Reply { const marks = &state.stream.handler; if (pf.unmarked) { answer(p, slot, "error no prompt marks", .{}); - } else if (pf.run != null or marks.phase != .input or + } else if (waitingRun(p, pane) != null or marks.phase != .input or !pardes.panes.Terminal.promptInputEmpty(pane) or p.hostTtyTaken(id)) { // Something is running, someone has typed at the prompt, or the @@ -180,7 +180,7 @@ pub fn writeRun(p: *Pardes, req: Req, id: usize, pane: *Pane) Reply { p.emitWrite(id, line); p.emitWrite(id, "\r"); slot.phase = .sent; - pf.run = @intCast(req.handle - 1); + pf.run = req.handle; } return .{ .tag = req.tag, .written = @intCast(req.data.len) }; } @@ -217,7 +217,7 @@ pub fn noteMarks(p: *Pardes, id: usize, pane: *Pane) void { if (comptime !pardes.panes.Terminal.enabled) return; const state = pane.terminal orelse return; const pf = &pane.fs; - const slot = &p.fs.opens[pf.run orelse return].what.run; + const slot = waitingRun(p, pane) orelse return; const marks = &state.stream.handler; if (marks.phase != .input) return; // ponytail: a prompt redrawn before the command starts (a resize in that @@ -297,11 +297,22 @@ pub fn noteMarks(p: *Pardes, id: usize, pane: *Pane) void { /// The pane closed or its shell was replaced: the command's end will never /// be reported, so the run says so instead of waiting forever. pub fn shellGone(p: *Pardes, pane: *Pane) void { - const idx = pane.fs.run orelse return; - answer(p, &p.fs.opens[idx].what.run, "error shell gone", .{}); + answer(p, waitingRun(p, pane) orelse return, "error shell gone", .{}); pane.fs.run = null; } +/// The run waiting on this shell's command. The pane keeps its open's +/// handle, which names that open only on the node it was opened on, so it +/// is looked up the way a request's is; one that no longer names a run is +/// forgotten. +fn waitingRun(p: *Pardes, pane: *Pane) ?*Run { + const handle = pane.fs.run orelse return null; + if (tree.openOf(p, .{ .tag = 0, .op = .read, .node = tree.Node.of(pane.serial, .pty_run), .handle = handle })) |o| + if (o.what == .run) return &o.what.run; + pane.fs.run = null; + return null; +} + const e_bad_line = "bad command line"; pub fn readData(p: *Pardes, req: Req, pf: *pane_files.State) Reply { diff --git a/src/ninep/tree.zig b/src/ninep/tree.zig index c9e7b31a..f93ab203 100644 --- a/src/ninep/tree.zig +++ b/src/ninep/tree.zig @@ -64,6 +64,7 @@ pub const e_bad_event = "bad event syntax"; /// A second open reading a file whose reads consume: rio's word for it /// (windows/rio/xfid.c:25), which 9ns turns into EBUSY. pub const e_in_use = "file in use"; +pub const e_shut_down = "window shut down"; fn reads(omode: u8) bool { return omode & 3 != 1; // OWRITE is the one access mode that never reads @@ -136,7 +137,7 @@ pub const Open = struct { /// `asker` is the connection the read came on, which only the listener /// knows. - pub const Held = struct { asker: *anyopaque, req: Req }; + pub const Held = struct { asker: *anyopaque, req: Req, ticket: cloud9.fs.Ticket }; pub fn deinit(o: *Open, gpa: std.mem.Allocator) void { switch (o.what) { @@ -655,13 +656,13 @@ fn releaseHandle(p: *Pardes, req: Req) void { pn.fs.pty_out.clearAndFree(p.gpa); }, // The command runs on in the shell; nobody is waiting for it any more. - .run => if (pn.fs.run == @as(u8, @intCast(req.handle - 1))) { + .run => if (pn.fs.run == req.handle) { pn.fs.run = null; }, // Closing the open that holds the lock gives it up, as acme's clunk // of its ctlfid does (editors/acme/xfid.c:211); a write parked on // `lock` goes again. - .ctl => if (pn.fs.lock == @as(u8, @intCast(req.handle - 1))) { + .ctl => if (pn.fs.lock == req.handle) { pn.fs.lock = null; pardes.turn.parked = true; }, @@ -717,7 +718,13 @@ fn read(p: *Pardes, req: Req, target: Target) Reply { .pane => |t| { // A run's answer outlives the pane it ran in. if (t.file == .pty_run and req.handle != 0) return pty.readRun(p, req); - const id = p.paneBySerial(t.serial) orelse return Reply.fail(req.tag, E.NOENT); + const id = p.paneBySerial(t.serial) orelse { + // An event or pty/data read the pane closed under: acme's + // answer to the same (editors/acme/xfid.c:1005). + if (openOf(p, req)) |o| if (o.what == .event or o.what == .pty_data) + return failText(req.tag, E.IO, e_shut_down); + return Reply.fail(req.tag, E.NOENT); + }; const pn = p.panes[id].?; if (t.file.inPty() and !pn.isTerminal()) return Reply.fail(req.tag, E.NOENT); return pane.read(p, req, id, pn, t.file); diff --git a/src/pardes.zig b/src/pardes.zig index 7ae9cf32..1159dd73 100644 --- a/src/pardes.zig +++ b/src/pardes.zig @@ -177,7 +177,8 @@ pub const Turn = struct { fn release(t: *Turn) void { // Whatever the holder just did may be what a held read waits for, - // and answering it takes the core, so before letting go. + // and answering it takes the core, so before letting go. Even with + // a step out in a syscall: a read is served then, as in onServe. if (t.answer_held) |f| f(t.wake_ctx); const woken = t.out == 0 and t.parked; if (woken) t.parked = false; |
