summaryrefslogtreecommitdiff
path: root/src/fs.zig
diff options
context:
space:
mode:
authorGabriel Schneider <[email protected]>2026-09-22 11:15:46 -0300
committerGabriel Schneider <[email protected]>2026-10-01 00:12:14 -0300
commit31cb659ded4cf50af5903fc107f8c868ee3c7311 (patch)
tree8ceaf0ae085cfee121a1b4bdcb923b2c03b01a60 /src/fs.zig
parenta1d5ee19a648abc65b557dffa14a3b2f70577286 (diff)
downloadpardes-31cb659ded4cf50af5903fc107f8c868ee3c7311.tar.gz
pardes-31cb659ded4cf50af5903fc107f8c868ee3c7311.zip
Answer 9P on the connection's task, so a session can open its own tree
The editor's loop was the only thing that could answer a 9P request, which made the editor's own syscalls through a mount of its own tree -- a Look at /mnt/9p/pardes/<me>/anything under a `9ns --mntgen` view, a Save into it -- requests only the blocked loop could serve. The name-based refusal that followed (ownMountSuffix) and the in-process routing of a mount of oneself (Client.sameSession) were patches over that, and both are gone, with the mailbox that shipped every request to the editor's thread. One rule replaces them, `pardes.turn`: the core is single-threaded, the editor's thread has the turn by default and gives it up in two kinds of gap -- while it waits for input and while a step of it is out in a host syscall -- and a cloud9 connection task takes it in those gaps to answer. `out` counts the steps that are out, from any thread: while one is, the core reads consistently but that step still holds pointers into it, so a request that would change a pane (a write, a truncation, an rmdir) is parked in the engine and retried when the turn is next given up with nothing out, and the editor's own wake waits for the count to reach zero. It is never a write of its own that a step waits on out there -- writes come from a shell performing a save between steps -- so a parked request is never the syscall's own, and making a pane or rendering a screen need not park: every yield sits before its step's mutation, so the layout and the surface are whole under it. A changing request that queued effects is answered once the editor has performed them (`echo Save > exec` returns with the file written, as acme's `put` does), and it settles the way a step does, because without that a /log reader waited for the user's next keystroke. Every host syscall on a user path has to give the turn up, not fs.zig's alone: the first end-to-end run hung in `inotify_add_watch` performing the new pane's watch effect. PDFs and images are read whole at open, so no draw goes out into the host. The core's allocator takes its fixed buffer through the lock-free interface, since a connection task allocates while the editor's thread is out in a syscall that allocates too. A Restore puts the replacement in first and releases every task waiting on the old core. cloud9 (pinned at eb1a104) parks an open, a truncating wstat, a clunk and a remove on `again`, not only reads and writes, and answers a parked job whose fid was clunked without asking the backend. Verified: test/selfmount.py runs the editor under `9ns --mntgen` and Looks at, reads and Saves its own tree through the mount; a unit test pins that a change parks while the editor is out mid-step and lands when it rests, while a read is answered in the window. 9P over the Unix socket against a tty session, same machine, Debug builds: a read of /index 278us -> 61us, a truncating body write 1184us -> 609us, exec Save 718us -> 583us; the gesture benchmark is unchanged (geometric mean 0.997 over 53 cells). Also from the reviews: a notice chip over an image or PDF pane was painted out by the picture drawn after the cells, so pictures give up the rows; in the GUI a tree-sitter context band painted over the chip, so body layers are emitted first; a message is one row of printable text, its 256-byte cut never leaves half a glyph, and one wider than its pane keeps its tail (the file name, the reason) rather than its head. Co-Authored-By: Claude Fable 5.1 <[email protected]>
Diffstat (limited to 'src/fs.zig')
-rw-r--r--src/fs.zig444
1 files changed, 147 insertions, 297 deletions
diff --git a/src/fs.zig b/src/fs.zig
index 9200236d..85eae825 100644
--- a/src/fs.zig
+++ b/src/fs.zig
@@ -64,10 +64,9 @@ pub const WriteError = error{
WriteFailed,
};
+/// The host takes the bytes. The caller has given the turn up (`write`),
+/// because the path may be a mount this editor serves.
pub fn writeFile(path: []const u8, bytes: []const u8) WriteError!void {
- // No core here to answer from, so the only safe answer is no answer: an
- // open(2) into our own mount is the call that never returns.
- if (isOwnMount(path)) return error.OpenFailed;
var pathbuf: [4096:0]u8 = undefined;
if (path.len >= pathbuf.len) return error.PathTooLong;
if (std.mem.indexOfScalar(u8, path, 0) != null) return error.OpenFailed;
@@ -124,12 +123,21 @@ fn osNode(p: *pardes.Pardes, path: []const u8) !u64 {
return node;
}
+/// The host filesystem under /os. Every syscall here may go out through a
+/// mount this editor serves, so the turn is given up around each and taken
+/// back before the core is touched; `path` is the table entry's own copy and
+/// outlives the yield, and nothing is staged into the shared reply buffer
+/// until the turn is back.
pub fn osHandle(p: *pardes.Pardes, req: Req) Reply {
if (comptime !platform_has_fs) return Reply.fail(req.tag, E.NOENT);
const path = osPath(p, req.node) orelse return Reply.fail(req.tag, E.NOENT);
if (req.op == .release) return .{ .tag = req.tag };
const io = std.Io.Threaded.global_single_threaded.io();
- const stat = std.Io.Dir.cwd().statFile(io, path, .{}) catch return Reply.fail(req.tag, E.NOENT);
+ const stat = stat: {
+ pardes.turn.yield();
+ defer pardes.turn.back();
+ break :stat std.Io.Dir.cwd().statFile(io, path, .{}) catch return Reply.fail(req.tag, E.NOENT);
+ };
const attr: Reply.Attr = .{ .name = if (req.node == os_root) "os" else std.fs.path.basename(path), .node = req.node, .dir = stat.kind == .directory, .size = stat.size, .mode = if (stat.kind == .directory) 0o755 else 0o644, .mtime = std.math.cast(u32, stat.mtime.toSeconds()) orelse 0 };
switch (req.op) {
.getattr => return .{ .tag = req.tag, .attr = attr },
@@ -150,52 +158,67 @@ pub fn osHandle(p: *pardes.Pardes, req: Req) Reply {
return osHandle(p, .{ .tag = req.tag, .op = .getattr, .node = node });
},
.readdir => {
- var dir = std.Io.Dir.cwd().openDir(io, path, .{ .iterate = true }) catch return Reply.fail(req.tag, E.NOTDIR);
- defer dir.close(io);
- var it = dir.iterate();
- const out = p.fs.stage(p.gpa);
- var skip = req.off;
- while (it.next(io) catch return Reply.fail(req.tag, E.IO)) |entry| {
- var buf: [4096]u8 = undefined;
- const joined = std.fmt.bufPrint(&buf, "{s}/{s}", .{ std.mem.trimEnd(u8, path, "/"), entry.name }) catch continue;
- var normalized_buf: [4096]u8 = undefined;
- const normalized = resolveOs(joined, &normalized_buf) orelse continue;
- const child = dir.statFile(io, entry.name, .{}) catch continue;
- if (skip > 0) {
- skip -= 1;
- continue;
+ // Listed into a buffer of this request's own first; the shared
+ // reply buffer is another request's to use while the turn is out.
+ var listing: [16 * 1024]u8 = undefined;
+ var fixed: std.heap.FixedBufferAllocator = .init(&listing);
+ var listed: std.ArrayList(u8) = .empty;
+ {
+ pardes.turn.yield();
+ defer pardes.turn.back();
+ var dir = std.Io.Dir.cwd().openDir(io, path, .{ .iterate = true }) catch return Reply.fail(req.tag, E.NOTDIR);
+ defer dir.close(io);
+ var it = dir.iterate();
+ var skip = req.off;
+ while (it.next(io) catch return Reply.fail(req.tag, E.IO)) |entry| {
+ var buf: [4096]u8 = undefined;
+ const joined = std.fmt.bufPrint(&buf, "{s}/{s}", .{ std.mem.trimEnd(u8, path, "/"), entry.name }) catch continue;
+ var normalized_buf: [4096]u8 = undefined;
+ const normalized = resolveOs(joined, &normalized_buf) orelse continue;
+ const child = dir.statFile(io, entry.name, .{}) catch continue;
+ if (skip > 0) {
+ skip -= 1;
+ continue;
+ }
+ const node = if (std.mem.eql(u8, normalized.path, "/")) os_root else os_node | (std.hash.Wyhash.hash(0, normalized.path) & (os_node - 1));
+ tree.stageDirent(&listed, fixed.allocator(), node, child.kind == .directory, entry.name);
+ if (listed.items.len >= @max(req.size, 512)) break;
}
- const node = if (std.mem.eql(u8, normalized.path, "/")) os_root else os_node | (std.hash.Wyhash.hash(0, normalized.path) & (os_node - 1));
- tree.stageDirent(out, p.gpa, node, child.kind == .directory, entry.name);
- if (out.items.len >= @max(req.size, 512)) break;
}
+ const out = p.fs.stage(p.gpa);
+ out.appendSlice(p.gpa, listed.items) catch return Reply.fail(req.tag, E.NOMEM);
return .{ .tag = req.tag, .payload = .{ .staged = @intCast(out.items.len) } };
},
.read, .write, .setattr => {
if (stat.kind != .file) return Reply.fail(req.tag, E.PERM);
var z: [4096]u8 = undefined;
const path_z = std.fmt.bufPrintSentinel(&z, "{s}", .{path}, 0) catch return Reply.fail(req.tag, E.NOENT);
- const fd = libc.open(path_z, .{ .ACCMODE = if (req.op == .read) .RDONLY else .WRONLY, .NONBLOCK = true, .CLOEXEC = true });
- if (fd < 0) return Reply.fail(req.tag, E.PERM);
- defer _ = libc.close(fd);
- if (req.op == .setattr) {
- if (!req.truncate or req.off != 0 or libc.ftruncate(fd, 0) != 0) return Reply.fail(req.tag, E.INVAL);
- var truncated = attr;
- truncated.size = 0;
- return .{ .tag = req.tag, .attr = truncated };
- }
- if (req.off > std.math.maxInt(i64)) return Reply.fail(req.tag, E.INVAL);
- if (libc.lseek(fd, @intCast(req.off), libc.SEEK.SET) < 0) return Reply.fail(req.tag, E.IO);
- if (req.op == .write) {
- const written = libc.write(fd, req.data.ptr, req.data.len);
- if (written < 0) return Reply.fail(req.tag, E.IO);
- return .{ .tag = req.tag, .written = @intCast(written) };
- }
+ var buf: [ninep_io.msize]u8 = undefined; // a read never asks for more than a frame
+ const got = io: {
+ pardes.turn.yield();
+ defer pardes.turn.back();
+ const fd = libc.open(path_z, .{ .ACCMODE = if (req.op == .read) .RDONLY else .WRONLY, .NONBLOCK = true, .CLOEXEC = true });
+ if (fd < 0) return Reply.fail(req.tag, E.PERM);
+ defer _ = libc.close(fd);
+ if (req.op == .setattr) {
+ if (!req.truncate or req.off != 0 or libc.ftruncate(fd, 0) != 0) return Reply.fail(req.tag, E.INVAL);
+ var truncated = attr;
+ truncated.size = 0;
+ return .{ .tag = req.tag, .attr = truncated };
+ }
+ if (req.off > std.math.maxInt(i64)) return Reply.fail(req.tag, E.INVAL);
+ if (libc.lseek(fd, @intCast(req.off), libc.SEEK.SET) < 0) return Reply.fail(req.tag, E.IO);
+ if (req.op == .write) {
+ const written = libc.write(fd, req.data.ptr, req.data.len);
+ if (written < 0) return Reply.fail(req.tag, E.IO);
+ return .{ .tag = req.tag, .written = @intCast(written) };
+ }
+ const got = libc.read(fd, &buf, @min(req.size, buf.len));
+ if (got < 0) return Reply.fail(req.tag, E.IO);
+ break :io @as(usize, @intCast(got));
+ };
const out = p.fs.stage(p.gpa);
- out.resize(p.gpa, @min(req.size, 65536)) catch return Reply.fail(req.tag, E.NOMEM);
- const got = libc.read(fd, out.items.ptr, out.items.len);
- if (got < 0) return Reply.fail(req.tag, E.IO);
- out.shrinkRetainingCapacity(@intCast(got));
+ out.appendSlice(p.gpa, buf[0..got]) catch return Reply.fail(req.tag, E.NOMEM);
return .{ .tag = req.tag, .payload = .{ .staged = @intCast(got) } };
},
else => return Reply.fail(req.tag, E.PERM),
@@ -378,21 +401,22 @@ test "bounded reads accept exact OS file lengths and empty files" {
const empty = try readLimit(p, explicit, 0);
defer gpa.free(empty);
try std.testing.expectEqual(@as(usize, 0), empty.len);
- try std.testing.expectEqual(@as(usize, 0), p.fs.os_paths.items.len);
+ // The directory's node stays in the table for the listener to collect
+ // once no connection names it (src/9p_io.zig, collectOs): a client may
+ // have walked to the same directory meanwhile and hold it by that node.
+ try std.testing.expectEqual(@as(usize, 1), p.fs.os_paths.items.len);
+ try std.testing.expectEqualStrings(native, p.fs.os_paths.items[0].path);
}
test "bounded reads apply the same limits to self bodies and embedded sources" {
const gpa = std.testing.allocator;
const p = try Pardes.init(gpa, .{ .tty_only = true });
defer p.deinit();
- p.fs.socket_path = "/tmp/pardes-limited-in-process.sock";
- try p.fs.mount(gpa, "own", p.fs.socket_path);
var contents: [8209]u8 = @splat('x');
contents[contents.len - 1] = '\n';
for ([_][]const u8{ &contents, "" }) |expected| {
const pane = try p.setTestFile(expected);
- for ([_][]const u8{ "/virtual", "/n/self", "/n/own" }) |prefix| {
- if (!platform_has_fs and std.mem.eql(u8, prefix, "/n/own")) continue;
+ for ([_][]const u8{ "/virtual", "/n/self" }) |prefix| {
var path_buf: [128]u8 = undefined;
const path = try std.fmt.bufPrint(&path_buf, "{s}/pane/{d}/body", .{ prefix, pane.serial });
const bytes = try readLimit(p, path, expected.len);
@@ -407,8 +431,7 @@ test "bounded reads apply the same limits to self bodies and embedded sources" {
}
if (limits.embedded_sources) {
const source = findEmbeddedSource("src/look.zig", false).?;
- for ([_][]const u8{ "/virtual/src/look.zig", "/n/self/src/look.zig", "/n/own/src/look.zig" }) |path| {
- if (!platform_has_fs and std.mem.startsWith(u8, path, "/n/own/")) continue;
+ for ([_][]const u8{ "/virtual/src/look.zig", "/n/self/src/look.zig" }) |path| {
const bytes = try readLimit(p, path, source.contents.len);
defer gpa.free(bytes);
try std.testing.expectEqualStrings(source.contents, bytes);
@@ -425,10 +448,8 @@ test "bounded reads count rendered directory paths and unknown self lengths" {
const gpa = std.testing.allocator;
const p = try Pardes.init(gpa, .{ .tty_only = true });
defer p.deinit();
- p.fs.socket_path = "/tmp/pardes-limited-in-process.sock";
- try p.fs.mount(gpa, "own", p.fs.socket_path);
- for ([_][]const u8{ "/n", "/virtual", "/n/self", "/n/own", "/n/own/pane", "/virtual/listeners", "/virtual/README" }) |path| {
- if (!platform_has_fs and std.mem.startsWith(u8, path, "/n/own")) continue;
+ p.fs.socket_path = "/tmp/pardes-limited-in-process.sock"; // so listeners has a line
+ for ([_][]const u8{ "/n", "/virtual", "/n/self", "/virtual/pane", "/virtual/listeners", "/virtual/README" }) |path| {
const expected = try read(p, path);
defer gpa.free(expected);
try std.testing.expect(expected.len > 0);
@@ -573,49 +594,6 @@ test "virtual body writes can read their input from the same pane" {
try std.testing.expectEqualStrings("pane contents\n", pane.file.?.content);
}
-test "same-core mount directories preserve their mount prefix across reads" {
- if (!platform_has_fs) return error.SkipZigTest;
- const gpa = std.testing.allocator;
- const p = try pardes.Pardes.init(gpa, .{ .tty_only = true });
- defer p.deinit();
- const socket_path = "/tmp/pardes-in-process-mount.sock";
- p.fs.socket_path = socket_path;
- try p.fs.mount(gpa, "own", socket_path);
- const roots = try read(p, "/n/own");
- defer gpa.free(roots);
- try std.testing.expect(std.mem.startsWith(u8, roots, "/n/own/README\n/n/own/index\n"));
- try std.testing.expect(std.mem.indexOf(u8, roots, "/n/own/pane/\n/n/own/os/\n") != null);
- try std.testing.expectError(error.NotADirectory, read(p, "/n/own/index/.."));
- try std.testing.expectError(error.FileNotFound, read(p, "/n/own/self/index"));
-
- var tmp = std.testing.tmpDir(.{});
- defer tmp.cleanup();
- for (0..520) |i| {
- var name: [32]u8 = undefined;
- try tmp.dir.writeFile(std.testing.io, .{ .sub_path = try std.fmt.bufPrint(&name, "entry-{d:0>4}.txt", .{i}), .data = "child\n" });
- }
- var directory_buffer: [4096]u8 = undefined;
- const directory = directory_buffer[0..try tmp.dir.realPath(std.testing.io, &directory_buffer)];
- const held_node = try osNode(p, directory);
- const path = try std.fmt.allocPrint(gpa, "/n/own/os{s}", .{directory});
- defer gpa.free(path);
- const listing = try read(p, path);
- defer gpa.free(listing);
- var entries = std.mem.tokenizeScalar(u8, listing, '\n');
- var count: usize = 0;
- while (entries.next()) |entry| {
- try std.testing.expect(std.mem.startsWith(u8, entry, path));
- const child = try read(p, entry);
- defer gpa.free(child);
- try std.testing.expectEqualStrings("child\n", child);
- try std.testing.expectEqual(@as(usize, 1), p.fs.os_paths.items.len);
- count += 1;
- }
- try std.testing.expectEqual(@as(usize, 520), count);
- try std.testing.expectEqual(held_node, p.fs.os_paths.items[0].node);
- try std.testing.expectEqualStrings(directory, p.fs.os_paths.items[0].path);
-}
-
pub fn isVirtual(path: []const u8) bool {
return std.mem.eql(u8, path, "/virtual") or std.mem.startsWith(u8, path, "/virtual/") or
std.mem.eql(u8, path, "/n") or std.mem.startsWith(u8, path, "/n/");
@@ -674,9 +652,6 @@ pub fn resolve(p: ?*pardes.Pardes, word: []const u8, cwd: []const u8, out: *[409
}
if (std.mem.eql(u8, joined, "/virtual")) return resolveVirtual(p, "/", out);
if (std.mem.startsWith(u8, joined, "/virtual/")) return resolveVirtual(p, joined[8..], out);
- // This editor's own tree, seen through a mount: answer from the tree
- // instead of walking out into the view and back in.
- if (ownMountSuffix(joined)) |inner| return resolveVirtual(p, inner, out);
if (resolveOs(joined, out)) |found| return found;
if (resolveVirtual(p, joined, out)) |found| return found;
if (resolveVirtual(p, word, out)) |found| return found;
@@ -687,116 +662,15 @@ pub fn resolve(p: ?*pardes.Pardes, word: []const u8, cwd: []const u8, out: *[409
return null;
}
-/// The name this session is posted under, taken from its socket path.
-/// As wide as a name a listener will accept, so there is no session whose
-/// name is too long to recognise and therefore too long to protect.
-var own_name_buf: [108]u8 = undefined;
-var own_name_len: usize = 0;
-
-pub fn noteOwnSocket(socket_path: []const u8) void {
- own_name_len = 0;
- const base = std.fs.path.basename(socket_path);
- const head = "pardes-9p-";
- const tail = ".sock";
- if (!std.mem.startsWith(u8, base, head) or !std.mem.endsWith(u8, base, tail)) return;
- const name = base[head.len .. base.len - tail.len];
- if (name.len == 0 or name.len > own_name_buf.len) return;
- @memcpy(own_name_buf[0..name.len], name);
- own_name_len = name.len;
-}
-
-/// What this path names inside this editor's OWN 9P tree, if it does:
-/// `/mnt/9p/pardes/<me>/pane/3/body` -> `pane/3/body`, and the mount root
-/// itself -> `/`.
-///
-/// A mount is a VIEW of a tree this editor already holds. Going out through
-/// the view to reach it deadlocks the session outright -- the realpath, the
-/// stat and the read all leave through the mount and come back as 9P requests
-/// only this editor's loop can answer, while that loop is blocked making them,
-/// and the filesystem then stops answering anybody. So the view is recognised
-/// by NAME, before any syscall (the syscall is the thing that never returns),
-/// and the request is served from the tree directly. Same answer, no round
-/// trip, and a session can drive itself through its own 9P namespace.
-pub fn ownMountSuffix(path: []const u8) ?[]const u8 {
- if (own_name_len == 0 or path.len == 0 or path[0] != '/') return null;
- // Whole components, and the registry's own layout: a session is posted at
- // `<runtime>/9p/pardes/<name>` and mounts group under `/mnt/9p/pardes/`.
- // Matching a bare `/pardes/<name>` anywhere in the string would claim
- // `~/src/pardes/<name>/README` -- an ordinary directory that happens to
- // read like a mount -- and serve the tree's README over the real file.
- var buf: [own_name_buf.len + 16]u8 = undefined;
- const stem = std.fmt.bufPrint(&buf, "/9p/pardes/{s}", .{own_name_buf[0..own_name_len]}) catch return null;
- var at: usize = 0;
- while (std.mem.indexOfPos(u8, path, at, stem)) |found| : (at = found + 1) {
- const rest = path[found + stem.len ..];
- if (rest.len != 0 and rest[0] != '/') continue; // a longer name that merely starts the same
- if (rest.len <= 1) return "/";
- return rest[1..];
- }
- return null;
-}
-
-pub fn isOwnMount(path: []const u8) bool {
- return ownMountSuffix(path) != null;
-}
-
-test "a path inside this session's own mount is answered from the tree, not through the mount" {
- noteOwnSocket("/run/user/1000/pardes-9p-demo.sock");
- defer own_name_len = 0;
- // What the path names inside the tree, with the mount prefix taken off.
- try std.testing.expectEqualStrings("index", ownMountSuffix("/mnt/9p/pardes/demo/index").?);
- try std.testing.expectEqualStrings("pane/new", ownMountSuffix("/mnt/9p/pardes/demo/pane/new").?);
- try std.testing.expectEqualStrings("/", ownMountSuffix("/mnt/9p/pardes/demo").?);
- try std.testing.expectEqualStrings("/", ownMountSuffix("/mnt/9p/pardes/demo/").?);
- // The registry posts the session there too, so that spelling counts.
- try std.testing.expectEqualStrings("index", ownMountSuffix("/run/user/1000/9p/pardes/demo/index").?);
- // Another session's mount is somebody else's to answer, and a name that
- // merely STARTS with ours is a different name.
- try std.testing.expect(ownMountSuffix("/mnt/9p/pardes/other/index") == null);
- try std.testing.expect(ownMountSuffix("/mnt/9p/pardes/demo2/index") == null);
- // An ordinary directory that reads like a mount is an ordinary directory.
- // Serving the tree here would hand back the tree's README for the file on
- // disk, and -- before `write` learned the same trick -- write the tree's
- // bytes over it.
- try std.testing.expect(ownMountSuffix("/home/goblin/src/pardes/demo/README") == null);
- try std.testing.expect(ownMountSuffix("/home/goblin/src/pardes/demo.zig") == null);
- // Relative paths never name a mount: they are resolved against a cwd first.
- try std.testing.expect(ownMountSuffix("mnt/9p/pardes/demo/index") == null);
-
- // The syscall is the thing that never returns, so it is never made.
- var out: [4096]u8 = undefined;
- try std.testing.expect(resolveOs("/mnt/9p/pardes/demo/index", &out) == null);
-
- // A session with no listener has no name, so nothing is redirected.
- noteOwnSocket("/tmp/not-a-pardes-socket");
- try std.testing.expect(ownMountSuffix("/mnt/9p/pardes/demo/index") == null);
-}
-
-test "reading this session's own mount returns what the tree holds" {
- const p = try pardes.Pardes.init(std.testing.allocator, .{ .tty_only = true, .cols = 80, .rows = 24 });
- defer p.deinit();
- noteOwnSocket("/run/user/1000/pardes-9p-selfread.sock");
- defer own_name_len = 0;
-
- const direct = try read(p, "/n/self/index");
- defer p.gpa.free(direct);
- const mounted = try read(p, "/mnt/9p/pardes/selfread/index");
- defer p.gpa.free(mounted);
- try std.testing.expectEqualStrings(direct, mounted);
- try std.testing.expect(direct.len > 0);
-
- // A write goes to the same tree the read came from, not out through the
- // mount: `/index` is 0400 there, and an OS write would instead try to
- // create a file under a directory that does not exist.
- try std.testing.expectError(error.ReadOnlyFilesystem, write(p, "/mnt/9p/pardes/selfread/index", "nope\n"));
- try std.testing.expectError(error.ReadOnlyFilesystem, write(p, "/n/self/index", "nope\n"));
-}
-
+/// Where a path really is on the host, and whether it is a directory. The
+/// path may lie in a mount this editor serves, so the turn is given up for
+/// the syscalls: another thread answers them.
pub fn resolveOs(path: []const u8, out: *[4096]u8) ?Resolved {
if (comptime !platform_has_fs) return null;
- if (isOwnMount(path)) return null;
var z: [4096]u8 = undefined;
const path_z = std.fmt.bufPrintSentinel(&z, "{s}", .{path}, 0) catch return null;
+ pardes.turn.yield();
+ defer pardes.turn.back();
const resolved = realpath(path_z, out) orelse return null;
return .{ .path = std.mem.span(resolved), .dir = isDir(resolved) };
}
@@ -818,13 +692,6 @@ pub fn read(p: *pardes.Pardes, path: []const u8) ![]u8 {
}
pub fn readLimit(p: *pardes.Pardes, path: []const u8, max_bytes: usize) ![]u8 {
- // The same view, reached by a reader that never went through `resolve`.
- // The rewritten path cannot contain the mount stem, so this recurses once.
- if (ownMountSuffix(path)) |inner| {
- var self_buf: [4096]u8 = undefined;
- const internal = std.fmt.bufPrint(&self_buf, "/n/self/{s}", .{inner}) catch return error.FileTooLarge;
- return readLimit(p, internal, max_bytes);
- }
const limit = @min(max_bytes, limits.max_file_bytes);
if (std.mem.eql(u8, std.mem.trimEnd(u8, path, "/"), "/n")) {
var out: std.Io.Writer.Allocating = .init(p.gpa);
@@ -847,11 +714,8 @@ pub fn readLimit(p: *pardes.Pardes, path: []const u8, max_bytes: usize) ![]u8 {
var native_buf: [4096]u8 = undefined;
const native = resolveOs(remote_path, &native_buf) orelse return readFileLimit(p.gpa, path, limit);
if (!native.dir) return readFileLimit(p.gpa, path, limit);
- const saved_paths = p.fs.os_paths.items.len;
- defer {
- for (p.fs.os_paths.items[saved_paths..]) |temporary| p.gpa.free(temporary.path);
- p.fs.os_paths.shrinkRetainingCapacity(saved_paths);
- }
+ // The node stays in the table until no connection names it either
+ // (src/9p_io.zig, collectOs): a client may have walked to it too.
const node = try osNode(p, native.path);
return readNode(p, node, path, limit);
}
@@ -861,25 +725,10 @@ pub fn readLimit(p: *pardes.Pardes, path: []const u8, max_bytes: usize) ![]u8 {
}
for (p.fs.mounts.items) |mount| {
if (!std.mem.eql(u8, name, mount.name)) continue;
- if (ninep_io.Client.sameSession(mount.dial, p.fs.socket_path, p.fs.tcp_address, p.fs.quic_address)) {
- const saved_paths = p.fs.os_paths.items.len;
- defer {
- for (p.fs.os_paths.items[saved_paths..]) |temporary| p.gpa.free(temporary.path);
- p.fs.os_paths.shrinkRetainingCapacity(saved_paths);
- }
- var node = tree.root;
- var directory = true;
- var parts = std.mem.tokenizeScalar(u8, remote_path, '/');
- while (parts.next()) |part| {
- if (!directory) return error.NotADirectory;
- if (std.mem.eql(u8, part, ".")) continue;
- const reply = tree.handle(p, .{ .tag = 0, .op = .lookup, .node = node, .data = part });
- if (reply.status != .ok) return error.FileNotFound;
- node = reply.attr.node;
- directory = reply.attr.dir;
- }
- return readNode(p, node, path, limit);
- }
+ // A peer answers on its own time, and the peer may be this very
+ // editor: the turn goes out with the request.
+ pardes.turn.yield();
+ defer pardes.turn.back();
return ninep_io.Client.readLimit(p.gpa, mount.dial, remote_path, path, limit);
}
return error.FileNotFound;
@@ -940,68 +789,64 @@ fn readNode(p: *pardes.Pardes, initial_node: u64, path: []const u8, limit: usize
}
}
+/// The editor's, between steps: a save or a dump the shell is performing,
+/// never a step of the core.
pub fn write(p: *pardes.Pardes, path: []const u8, bytes: []const u8) !void {
- // The same view `readLimit` answers from the tree, so a write lands where
- // the matching read came from -- and, just as importantly, never becomes
- // an open(2) through a mount this loop is the one that answers.
- if (ownMountSuffix(path)) |inner| {
- var self_buf: [4096]u8 = undefined;
- const internal = std.fmt.bufPrint(&self_buf, "/n/self/{s}", .{inner}) catch return error.PathTooLong;
- return write(p, internal, bytes);
- }
if (std.mem.eql(u8, std.mem.trimEnd(u8, path, "/"), "/n")) return error.IsDirectory;
- var self_path: ?[]const u8 = null;
- if (std.mem.eql(u8, path, "/virtual")) self_path = "";
- if (std.mem.startsWith(u8, path, "/virtual/")) self_path = path[9..];
- if (std.mem.startsWith(u8, path, "/n/")) {
- const explicit = path[3..];
- const cut = std.mem.indexOfScalar(u8, explicit, '/') orelse explicit.len;
- const name = explicit[0..cut];
- const remote_path = if (cut < explicit.len) explicit[cut..] else "/";
- if (std.mem.eql(u8, name, "os")) return writeFile(remote_path, bytes);
- if (std.mem.eql(u8, name, "self")) {
- self_path = remote_path;
- } else {
- for (p.fs.mounts.items) |mount| {
- if (!std.mem.eql(u8, name, mount.name)) continue;
- if (ninep_io.Client.sameSession(mount.dial, p.fs.socket_path, p.fs.tcp_address, p.fs.quic_address)) {
- var buf: [4096]u8 = undefined;
- const local = try std.fmt.bufPrint(&buf, "/n{s}", .{remote_path});
- return write(p, local, bytes);
- }
- return ninep_io.Client.write(p.gpa, mount.dial, remote_path, bytes);
- }
- return error.FileNotFound;
- }
+ if (std.mem.eql(u8, path, "/virtual")) return writeSelf(p, "", bytes);
+ if (std.mem.startsWith(u8, path, "/virtual/")) return writeSelf(p, path[9..], bytes);
+ if (!std.mem.startsWith(u8, path, "/n/")) return writeOut(p, null, path, bytes);
+ const explicit = path[3..];
+ const cut = std.mem.indexOfScalar(u8, explicit, '/') orelse explicit.len;
+ const name = explicit[0..cut];
+ const remote_path = if (cut < explicit.len) explicit[cut..] else "/";
+ if (std.mem.eql(u8, name, "os")) return writeOut(p, null, remote_path, bytes);
+ if (std.mem.eql(u8, name, "self")) return writeSelf(p, remote_path, bytes);
+ for (p.fs.mounts.items) |mount| {
+ if (std.mem.eql(u8, name, mount.name)) return writeOut(p, mount.dial, remote_path, bytes);
}
- if (self_path) |name| {
- var node = tree.resolveSelf(p, name) orelse return error.FileNotFound;
- const attributes = tree.handle(p, .{ .tag = 0, .op = .getattr, .node = node });
- if (attributes.status != .ok) return error.FileNotFound;
- if (attributes.attr.dir) return error.IsDirectory;
- if (attributes.attr.mode & 0o200 == 0) return error.ReadOnlyFilesystem;
- const opened = tree.handle(p, .{ .tag = 0, .op = .open, .node = node });
- if (opened.status != .ok) return error.OpenFailed;
- if (opened.attr.node != 0) node = opened.attr.node;
- defer _ = tree.handle(p, .{ .tag = 0, .op = .release, .node = node, .handle = opened.handle });
- var preserved: ?[]u8 = null;
- defer if (preserved) |copy| p.gpa.free(copy);
- const target = tree.Node.target(node);
- if (target != null and target.? == .pane and target.?.pane.file == .body) {
- preserved = try p.gpa.dupe(u8, bytes);
- const trunc = tree.handle(p, .{ .tag = 0, .op = .setattr, .node = node, .truncate = true });
- if (trunc.status != .ok) return error.WriteFailed;
- }
- const contents = preserved orelse bytes;
- var off: usize = 0;
- while (off < contents.len) {
- const reply = tree.handle(p, .{ .tag = 0, .op = .write, .node = node, .off = off, .data = contents[off..] });
- if (reply.status != .ok or reply.written == 0) return error.WriteFailed;
- off += reply.written;
- }
- return;
+ return error.FileNotFound;
+}
+
+/// The host, or the peer at `dial`, takes the bytes with the turn given up
+/// entirely -- the peer may be this very editor, and a write it serves may
+/// change any pane -- so they are copied first: `bytes` is usually a pane's
+/// own text.
+fn writeOut(p: *pardes.Pardes, dial: ?[]const u8, path: []const u8, bytes: []const u8) !void {
+ const copy = try p.gpa.dupe(u8, bytes);
+ defer p.gpa.free(copy);
+ pardes.turn.rest();
+ defer pardes.turn.wake();
+ if (dial) |d| return ninep_io.Client.write(p.gpa, d, path, copy);
+ return writeFile(path, copy);
+}
+
+/// This editor's own tree, written in place.
+fn writeSelf(p: *pardes.Pardes, name: []const u8, bytes: []const u8) !void {
+ var node = tree.resolveSelf(p, name) orelse return error.FileNotFound;
+ const attributes = tree.handle(p, .{ .tag = 0, .op = .getattr, .node = node });
+ if (attributes.status != .ok) return error.FileNotFound;
+ if (attributes.attr.dir) return error.IsDirectory;
+ if (attributes.attr.mode & 0o200 == 0) return error.ReadOnlyFilesystem;
+ const opened = tree.handle(p, .{ .tag = 0, .op = .open, .node = node });
+ if (opened.status != .ok) return error.OpenFailed;
+ if (opened.attr.node != 0) node = opened.attr.node;
+ defer _ = tree.handle(p, .{ .tag = 0, .op = .release, .node = node, .handle = opened.handle });
+ var preserved: ?[]u8 = null;
+ defer if (preserved) |copy| p.gpa.free(copy);
+ const target = tree.Node.target(node);
+ if (target != null and target.? == .pane and target.?.pane.file == .body) {
+ preserved = try p.gpa.dupe(u8, bytes);
+ const trunc = tree.handle(p, .{ .tag = 0, .op = .setattr, .node = node, .truncate = true });
+ if (trunc.status != .ok) return error.WriteFailed;
+ }
+ const contents = preserved orelse bytes;
+ var off: usize = 0;
+ while (off < contents.len) {
+ const reply = tree.handle(p, .{ .tag = 0, .op = .write, .node = node, .off = off, .data = contents[off..] });
+ if (reply.status != .ok or reply.written == 0) return error.WriteFailed;
+ off += reply.written;
}
- return writeFile(path, bytes);
}
fn resolveEmbedded(word: []const u8, cwd: []const u8, scratch: *[4096]u8) ?Source {
@@ -1069,6 +914,8 @@ pub fn find(arena: std.mem.Allocator, dir: []const u8, pat: []const u8, out: []u
var hits: [find_max_hits][]const u8 = undefined;
var hits_len: usize = 0;
if (platform_has_fs) {
+ pardes.turn.yield();
+ defer pardes.turn.back();
const io = std.Io.Threaded.global_single_threaded.io();
var root = try std.Io.Dir.cwd().openDir(io, dir, .{ .iterate = true });
defer root.close(io);
@@ -1152,6 +999,9 @@ pub fn grep(arena: std.mem.Allocator, gpa: std.mem.Allocator, dir: []const u8, b
const home = std.mem.trimEnd(u8, base, "/");
const files = try arena.alloc([]const u8, grep_max_files);
var files_len: usize = 0;
+ // A walk and thousands of reads: the turn goes out for all of it.
+ pardes.turn.yield();
+ defer pardes.turn.back();
{
const io = std.Io.Threaded.global_single_threaded.io();
var root = try std.Io.Dir.cwd().openDir(io, dir, .{ .iterate = true });
@@ -1311,10 +1161,6 @@ test "Restore prefers default directory then falls back to original path" {
fn readFileLimit(gpa: std.mem.Allocator, path: []const u8, limit: usize) ![]u8 {
if (std.mem.indexOfScalar(u8, path, 0) != null) return error.OpenFailed;
- // Same reason as `writeFile`: no core to answer from here, and the open is
- // the call that never returns. Callers holding one use `readLimit`, which
- // serves the tree instead.
- if (isOwnMount(path)) return error.FileNotFound;
if (!platform_has_fs or std.mem.startsWith(u8, path, "/virtual/")) {
const archive_path = if (std.mem.startsWith(u8, path, "/virtual/")) path[9..] else path;
var normalized_buf: [4096]u8 = undefined;
@@ -1326,6 +1172,10 @@ fn readFileLimit(gpa: std.mem.Allocator, path: []const u8, limit: usize) ![]u8 {
var pathbuf: [4096]u8 = undefined;
const native = localPath(path) orelse return error.OpenFailed;
const path_z = std.fmt.bufPrintSentinel(&pathbuf, "{s}", .{native}, 0) catch return error.PathTooLong;
+ // The path may be a mount this editor serves: the turn goes out with the
+ // syscalls, and the bytes come back into memory of the caller's own.
+ pardes.turn.yield();
+ defer pardes.turn.back();
const fd = libc.open(path_z, .{ .ACCMODE = .RDONLY, .NONBLOCK = true });
if (fd < 0) return switch (libc.errno(fd)) {
.ACCES, .PERM => error.PermissionDenied,