diff options
Diffstat (limited to 'src/ninep/ctl.zig')
| -rw-r--r-- | src/ninep/ctl.zig | 116 |
1 files changed, 103 insertions, 13 deletions
diff --git a/src/ninep/ctl.zig b/src/ninep/ctl.zig index 7625f501..83b1196f 100644 --- a/src/ninep/ctl.zig +++ b/src/ninep/ctl.zig @@ -2,8 +2,8 @@ //! a right click on it and writing one to `exec` a middle click, at the //! active pane from the root and at that pane from /pane/<n>/; reading either //! answers the serials the last command made or touched. /status reports the -//! editor, and a pane's ctl its acme status line and the one verb, `get`, -//! that no file of its own would say any better. +//! editor, and a pane's ctl its acme status line and the verbs no file of its +//! own would say any better: `get`, `lock` and `unlock`. const std = @import("std"); const pardes = @import("../pardes.zig"); const panes = @import("../panes.zig"); @@ -179,17 +179,48 @@ pub fn readPane(p: *Pardes, req: Req, pane: *Pane) Reply { return tree.stagedReply(p, req); } -/// `get` is the one thing here that no file of the pane's own would say: it -/// reloads the buffer from the name it carries, wherever that name resolves. -/// Repeating it in one write would only reload the same bytes, so it runs once. +/// `get` reloads the buffer from the name it carries, wherever that name +/// resolves. Repeating it in one write would only reload the same bytes, so +/// it runs once. +/// +/// `lock` and `unlock` are acme's (editors/acme/xfid.c:603-611), so that a +/// client can make an edit of several writes to addr and data without +/// another's landing in between. As in acme the lock binds only the clients +/// that ask for it: a `lock` while another open holds it waits (parked, as +/// acme's qlock blocks the writer) until that open unlocks or closes; a +/// write to any other file is never refused for it, nor is the person at +/// 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; + const other = pane.fs.lock != null and pane.fs.lock != mine; var asked = false; - var it = std.mem.splitScalar(u8, req.data, '\n'); - while (it.next()) |raw| { - const line = std.mem.trim(u8, raw, " \t\r"); - if (line.len == 0) continue; - if (!std.mem.eql(u8, line, "get")) return tree.failText(req.tag, E.INVAL, tree.e_bad_ctl); - asked = true; + // Checked whole before anything applies, so a write that must wait for + // the lock has done nothing yet when it goes again. + for ([2]bool{ false, true }) |apply| { + var held = !other and pane.fs.lock != null; + var it = std.mem.splitScalar(u8, req.data, '\n'); + while (it.next()) |raw| { + const line = std.mem.trim(u8, raw, " \t\r"); + if (line.len == 0) continue; + if (std.mem.eql(u8, line, "get")) { + asked = true; + } else if (std.mem.eql(u8, line, "lock")) { + if (mine == null) return tree.failText(req.tag, E.INVAL, tree.e_bad_ctl); + if (other) return .{ .tag = req.tag, .status = .again }; + held = true; + if (apply) pane.fs.lock = mine; + } else if (std.mem.eql(u8, line, "unlock")) { + if (!held) return tree.failText(req.tag, E.INVAL, e_not_locked); + held = false; + if (apply) { + pane.fs.lock = null; + pardes.turn.parked = true; // a `lock` waiting goes again + } + } else return tree.failText(req.tag, E.INVAL, tree.e_bad_ctl); + } } if (asked) { const errno = get(p, pane); @@ -198,6 +229,8 @@ pub fn writePane(p: *Pardes, req: Req, pane: *Pane) Reply { return .{ .tag = req.tag, .written = @intCast(req.data.len) }; } +const e_not_locked = "window not locked by this open"; + fn get(p: *Pardes, pane: *Pane) u16 { const f = pane_files.fileOf(pane) orelse return 0; if (!panes.Output.fileTraits(f.output).saves) return 0; @@ -251,13 +284,13 @@ test "pane ctl read is index's five fields plus width in cells, font and tab wid try testing.expectEqualStrings("'it''s'", w.buffered()); } -test "the pane ctl takes get, and nothing that a file of its own now answers" { +test "the pane ctl takes get, lock and unlock, and nothing that a file of its own now answers" { const gpa = testing.allocator; const p = try withFile(gpa, "one\ntwo\n"); defer p.deinit(); const ctl_node = Node.of(serialOf(p), .ctl); for ([_][]const u8{ - "menu", "nomenu", "dump echo hi", "font Go Mono", "lock", "bogus", "DEL", + "menu", "nomenu", "dump echo hi", "font Go Mono", "lock x", "bogus", "DEL", "name x.txt", "put", "del", "delete", "Look x", "Exec Save", "clean", "dirty", "cleartag", "dot=addr", "addr=dot", "show", "mark", "nomark", "scroll", "limit=addr", "get x", "look /tmp", "exec Del", @@ -265,6 +298,63 @@ test "the pane ctl takes get, and nothing that a file of its own now answers" { try testing.expect(p.paneBySerial(serialOf(p)) != null); } +test "a second lock waits until the holder unlocks or closes, and binds nobody else" { + const p = try withFile(testing.allocator, "one\ntwo\n"); + defer p.deinit(); + const serial = serialOf(p); + const ctl_node = Node.of(serial, .ctl); + const w = struct { + fn ctl(pp: *Pardes, node: u64, h: u32, data: []const u8) th.Answer { + return call(pp, .{ .tag = 4, .op = .write, .node = node, .handle = h, .data = data }); + } + }; + const a = call(p, .{ .tag = 1, .op = .open, .node = ctl_node, .omode = 2 }).reply.handle; + const b = call(p, .{ .tag = 2, .op = .open, .node = ctl_node, .omode = 1 }).reply.handle; + try testing.expect(a != 0 and b != 0 and a != b); + // Reading the status line holds nothing, so it cannot lock either. + try testing.expectEqual(@as(u32, 0), call(p, .{ .tag = 3, .op = .open, .node = ctl_node }).reply.handle); + try testing.expectEqual(E.INVAL, wr(p, ctl_node, "lock\n").errno()); + + try testing.expectEqual(Status.ok, w.ctl(p, ctl_node, a, "lock\n").reply.status); + try testing.expectEqual(Status.ok, w.ctl(p, ctl_node, a, "lock\n").reply.status); + // Another open's lock waits, and its unlock is refused; writes to the + // pane's other files are not. + try testing.expectEqual(Status.again, w.ctl(p, ctl_node, b, "lock\n").reply.status); + try testing.expectEqualStrings(e_not_locked, w.ctl(p, ctl_node, b, "unlock\n").reply.ename); + try testing.expectEqual(Status.ok, wr(p, Node.of(serial, .addr), "1").reply.status); + try testing.expectEqual(Status.ok, wr(p, Node.of(serial, .data), "ONE\n").reply.status); + try testing.expectEqualStrings("ONE\ntwo\n", p.panes[0].?.file.?.content); + + // Unlocking lets the parked lock go again, and now it takes the lock. + pardes.turn.parked = false; + try testing.expectEqual(Status.ok, w.ctl(p, ctl_node, a, "unlock\n").reply.status); + try testing.expect(pardes.turn.parked); + try testing.expectEqual(Status.ok, w.ctl(p, ctl_node, b, "lock\n").reply.status); + try testing.expectEqual(Status.again, w.ctl(p, ctl_node, a, "lock\nunlock\n").reply.status); + // Closing the holder gives it up. + pardes.turn.parked = false; + _ = call(p, .{ .tag = 5, .op = .release, .node = ctl_node, .handle = b }); + 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); + + // The lock lives and dies with the pane: closing it wakes whoever waits. + const other = try th.newPane(p); + const other_ctl = Node.of(other, .ctl); + const c = call(p, .{ .tag = 6, .op = .open, .node = other_ctl, .omode = 1 }).reply.handle; + try testing.expectEqual(Status.ok, w.ctl(p, other_ctl, c, "lock\n").reply.status); + pardes.turn.parked = false; + try testing.expectEqual(Status.ok, th.rmdir(p, Node.of(other, .dir)).reply.status); + try testing.expect(pardes.turn.parked); + pardes.turn.parked = false; + try testing.expectEqual(E.NOENT, w.ctl(p, other_ctl, c, "lock\n").errno()); + for ([_]u64{ ctl_node, other_ctl }, [_]u32{ a, c }) |node, h| + _ = call(p, .{ .tag = 7, .op = .release, .node = node, .handle = h }); + try testing.expect(p.panes[0].?.fs.lock == null); + for (p.fs.opens) |o| try testing.expect(o.node == 0); +} + test "exec runs a builtin at the pane and records the pane it acted on" { const gpa = testing.allocator; const p = try withFile(gpa, "Msg fs-ran\n"); |
