summaryrefslogtreecommitdiff
path: root/src/ninep/tree.zig
diff options
context:
space:
mode:
Diffstat (limited to 'src/ninep/tree.zig')
-rw-r--r--src/ninep/tree.zig207
1 files changed, 132 insertions, 75 deletions
diff --git a/src/ninep/tree.zig b/src/ninep/tree.zig
index 56c3112d..46609649 100644
--- a/src/ninep/tree.zig
+++ b/src/ninep/tree.zig
@@ -4,8 +4,10 @@
//! extended here with the editor's reply payload; fs.zig keeps host access
//! and mounts.
//!
-//! /README /index /ctl /new /log /screen /listeners
-//! /pane/<serial>/{name,body,tag,ctl,addr,data,xdata,sel,errors,event,pty/}
+//! /README /index /status /look /exec /log /screen /listeners
+//! /pane/ mkdir makes a pane, rmdir closes it
+//! /pane/<serial>/{name,body,tag,ctl,addr,dot,limit,data,xdata,sel,
+//! dirty,mark,scroll,errors,event,look,exec,pty/}
//! /os/... the host filesystem /src/... embedded sources (opt-in)
const std = @import("std");
const cloud9 = @import("cloud9");
@@ -47,11 +49,16 @@ pub const Payload = union(enum) {
pub const Reply = cloud9.fs.ReplyWith(Payload);
+/// Tcreate opens a pane and Tremove closes one; nothing else in the tree is
+/// created or destroyed by the protocol, and wstat stays a truncation.
+pub const features: cloud9.fs.Features = .{ .create = true, .remove = true };
+
pub fn changesPane(req: Req) bool {
return switch (req.op) {
.write, .setattr => true,
- .open => req.node == @intFromEnum(TopFile.new),
- .lookup, .getattr, .read, .release, .readdir => false,
+ .open => req.create,
+ .release => req.remove,
+ .lookup, .getattr, .read, .readdir => false,
};
}
@@ -63,8 +70,9 @@ pub const TopFile = enum(u4) {
root = 1,
README,
index,
- ctl,
- new,
+ status,
+ look,
+ exec,
log,
screen,
listeners,
@@ -77,8 +85,8 @@ pub const TopFile = enum(u4) {
pub fn mode(f: TopFile) u16 {
return switch (f) {
.root, .pane => 0o755,
- .ctl => 0o666,
- .README, .index, .new, .log, .screen, .listeners => 0o444,
+ .look, .exec => 0o666,
+ .README, .index, .status, .log, .screen, .listeners => 0o444,
};
}
@@ -89,18 +97,26 @@ pub const TopFile = enum(u4) {
pub const root: u64 = @intFromEnum(TopFile.root);
-pub const PaneFile = enum(u4) {
+/// Declaration order is readdir order.
+pub const PaneFile = enum(u5) {
dir = 0,
name,
body,
tag,
ctl,
addr,
+ dot,
+ limit,
data,
xdata,
sel,
+ dirty,
+ mark,
+ scroll,
errors,
event,
+ look,
+ exec,
pty,
pty_ctl,
pty_status,
@@ -138,8 +154,8 @@ pub const PaneFile = enum(u4) {
};
pub const Node = packed struct(u64) {
- file: u4 = 0,
- serial: u60 = 0,
+ file: u5 = 0,
+ serial: u59 = 0,
pub fn of(serial: u32, file: PaneFile) u64 {
std.debug.assert(serial != 0);
@@ -240,8 +256,13 @@ pub fn stagedReply(p: *Pardes, req: Req) Reply {
// ---- dispatch ----
pub fn handle(p: *Pardes, req: Req) Reply {
- if (req.node == fs.os_root or req.node & fs.os_node != 0) return fs.osHandle(p, req);
- if (req.node & sources.archive_node != 0) return sources.handle(p, req);
+ const host = req.node == fs.os_root or req.node & fs.os_node != 0;
+ const archive = req.node & sources.archive_node != 0;
+ // Only /pane is created in and removed from. The host tree and the
+ // embedded sources say so, rather than quietly doing nothing.
+ if ((host or archive) and (req.create or req.remove)) return Reply.fail(req.tag, E.PERM);
+ if (host) return fs.osHandle(p, req);
+ if (archive) return sources.handle(p, req);
const target = Node.target(req.node) orelse return Reply.fail(req.tag, E.NOENT);
if (req.op == .write or req.op == .setattr) p.fs.origin = switch (target) {
.pane => |t| @as(u8, if (t.file == .body or t.file == .tag) 'E' else 'F'),
@@ -280,6 +301,7 @@ fn attrOf(p: *Pardes, target: Target) ?Reply.Attr {
.mode = t.file.mode(),
.size = pane.fileSize(p, id, t.file),
.mtime = pane.mtimeOf(p, pn),
+ .version = pane.versionOf(pn, t.file),
};
},
}
@@ -292,13 +314,14 @@ fn attrReply(p: *Pardes, tag: u64, target: Target) Reply {
fn topSize(p: *Pardes, f: TopFile) u64 {
return switch (f) {
- .root, .pane, .new, .log, .screen, .listeners => 0,
+ // ponytail: /screen has no length until an open renders its frame.
+ .root, .pane, .screen => 0,
.index => pane.indexLen(p),
.README => fs.help.len,
- .ctl => ctl: {
- var buf: [ctl.root_text_cap]u8 = undefined;
- break :ctl ctl.rootText(p, &buf).len;
- },
+ .status => ctl.statusLen(p),
+ .look, .exec => ctl.resultsLen(p),
+ .log => events.pending(&p.fs.log),
+ .listeners => screen.listenersLen(p),
};
}
@@ -366,7 +389,7 @@ fn readdir(p: *Pardes, req: Req, target: Target) Reply {
switch (target) {
.top => |f| switch (f) {
.root => {
- inline for (.{ TopFile.README, TopFile.index, TopFile.ctl, TopFile.new, TopFile.log, TopFile.screen, TopFile.listeners, TopFile.pane }) |t| {
+ inline for (.{ TopFile.README, TopFile.index, TopFile.status, TopFile.look, TopFile.exec, TopFile.log, TopFile.screen, TopFile.listeners, TopFile.pane }) |t| {
if (skip > 0) skip -= 1 else stageDirent(out, p.gpa, @intFromEnum(t), t.dir(), t.fileName());
}
if (skip > 0) skip -= 1 else stageDirent(out, p.gpa, fs.os_root, true, "os");
@@ -403,21 +426,27 @@ fn readdir(p: *Pardes, req: Req, target: Target) Reply {
return .{ .tag = req.tag, .payload = .{ .staged = @intCast(out.items.len) } };
}
-fn newPane(p: *Pardes) ?u32 {
- const slot = p.freeSlot() orelse return null;
+/// Tcreate in /pane opens a pane, the way mkdir opens a directory. The name
+/// asked for is ignored: a pane is named by the serial the editor gives it,
+/// which the reply carries back and /index lists last.
+fn create(p: *Pardes, req: Req, target: Target) Reply {
+ switch (target) {
+ .top => |f| if (f != .pane) return Reply.fail(req.tag, E.PERM),
+ .pane => return Reply.fail(req.tag, E.PERM),
+ }
+ if (req.perm & cloud9.dmdir == 0) return Reply.fail(req.tag, E.PERM);
+ const slot = p.freeSlot() orelse return Reply.fail(req.tag, E.NFILE);
p.newScratchBelow(p.active);
- const pn = p.panes[slot] orelse return null;
- return pn.serial;
+ const made = p.panes[slot] orelse return Reply.fail(req.tag, E.NFILE);
+ const attr = attrOf(p, .{ .pane = .{ .serial = made.serial, .file = .dir } }) orelse
+ return Reply.fail(req.tag, E.NFILE);
+ return .{ .tag = req.tag, .handle = 1, .attr = attr };
}
fn open(p: *Pardes, req: Req, target: Target) Reply {
+ if (req.create) return create(p, req, target);
switch (target) {
.top => |f| switch (f) {
- // The clone idiom: each open makes one empty pane; reads answer its serial.
- .new => {
- const serial = newPane(p) orelse return Reply.fail(req.tag, E.NFILE);
- return .{ .tag = req.tag, .handle = serial };
- },
.screen => return screen.openSnapshot(p, req, true),
.log => p.fs.log_readers +|= 1,
else => {},
@@ -429,11 +458,7 @@ fn open(p: *Pardes, req: Req, target: Target) Reply {
if (t.file.inPty() and !pn.isTerminal()) return Reply.fail(req.tag, E.NOENT);
switch (t.file) {
.body => if (pn.isTerminal()) return screen.openSnapshot(p, req, false),
- .ctl => pf.limit = null,
- .addr => {
- pf.addr = .{};
- pf.limit = null;
- },
+ .addr => pf.addr = .{},
.event => {
pf.readers +|= 1;
p.fs.listeners +|= 1;
@@ -446,7 +471,27 @@ fn open(p: *Pardes, req: Req, target: Target) Reply {
return .{ .tag = req.tag, .handle = 1 };
}
+/// Tremove closes a pane. Nothing else in the tree can be removed, and this
+/// is also where an ORCLOSE open lands, so every node is checked here.
+fn remove(p: *Pardes, req: Req) Reply {
+ const target = Node.target(req.node) orelse return Reply.fail(req.tag, E.NOENT);
+ const t = switch (target) {
+ .pane => |t| t,
+ .top => return Reply.fail(req.tag, E.PERM),
+ };
+ if (t.file != .dir) return Reply.fail(req.tag, E.PERM);
+ const id = p.paneBySerial(t.serial) orelse return Reply.fail(req.tag, E.NOENT);
+ p.removePane(id) catch return Reply.fail(req.tag, E.IO);
+ return .{ .tag = req.tag };
+}
+
fn release(p: *Pardes, req: Req) Reply {
+ // The handle's bookkeeping runs whether or not the removal is allowed.
+ const done = releaseHandle(p, req);
+ return if (req.remove) remove(p, req) else done;
+}
+
+fn releaseHandle(p: *Pardes, req: Req) Reply {
if (screen.releaseSnapshot(p, req)) return .{ .tag = req.tag };
const target = Node.target(req.node) orelse return .{ .tag = req.tag };
switch (target) {
@@ -502,12 +547,8 @@ fn read(p: *Pardes, req: Req, target: Target) Reply {
}
break :index stagedReply(p, req);
},
- .ctl => ctl.readRoot(p, req),
- .new => new: {
- if (req.handle == 0) break :new Reply.fail(req.tag, E.INVAL);
- p.fs.stage(p.gpa).print(p.gpa, "{d}\n", .{req.handle}) catch break :new Reply.fail(req.tag, E.NOMEM);
- break :new stagedReply(p, req);
- },
+ .status => ctl.readStatus(p, req),
+ .look, .exec => ctl.readResults(p, req),
.log => events.readQueue(p, req, &p.fs.log),
.screen => screen.readSnapshot(p, req, null),
.listeners => screen.readListeners(p, req),
@@ -525,7 +566,7 @@ fn read(p: *Pardes, req: Req, target: Target) Reply {
fn write(p: *Pardes, req: Req, target: Target) Reply {
switch (target) {
.top => |f| return switch (f) {
- .ctl => ctl.writeRoot(p, req),
+ .look, .exec => ctl.command(p, req, null, f == .exec),
else => Reply.fail(req.tag, E.PERM),
},
.pane => |t| {
@@ -587,8 +628,8 @@ test "filesystem inspection preserves pending and displayed Look hover" {
const preview = p.look_hover_preview;
const requests = [_]Req{
.{ .tag = 1, .op = .lookup, .node = root, .data = "screen" },
- .{ .tag = 1, .op = .lookup, .node = root, .data = "new" },
- .{ .tag = 1, .op = .getattr, .node = @intFromEnum(TopFile.new) },
+ .{ .tag = 1, .op = .lookup, .node = root, .data = "status" },
+ .{ .tag = 1, .op = .getattr, .node = @intFromEnum(TopFile.pane) },
.{ .tag = 2, .op = .getattr, .node = body },
.{ .tag = 3, .op = .open, .node = body },
.{ .tag = 4, .op = .read, .node = body, .size = 5 },
@@ -614,7 +655,7 @@ test "filesystem inspection preserves pending and displayed Look hover" {
test "filesystem pane creation and truncation cancel Look hover" {
const requests = [_]Req{
- .{ .tag = 2, .op = .open, .node = @intFromEnum(TopFile.new) },
+ .{ .tag = 2, .op = .open, .node = @intFromEnum(TopFile.pane), .data = "x", .create = true, .perm = cloud9.dmdir | 0o755 },
.{ .tag = 3, .op = .setattr, .node = 0, .truncate = true },
};
for (requests) |request| {
@@ -644,13 +685,13 @@ test "readdir lists the root and a pane directory without creating anything" {
var top_bytes: [4096]u8 = undefined;
@memcpy(top_bytes[0..top_reply.bytes.len], top_reply.bytes);
const top = th.dirents(top_bytes[0..top_reply.bytes.len], &buf);
- try testing.expect(top.len >= 9);
- for ([_][]const u8{ "README", "index", "ctl", "new", "log", "screen", "listeners", "pane", "os" }, 0..) |name, i| {
+ try testing.expect(top.len >= 10);
+ for ([_][]const u8{ "README", "index", "status", "look", "exec", "log", "screen", "listeners", "pane", "os" }, 0..) |name, i| {
try testing.expectEqualStrings(name, top[i].name);
- try testing.expectEqual(i >= 7, top[i].dir);
+ try testing.expectEqual(i >= 8, top[i].dir);
}
- try testing.expectEqual(@intFromEnum(TopFile.pane), top[7].node);
- try testing.expectEqual(fs.os_root, top[8].node);
+ try testing.expectEqual(@intFromEnum(TopFile.pane), top[8].node);
+ try testing.expectEqual(fs.os_root, top[9].node);
var idbuf: [16]u8 = undefined;
try testing.expect(th.nameAt(top, try std.fmt.bufPrint(&idbuf, "{d}", .{serial})) == null);
try testing.expect(th.nameAt(top, "self") == null);
@@ -674,13 +715,18 @@ test "readdir lists the root and a pane directory without creating anything" {
}
const panes_dir = rdir(p, @intFromEnum(TopFile.pane), 0);
const listed = th.dirents(panes_dir.bytes, &buf);
- try testing.expectEqual(@as(usize, 1), listed.len);
- try testing.expectEqual(Node.of(serial, .dir), listed[0].node);
+ var idname: [16]u8 = undefined;
+ const own = th.nameAt(listed, try std.fmt.bufPrint(&idname, "{d}", .{serial})).?;
+ try testing.expectEqual(Node.of(serial, .dir), own.node);
+ try testing.expect(own.dir);
const dir = rdir(p, Node.of(serial, .dir), 0);
const files = th.dirents(dir.bytes, &buf);
- try testing.expectEqual(@as(usize, 10), files.len);
- for ([_][]const u8{ "name", "body", "tag", "ctl", "addr", "data", "xdata", "sel", "errors", "event" }, 0..) |name, i|
+ try testing.expectEqual(@as(usize, 17), files.len);
+ for ([_][]const u8{
+ "name", "body", "tag", "ctl", "addr", "dot", "limit", "data", "xdata",
+ "sel", "dirty", "mark", "scroll", "errors", "event", "look", "exec",
+ }, 0..) |name, i|
try testing.expectEqualStrings(name, files[i].name);
try testing.expect(th.nameAt(files, ".") == null);
try testing.expect(th.nameAt(files, "rdsel") == null);
@@ -703,8 +749,10 @@ test "lookup resolves top files, pane serials and pane files" {
const serial = serialOf(p);
try testing.expectEqual(@as(u64, @intFromEnum(TopFile.index)), look_up(p, root, "index").reply.attr.node);
- try testing.expect(!look_up(p, root, "new").reply.attr.dir);
- try testing.expectEqual(@as(u16, 0o444), look_up(p, root, "new").reply.attr.mode);
+ try testing.expect(!look_up(p, root, "status").reply.attr.dir);
+ try testing.expectEqual(@as(u16, 0o444), look_up(p, root, "status").reply.attr.mode);
+ try testing.expectEqual(@as(u16, 0o666), look_up(p, root, "look").reply.attr.mode);
+ try testing.expectEqual(E.NOENT, look_up(p, root, "new").errno());
try testing.expect(look_up(p, root, "pane").reply.attr.dir);
try testing.expect(look_up(p, root, "os").reply.attr.dir);
try testing.expectEqual(E.NOENT, look_up(p, root, "nosuchthing").errno());
@@ -732,38 +780,46 @@ test "lookup resolves top files, pane serials and pane files" {
try testing.expectEqual(E.NOTDIR, look_up(p, Node.of(serial, .body), "x").errno());
}
-test "each open of new creates exactly one pane whose serial the read answers" {
+test "creating in the pane directory opens a pane and removing one closes it" {
const gpa = testing.allocator;
const p = try withFile(gpa, "first\n");
defer p.deinit();
const before = p.next_serial;
- const new = @intFromEnum(TopFile.new);
+ const panes_dir = @intFromEnum(TopFile.pane);
- try testing.expectEqual(Status.ok, look_up(p, root, "new").reply.status);
- try testing.expectEqual(Status.ok, call(p, .{ .tag = 1, .op = .getattr, .node = new }).reply.status);
+ // Browsing /pane creates nothing; only a create does.
+ try testing.expectEqual(Status.ok, rdir(p, panes_dir, 0).reply.status);
try testing.expectEqual(before, p.next_serial);
- const opened = call(p, .{ .tag = 1, .op = .open, .node = new });
- try testing.expectEqual(Status.ok, opened.reply.status);
+ const made = th.mkdir(p, "scratch");
+ try testing.expectEqual(Status.ok, made.reply.status);
try testing.expectEqual(before + 1, p.next_serial);
- try testing.expectEqual(before + 1, opened.reply.handle);
- try testing.expectEqual(@as(u64, 0), opened.reply.attr.node);
+ const serial = before + 1;
+ try testing.expectEqual(Node.of(serial, .dir), made.reply.attr.node);
+ try testing.expect(made.reply.attr.dir);
var expected: [16]u8 = undefined;
- const line = try std.fmt.bufPrint(&expected, "{d}\n", .{opened.reply.handle});
- try testing.expectEqualStrings(line, call(p, .{ .tag = 2, .op = .read, .node = new, .handle = opened.reply.handle, .size = 64 }).bytes);
- try testing.expectEqualStrings(line[1..], call(p, .{ .tag = 2, .op = .read, .node = new, .handle = opened.reply.handle, .off = 1, .size = 64 }).bytes);
- try testing.expectEqualStrings("", call(p, .{ .tag = 2, .op = .read, .node = new, .handle = opened.reply.handle, .off = line.len, .size = 64 }).bytes);
- try testing.expectEqual(before + 1, p.next_serial);
- _ = call(p, .{ .tag = 3, .op = .release, .node = new, .handle = opened.reply.handle });
+ try testing.expectEqualStrings(try std.fmt.bufPrint(&expected, "{d}", .{serial}), made.reply.attr.name);
- const id = p.paneBySerial(opened.reply.handle).?;
+ const id = p.paneBySerial(serial).?;
try testing.expectEqualStrings("", p.panes[id].?.file.?.content);
- _ = wr(p, Node.of(opened.reply.handle, .body), "hi");
- const second = call(p, .{ .tag = 4, .op = .open, .node = new });
+ _ = wr(p, Node.of(serial, .body), "hi");
+ const second = th.mkdir(p, "another");
try testing.expectEqual(Status.ok, second.reply.status);
- try testing.expect(second.reply.handle != opened.reply.handle);
+ try testing.expect(second.reply.attr.node != made.reply.attr.node);
try testing.expectEqualStrings("hi", p.panes[id].?.file.?.content);
- try testing.expectEqual(E.INVAL, call(p, .{ .tag = 5, .op = .read, .node = new, .size = 64 }).errno());
+
+ // A plain file, and a create anywhere else, are refused.
+ try testing.expectEqual(E.PERM, call(p, .{ .tag = 1, .op = .open, .node = panes_dir, .data = "f", .create = true, .perm = 0o666 }).errno());
+ try testing.expectEqual(E.PERM, call(p, .{ .tag = 1, .op = .open, .node = root, .data = "d", .create = true, .perm = cloud9.dmdir | 0o755 }).errno());
+ try testing.expectEqual(E.PERM, call(p, .{ .tag = 1, .op = .open, .node = Node.of(serial, .dir), .data = "d", .create = true, .perm = cloud9.dmdir | 0o755 }).errno());
+
+ // Tremove closes the pane it names, and nothing else in the tree.
+ try testing.expectEqual(E.PERM, th.rmdir(p, Node.of(serial, .body)).errno());
+ try testing.expectEqual(E.PERM, th.rmdir(p, @intFromEnum(TopFile.index)).errno());
+ try testing.expect(p.paneBySerial(serial) != null);
+ try testing.expectEqual(Status.ok, th.rmdir(p, Node.of(serial, .dir)).reply.status);
+ try testing.expect(p.paneBySerial(serial) == null);
+ try testing.expectEqual(E.NOENT, th.rmdir(p, Node.of(serial, .dir)).errno());
}
test "directories walk to their parents" {
@@ -791,14 +847,15 @@ test "editor paths resolve to the same nodes the wire serves" {
try testing.expectEqual(root, resolveSelf(p, "").?);
try testing.expectEqual(root, resolveSelf(p, "/./pane/../").?);
try testing.expectEqual(@intFromEnum(TopFile.index), resolveSelf(p, "index").?);
- try testing.expectEqual(@intFromEnum(TopFile.new), resolveSelf(p, "new").?);
+ try testing.expectEqual(@intFromEnum(TopFile.status), resolveSelf(p, "status").?);
+ try testing.expect(resolveSelf(p, "new") == null);
try testing.expectEqual(@intFromEnum(TopFile.pane), resolveSelf(p, "pane").?);
try testing.expectEqual(Node.of(serial, .dir), resolveSelf(p, try std.fmt.bufPrint(&path, "pane/{d}", .{serial})).?);
try testing.expectEqual(Node.of(serial, .sel), resolveSelf(p, try std.fmt.bufPrint(&path, "pane/{d}/sel", .{serial})).?);
try testing.expect(resolveSelf(p, try std.fmt.bufPrint(&path, "pane/{d}/pty/ctl", .{serial})) == null);
try testing.expect(resolveSelf(p, try std.fmt.bufPrint(&path, "pane/{d}/rdsel", .{serial})) == null);
try testing.expect(resolveSelf(p, "self/index") == null);
- try testing.expect(resolveSelf(p, "new/ctl") == null);
+ try testing.expect(resolveSelf(p, "status/ctl") == null);
try testing.expect(resolveSelf(p, "cons") == null);
try testing.expect(resolveSelf(p, "pane/0") == null);
}