summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/9p_io.zig123
-rw-r--r--src/fs-help.txt2
-rw-r--r--src/ninep/ctl.zig14
-rw-r--r--src/ninep/events.zig4
-rw-r--r--src/ninep/pane.zig10
-rw-r--r--src/ninep/pty.zig21
-rw-r--r--src/ninep/tree.zig15
-rw-r--r--src/pardes.zig3
8 files changed, 131 insertions, 61 deletions
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;