diff options
| author | Gabriel Schneider <[email protected]> | 2026-09-27 20:51:09 -0300 |
|---|---|---|
| committer | Gabriel Schneider <[email protected]> | 2026-10-01 00:12:14 -0300 |
| commit | a458b6e610ed573b987ade7c0048663e57fb7191 (patch) | |
| tree | 8afecb4478b3d9d76df55aa67d6183785fcdc5e6 | |
| parent | c3e2cd3b854964ca7db3798cb8750ee6d0833057 (diff) | |
| download | pardes-a458b6e610ed573b987ade7c0048663e57fb7191.tar.gz pardes-a458b6e610ed573b987ade7c0048663e57fb7191.zip | |
Answer a held read by its cloud9 ticket, refuse a second, and say a pane shut down
The held read is now kept by the Ticket cloud9's Conn.hold() gives its
park and answered through Conn.answerWith(), which makes the answer only
while that very park still waits, instead of walking the engine's slots
by tag; pardes no longer reaches into the engine for it. A second read on
an open whose read is held fails with file in use rather than sitting
parked where nothing answers it, and a read that waits with no open
record to hold it is logged and asserted on. A read on an event or
pty/data open whose pane closed answers acme's "window shut down"
(editors/acme/xfid.c:1005). The pane keeps its run's and its lock's open
handles, checked through openOf on use, not record indices. Docs: lock
from a shell needs a held fd, and a command that clears the screen may
read as cut.
Needs cloud9 zvuqvnzy (cca47d63), which adds Conn.hold, waiting and
answerWith; build.zig.zon still pins 82d8152c until that is pushed and
re-pinned.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
| -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; |
