summaryrefslogtreecommitdiff
path: root/src/fs.zig
diff options
context:
space:
mode:
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,