diff options
Diffstat (limited to 'src/fs.zig')
| -rw-r--r-- | src/fs.zig | 444 |
1 files changed, 147 insertions, 297 deletions
@@ -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, |
