M5: VFS mount support — mount table + forwarding router
Turn the flat-ramfs VFS into a router: a mount table maps an absolute path prefix (e.g. /mnt/usb) to a backend server's endpoint, and open/read/write/status/ readdir/close on a path under a mount are forwarded to that backend, which speaks the same vfs-protocol. This is what a FAT filesystem mounts into. - protocol: append readdir / mount / unmount operations, a NodeKind enum (the FSH file types) that now fills FileStatus.kind, a DirectoryEntry record, and a directory open flag. Appended values keep existing clients and tests unchanged. - vfs.zig: a mount table, longest-prefix routing, forwarding of every op on a backend handle, mount/unmount handlers (the backend arrives as the call's capability), and release-on-death that also closes the backend's handles. - path.zig: pure, host-tested mount-prefix matching that never captures a non-boundary like /mnt/usbextra. - unistd: mount(), opendir / readdir / closedir clients. Bare names still resolve in the flat ramfs — the backward-compat contract; the vfs and vfs-client-death tests pass unchanged. End-to-end mount+read is exercised by the FAT server (M6). Host units cover path matching and protocol sizes.
This commit is contained in:
@@ -638,6 +638,8 @@ pub fn build(b: *std.Build) void {
|
|||||||
"system/drivers/usb-hid/hid-report.zig", // HID boot-report keyboard/mouse decode
|
"system/drivers/usb-hid/hid-report.zig", // HID boot-report keyboard/mouse decode
|
||||||
"system/drivers/usb-storage/bulk-only-transport.zig", // CBW/CSW wrapper sizes
|
"system/drivers/usb-storage/bulk-only-transport.zig", // CBW/CSW wrapper sizes
|
||||||
"system/drivers/usb-storage/scsi.zig", // SCSI CDB encodings (big-endian)
|
"system/drivers/usb-storage/scsi.zig", // SCSI CDB encodings (big-endian)
|
||||||
|
"system/services/vfs/path.zig", // mount-prefix path matching
|
||||||
|
"system/services/vfs/protocol.zig", // NodeKind / DirectoryEntry sizes + op values
|
||||||
}) |root| {
|
}) |root| {
|
||||||
const mod_tests = b.addTest(.{
|
const mod_tests = b.addTest(.{
|
||||||
.root_module = b.createModule(.{
|
.root_module = b.createModule(.{
|
||||||
|
|||||||
@@ -146,3 +146,61 @@ pub fn close(fd: i32) void {
|
|||||||
_ = transact(request, &.{}, &.{});
|
_ = transact(request, &.{}, &.{});
|
||||||
f.used = false;
|
f.used = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Mount a filesystem backend (its server endpoint) at absolute path `target`;
|
||||||
|
/// the VFS then routes every path under `target` to that backend. Returns 0 or
|
||||||
|
/// -1. This is the one call that hands the VFS a capability (the backend).
|
||||||
|
pub fn mount(target: []const u8, backend: usize) i32 {
|
||||||
|
const h = vfs() orelse return -1;
|
||||||
|
const request = protocol.Request{ .operation = .mount, .node = 0, .offset = 0, .len = @intCast(target.len), .flags = 0 };
|
||||||
|
var message: [protocol.message_maximum]u8 = undefined;
|
||||||
|
@memcpy(message[0..protocol.request_size], std.mem.asBytes(&request));
|
||||||
|
const tlen = @min(target.len, protocol.maximum_payload);
|
||||||
|
@memcpy(message[protocol.request_size..][0..tlen], target[0..tlen]);
|
||||||
|
var rbuf: [protocol.message_maximum]u8 = undefined;
|
||||||
|
const result = ipc.callCap(h, message[0 .. protocol.request_size + tlen], &rbuf, backend) catch return -1;
|
||||||
|
if (result.len < protocol.reply_size) return -1;
|
||||||
|
return if (std.mem.bytesToValue(protocol.Reply, rbuf[0..protocol.reply_size]).status == 0) 0 else -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A directory entry filled by `readdir`.
|
||||||
|
pub const DirEntry = struct {
|
||||||
|
kind: u32 = 0, // a protocol.NodeKind
|
||||||
|
size: u64 = 0,
|
||||||
|
name_buffer: [64]u8 = undefined,
|
||||||
|
name_len: usize = 0,
|
||||||
|
|
||||||
|
pub fn name(self: *const DirEntry) []const u8 {
|
||||||
|
return self.name_buffer[0..self.name_len];
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/// Open a directory for reading with `readdir`. Returns an fd or -1.
|
||||||
|
pub fn opendir(path: []const u8) i32 {
|
||||||
|
return open(path, protocol.directory);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Read the next entry of a directory fd into `entry`; returns false at EOF or on
|
||||||
|
/// error. Advances the fd's cursor by one entry.
|
||||||
|
pub fn readdir(fd: i32, entry: *DirEntry) bool {
|
||||||
|
const f = fdPtr(fd) orelse return false;
|
||||||
|
const request = protocol.Request{ .operation = .readdir, .node = f.node, .offset = f.offset, .len = 0, .flags = 0 };
|
||||||
|
var buffer: [protocol.message_maximum]u8 = undefined;
|
||||||
|
const r = transact(request, &.{}, &buffer) orelse return false;
|
||||||
|
if (r.reply.status != 0 or r.reply.len == 0) return false; // error or EOF
|
||||||
|
if (r.payload.len < protocol.directory_entry_size) return false;
|
||||||
|
const header = std.mem.bytesToValue(protocol.DirectoryEntry, r.payload[0..protocol.directory_entry_size]);
|
||||||
|
entry.kind = header.kind;
|
||||||
|
entry.size = header.size;
|
||||||
|
const source = r.payload[protocol.directory_entry_size..];
|
||||||
|
const nlen = @min(@min(@as(usize, header.name_len), source.len), entry.name_buffer.len);
|
||||||
|
@memcpy(entry.name_buffer[0..nlen], source[0..nlen]);
|
||||||
|
entry.name_len = nlen;
|
||||||
|
f.offset += 1;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Close a directory fd (same as `close`).
|
||||||
|
pub fn closedir(fd: i32) void {
|
||||||
|
close(fd);
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,39 @@
|
|||||||
|
//! Pure path utilities for the VFS mount router — no IPC, no state, so they are
|
||||||
|
//! host-testable in isolation. The router uses these to decide whether an opened
|
||||||
|
//! path lies under a mount point and, if so, what it looks like relative to that
|
||||||
|
//! mount.
|
||||||
|
|
||||||
|
const std = @import("std");
|
||||||
|
|
||||||
|
/// If `path` lies under `mount_prefix` — equal to it, or the prefix followed by a
|
||||||
|
/// path separator — return the path relative to the mount ("/" for an exact
|
||||||
|
/// match, otherwise the tail beginning with '/'). Returns null when `path` is not
|
||||||
|
/// under the mount, so a prefix like "/mnt/usb" never captures "/mnt/usbextra".
|
||||||
|
pub fn underMount(path: []const u8, mount_prefix: []const u8) ?[]const u8 {
|
||||||
|
if (path.len < mount_prefix.len) return null;
|
||||||
|
if (!std.mem.eql(u8, path[0..mount_prefix.len], mount_prefix)) return null;
|
||||||
|
if (path.len == mount_prefix.len) return "/";
|
||||||
|
if (path[mount_prefix.len] != '/') return null;
|
||||||
|
return path[mount_prefix.len..];
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Whether `path` is absolute (rooted at '/'). Bare names — what the flat ramfs
|
||||||
|
/// uses — are relative and never route through a mount.
|
||||||
|
pub fn isAbsolute(path: []const u8) bool {
|
||||||
|
return path.len > 0 and path[0] == '/';
|
||||||
|
}
|
||||||
|
|
||||||
|
test "underMount matches only at path boundaries" {
|
||||||
|
try std.testing.expectEqualStrings("/", underMount("/mnt/usb", "/mnt/usb").?);
|
||||||
|
try std.testing.expectEqualStrings("/system/kernel", underMount("/mnt/usb/system/kernel", "/mnt/usb").?);
|
||||||
|
try std.testing.expect(underMount("/mnt/usbextra", "/mnt/usb") == null); // not a boundary
|
||||||
|
try std.testing.expect(underMount("/mnt", "/mnt/usb") == null); // shorter than the prefix
|
||||||
|
try std.testing.expect(underMount("/other", "/mnt/usb") == null);
|
||||||
|
try std.testing.expect(underMount("greeting", "/mnt/usb") == null); // a bare name
|
||||||
|
}
|
||||||
|
|
||||||
|
test "isAbsolute distinguishes paths from bare names" {
|
||||||
|
try std.testing.expect(isAbsolute("/mnt/usb"));
|
||||||
|
try std.testing.expect(!isAbsolute("greeting"));
|
||||||
|
try std.testing.expect(!isAbsolute(""));
|
||||||
|
}
|
||||||
@@ -17,8 +17,36 @@ pub const Operation = enum(u32) {
|
|||||||
read, // read(node, offset, len) -> bytes
|
read, // read(node, offset, len) -> bytes
|
||||||
write, // write(node, offset, bytes) -> count
|
write, // write(node, offset, bytes) -> count
|
||||||
status, // status(node) -> FileStatus
|
status, // status(node) -> FileStatus
|
||||||
|
// Appended for the mount router (M5). Values stay stable, so existing clients
|
||||||
|
// and the flat-ramfs tests are unaffected.
|
||||||
|
readdir, // readdir(dir_node, cursor=offset) -> one DirectoryEntry (len==0 => EOF)
|
||||||
|
mount, // mount(prefix payload, capability = backend endpoint)
|
||||||
|
unmount, // unmount(prefix payload)
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/// The type of a filesystem node, aligned to the FSH file-type table
|
||||||
|
/// (docs/danos-file-system-hierarchy-FSH.md). Fills `FileStatus.kind` and
|
||||||
|
/// `DirectoryEntry.kind`; `regular = 0` keeps the historical hardcoded value.
|
||||||
|
pub const NodeKind = enum(u32) {
|
||||||
|
regular = 0,
|
||||||
|
directory = 1,
|
||||||
|
character_device = 2,
|
||||||
|
block_device = 3,
|
||||||
|
symbolic_link = 4,
|
||||||
|
fifo = 5,
|
||||||
|
socket = 6,
|
||||||
|
};
|
||||||
|
|
||||||
|
/// One directory entry, returned by `readdir`: a fixed header followed inline in
|
||||||
|
/// the reply payload by `name_len` bytes of name. A zero-length reply is EOF.
|
||||||
|
pub const DirectoryEntry = extern struct {
|
||||||
|
kind: u32, // a NodeKind
|
||||||
|
name_len: u32,
|
||||||
|
size: u64,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub const directory_entry_size: usize = @sizeOf(DirectoryEntry);
|
||||||
|
|
||||||
/// Request header. `node` is the server-side open-file id (from a prior open);
|
/// Request header. `node` is the server-side open-file id (from a prior open);
|
||||||
/// for `open` the path is the payload and `len` is its length. `offset`/`len`
|
/// for `open` the path is the payload and `len` is its length. `offset`/`len`
|
||||||
/// carry the read/write position and count.
|
/// carry the read/write position and count.
|
||||||
@@ -57,3 +85,17 @@ pub const maximum_payload: usize = message_maximum - request_size;
|
|||||||
|
|
||||||
/// Open flags (danos-native; the POSIX layer maps `O_CREAT` onto `create`).
|
/// Open flags (danos-native; the POSIX layer maps `O_CREAT` onto `create`).
|
||||||
pub const create: u32 = 1;
|
pub const create: u32 = 1;
|
||||||
|
/// Open a directory (for readdir) rather than a file. A mounted backend uses
|
||||||
|
/// this to open a directory node; the flat ramfs ignores it.
|
||||||
|
pub const directory: u32 = 2;
|
||||||
|
|
||||||
|
test "protocol struct sizes and node kinds" {
|
||||||
|
const std = @import("std");
|
||||||
|
try std.testing.expectEqual(@as(u32, 0), @intFromEnum(NodeKind.regular));
|
||||||
|
try std.testing.expectEqual(@as(u32, 1), @intFromEnum(NodeKind.directory));
|
||||||
|
try std.testing.expectEqual(@as(usize, 16), @sizeOf(DirectoryEntry));
|
||||||
|
// The appended operations keep the original values.
|
||||||
|
try std.testing.expectEqual(@as(u32, 0), @intFromEnum(Operation.open));
|
||||||
|
try std.testing.expectEqual(@as(u32, 4), @intFromEnum(Operation.status));
|
||||||
|
try std.testing.expectEqual(@as(u32, 5), @intFromEnum(Operation.readdir));
|
||||||
|
}
|
||||||
|
|||||||
+181
-16
@@ -3,14 +3,24 @@
|
|||||||
//! file API marshals open/read/write/stat/close into calls to this server's
|
//! file API marshals open/read/write/stat/close into calls to this server's
|
||||||
//! endpoint, published under the well-known `vfs` service id).
|
//! endpoint, published under the well-known `vfs` service id).
|
||||||
//!
|
//!
|
||||||
//! For now the namespace is a small in-memory ramfs (opening a name creates it):
|
//! Two namespaces meet here (M5):
|
||||||
//! enough to prove the whole path — client file API -> IPC -> server dispatch ->
|
//! - a small in-memory **ramfs** — opening a bare name creates it — enough to
|
||||||
//! reply. Device nodes backed by user-space drivers (/device) layer on top in M10,
|
//! prove the round trip and to back the existing tests;
|
||||||
//! where `open` on a /device name forwards to the owning driver's endpoint.
|
//! - **mounted filesystems**: a mount table maps an absolute path prefix (e.g.
|
||||||
|
//! `/mnt/usb`) to a backend server's endpoint. An open of a path under a mount
|
||||||
|
//! is *forwarded* to that backend (which speaks this same protocol), and every
|
||||||
|
//! later read/write/status/readdir/close on the resulting handle is relayed to
|
||||||
|
//! it. The VFS is the router; a filesystem (FAT) is the backend.
|
||||||
|
//!
|
||||||
|
//! A path routes through a mount only when it is absolute and lies under a mount
|
||||||
|
//! prefix; bare names always resolve in the flat ramfs — the backward-compat
|
||||||
|
//! contract the `vfs` / `vfs-client-death` tests rely on.
|
||||||
|
|
||||||
const std = @import("std");
|
const std = @import("std");
|
||||||
const runtime = @import("runtime");
|
const runtime = @import("runtime");
|
||||||
const protocol = runtime.vfs_protocol;
|
const protocol = runtime.vfs_protocol;
|
||||||
|
const path = @import("path.zig");
|
||||||
|
const ipc = runtime.ipc;
|
||||||
|
|
||||||
const Node = struct {
|
const Node = struct {
|
||||||
used: bool = false,
|
used: bool = false,
|
||||||
@@ -22,15 +32,29 @@ const Node = struct {
|
|||||||
|
|
||||||
const OpenFile = struct {
|
const OpenFile = struct {
|
||||||
used: bool = false,
|
used: bool = false,
|
||||||
|
// For a local handle: an index into `nodes`. For a forwarding handle: the
|
||||||
|
// node id the backend returned. (usize == u64 here, so it holds either.)
|
||||||
node: usize = 0,
|
node: usize = 0,
|
||||||
|
// Non-null for a handle that forwards to a mounted backend.
|
||||||
|
backend: ?ipc.Handle = null,
|
||||||
// The client (task id — an IPC badge is one) that opened this handle. What
|
// The client (task id — an IPC badge is one) that opened this handle. What
|
||||||
// release-on-death sweeps by: a service must never depend on its clients
|
// release-on-death sweeps by: a service must never depend on its clients
|
||||||
// cleaning up after themselves (docs/process-lifecycle.md).
|
// cleaning up after themselves (docs/process-lifecycle.md).
|
||||||
owner: u32 = 0,
|
owner: u32 = 0,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// One mounted filesystem: an absolute path prefix and the backend endpoint that
|
||||||
|
// serves everything under it.
|
||||||
|
const Mount = struct {
|
||||||
|
used: bool = false,
|
||||||
|
prefix: [64]u8 = undefined,
|
||||||
|
prefix_len: usize = 0,
|
||||||
|
backend: ipc.Handle = 0,
|
||||||
|
};
|
||||||
|
|
||||||
var nodes = [_]Node{.{}} ** 8;
|
var nodes = [_]Node{.{}} ** 8;
|
||||||
var opens = [_]OpenFile{.{}} ** 16;
|
var opens = [_]OpenFile{.{}} ** 16;
|
||||||
|
var mounts = [_]Mount{.{}} ** 8;
|
||||||
|
|
||||||
fn findNode(name: []const u8) ?usize {
|
fn findNode(name: []const u8) ?usize {
|
||||||
for (&nodes, 0..) |*n, i| {
|
for (&nodes, 0..) |*n, i| {
|
||||||
@@ -57,6 +81,26 @@ fn openAt(id: u64) ?*OpenFile {
|
|||||||
return if (o.used) o else null;
|
return if (o.used) o else null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// The mount whose prefix most specifically contains `name`, and the path
|
||||||
|
/// relative to it. Only absolute paths route; bare names never match.
|
||||||
|
const MountMatch = struct { backend: ipc.Handle, relative: []const u8 };
|
||||||
|
fn longestMount(name: []const u8) ?MountMatch {
|
||||||
|
if (!path.isAbsolute(name)) return null;
|
||||||
|
var best: ?MountMatch = null;
|
||||||
|
var best_len: usize = 0;
|
||||||
|
for (&mounts) |*m| {
|
||||||
|
if (!m.used) continue;
|
||||||
|
const prefix = m.prefix[0..m.prefix_len];
|
||||||
|
if (path.underMount(name, prefix)) |relative| {
|
||||||
|
if (best == null or prefix.len >= best_len) {
|
||||||
|
best_len = prefix.len;
|
||||||
|
best = .{ .backend = m.backend, .relative = relative };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return best;
|
||||||
|
}
|
||||||
|
|
||||||
/// Serialise a reply header + payload into `out`; returns the total length.
|
/// Serialise a reply header + payload into `out`; returns the total length.
|
||||||
fn writeReply(out: []u8, reply: protocol.Reply, payload: []const u8) usize {
|
fn writeReply(out: []u8, reply: protocol.Reply, payload: []const u8) usize {
|
||||||
@memcpy(out[0..protocol.reply_size], std.mem.asBytes(&reply));
|
@memcpy(out[0..protocol.reply_size], std.mem.asBytes(&reply));
|
||||||
@@ -76,13 +120,96 @@ fn writeLine(comptime fmt: []const u8, arguments: anytype) void {
|
|||||||
_ = runtime.system.write(std.fmt.bufPrint(&line, fmt, arguments) catch return);
|
_ = runtime.system.write(std.fmt.bufPrint(&line, fmt, arguments) catch return);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// --- mount routing ----------------------------------------------------------
|
||||||
|
|
||||||
|
/// Forward an open under a mount to its backend and, on success, allocate a local
|
||||||
|
/// forwarding handle that remembers the backend's node id.
|
||||||
|
fn forwardOpen(out: []u8, backend: ipc.Handle, relative: []const u8, flags: u32, sender: u32) usize {
|
||||||
|
const request = protocol.Request{ .operation = .open, .node = 0, .offset = 0, .len = @intCast(relative.len), .flags = flags };
|
||||||
|
var message: [protocol.message_maximum]u8 = undefined;
|
||||||
|
@memcpy(message[0..protocol.request_size], std.mem.asBytes(&request));
|
||||||
|
const rel = relative[0..@min(relative.len, protocol.maximum_payload)];
|
||||||
|
@memcpy(message[protocol.request_size..][0..rel.len], rel);
|
||||||
|
|
||||||
|
var reply: [protocol.message_maximum]u8 = undefined;
|
||||||
|
const n = ipc.call(backend, message[0 .. protocol.request_size + rel.len], &reply) catch return fail(out);
|
||||||
|
if (n < protocol.reply_size) return fail(out);
|
||||||
|
const backend_reply = std.mem.bytesToValue(protocol.Reply, reply[0..protocol.reply_size]);
|
||||||
|
if (backend_reply.status != 0) return writeReply(out, .{ .status = backend_reply.status }, &.{});
|
||||||
|
|
||||||
|
for (&opens, 0..) |*o, i| {
|
||||||
|
if (!o.used) {
|
||||||
|
o.* = .{ .used = true, .node = @intCast(backend_reply.node), .backend = backend, .owner = sender };
|
||||||
|
return writeReply(out, .{ .status = 0, .node = i }, &.{});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return fail(out);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Relay a read/write/status/readdir/close on a forwarding handle to the backend
|
||||||
|
/// (the node already rewritten to the backend's id) and copy its reply out.
|
||||||
|
fn forwardRequest(out: []u8, backend: ipc.Handle, request: protocol.Request, payload: []const u8) usize {
|
||||||
|
var message: [protocol.message_maximum]u8 = undefined;
|
||||||
|
@memcpy(message[0..protocol.request_size], std.mem.asBytes(&request));
|
||||||
|
const plen = @min(payload.len, protocol.maximum_payload);
|
||||||
|
@memcpy(message[protocol.request_size..][0..plen], payload[0..plen]);
|
||||||
|
|
||||||
|
var reply: [protocol.message_maximum]u8 = undefined;
|
||||||
|
const n = ipc.call(backend, message[0 .. protocol.request_size + plen], &reply) catch return fail(out);
|
||||||
|
const copy = @min(n, out.len);
|
||||||
|
@memcpy(out[0..copy], reply[0..copy]);
|
||||||
|
return copy;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Best-effort close of a backend node (used when a dead client's forwarding
|
||||||
|
/// handles are swept — the backend must not leak the vfs's opens).
|
||||||
|
fn forwardClose(backend: ipc.Handle, backend_node: u64) void {
|
||||||
|
const request = protocol.Request{ .operation = .close, .node = backend_node, .offset = 0, .len = 0, .flags = 0 };
|
||||||
|
var reply: [protocol.message_maximum]u8 = undefined;
|
||||||
|
_ = ipc.call(backend, std.mem.asBytes(&request), &reply) catch {};
|
||||||
|
}
|
||||||
|
|
||||||
|
fn doMount(out: []u8, prefix: []const u8, backend: ipc.Handle) usize {
|
||||||
|
for (&mounts) |*m| {
|
||||||
|
if (m.used and std.mem.eql(u8, m.prefix[0..m.prefix_len], prefix)) {
|
||||||
|
m.backend = backend;
|
||||||
|
writeLine("/system/services/vfs: remounted {s}\n", .{prefix});
|
||||||
|
return writeReply(out, .{ .status = 0 }, &.{});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (&mounts) |*m| {
|
||||||
|
if (!m.used) {
|
||||||
|
const l = @min(prefix.len, m.prefix.len);
|
||||||
|
m.used = true;
|
||||||
|
@memcpy(m.prefix[0..l], prefix[0..l]);
|
||||||
|
m.prefix_len = l;
|
||||||
|
m.backend = backend;
|
||||||
|
writeLine("/system/services/vfs: mounted {s}\n", .{prefix[0..l]});
|
||||||
|
return writeReply(out, .{ .status = 0 }, &.{});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return fail(out);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn doUnmount(out: []u8, prefix: []const u8) usize {
|
||||||
|
for (&mounts) |*m| {
|
||||||
|
if (m.used and std.mem.eql(u8, m.prefix[0..m.prefix_len], prefix)) {
|
||||||
|
m.used = false;
|
||||||
|
writeLine("/system/services/vfs: unmounted {s}\n", .{prefix});
|
||||||
|
return writeReply(out, .{ .status = 0 }, &.{});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return fail(out);
|
||||||
|
}
|
||||||
|
|
||||||
/// Release every open handle `client` held — called on that client's published
|
/// Release every open handle `client` held — called on that client's published
|
||||||
/// exit event. The nodes (the files) stay: ramfs contents outlive their writers,
|
/// exit event. Forwarding handles also tell their backend to release; local
|
||||||
/// only the dead client's handles go.
|
/// nodes (the ramfs files) stay, since ramfs contents outlive their writers.
|
||||||
fn releaseClientHandles(client: u32) void {
|
fn releaseClientHandles(client: u32) void {
|
||||||
var released: u32 = 0;
|
var released: u32 = 0;
|
||||||
for (&opens) |*o| {
|
for (&opens) |*o| {
|
||||||
if (o.used and o.owner == client) {
|
if (o.used and o.owner == client) {
|
||||||
|
if (o.backend) |backend| forwardClose(backend, o.node);
|
||||||
o.used = false;
|
o.used = false;
|
||||||
released += 1;
|
released += 1;
|
||||||
}
|
}
|
||||||
@@ -91,19 +218,28 @@ fn releaseClientHandles(client: u32) void {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Handle one request from `sender`; write the reply into `out`, return its length.
|
/// Handle one request from `sender`; write the reply into `out`, return its length.
|
||||||
fn handle(message: []const u8, out: []u8, sender: u32, capability: ?runtime.ipc.Handle) usize {
|
fn handle(message: []const u8, out: []u8, sender: u32, capability: ?ipc.Handle) usize {
|
||||||
_ = capability;
|
|
||||||
if (message.len < protocol.request_size) return fail(out);
|
if (message.len < protocol.request_size) return fail(out);
|
||||||
const request = std.mem.bytesToValue(protocol.Request, message[0..protocol.request_size]);
|
const request = std.mem.bytesToValue(protocol.Request, message[0..protocol.request_size]);
|
||||||
const payload = message[protocol.request_size..];
|
const payload = message[protocol.request_size..];
|
||||||
|
|
||||||
switch (request.operation) {
|
switch (request.operation) {
|
||||||
|
.mount => {
|
||||||
|
const prefix = payload[0..@min(payload.len, request.len)];
|
||||||
|
const backend = capability orelse return fail(out);
|
||||||
|
return doMount(out, prefix, backend);
|
||||||
|
},
|
||||||
|
.unmount => {
|
||||||
|
const prefix = payload[0..@min(payload.len, request.len)];
|
||||||
|
return doUnmount(out, prefix);
|
||||||
|
},
|
||||||
.open => {
|
.open => {
|
||||||
const name = payload[0..@min(payload.len, request.len)];
|
const name = payload[0..@min(payload.len, request.len)];
|
||||||
|
if (longestMount(name)) |m| return forwardOpen(out, m.backend, m.relative, request.flags, sender);
|
||||||
const ni = findNode(name) orelse createNode(name) orelse return fail(out);
|
const ni = findNode(name) orelse createNode(name) orelse return fail(out);
|
||||||
for (&opens, 0..) |*o, i| {
|
for (&opens, 0..) |*o, i| {
|
||||||
if (!o.used) {
|
if (!o.used) {
|
||||||
o.* = .{ .used = true, .node = ni, .owner = sender };
|
o.* = .{ .used = true, .node = ni, .backend = null, .owner = sender };
|
||||||
return writeReply(out, .{ .status = 0, .node = i }, &.{});
|
return writeReply(out, .{ .status = 0, .node = i }, &.{});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -111,7 +247,12 @@ fn handle(message: []const u8, out: []u8, sender: u32, capability: ?runtime.ipc.
|
|||||||
},
|
},
|
||||||
.read => {
|
.read => {
|
||||||
const of = openAt(request.node) orelse return fail(out);
|
const of = openAt(request.node) orelse return fail(out);
|
||||||
const nd = &nodes[of.node];
|
if (of.backend) |backend| {
|
||||||
|
var forwarded = request;
|
||||||
|
forwarded.node = of.node;
|
||||||
|
return forwardRequest(out, backend, forwarded, payload);
|
||||||
|
}
|
||||||
|
const nd = &nodes[@intCast(of.node)];
|
||||||
const off: usize = @intCast(request.offset);
|
const off: usize = @intCast(request.offset);
|
||||||
if (off >= nd.size) return writeReply(out, .{ .status = 0, .len = 0 }, &.{}); // EOF
|
if (off >= nd.size) return writeReply(out, .{ .status = 0, .len = 0 }, &.{}); // EOF
|
||||||
const n = @min(@min(nd.size - off, request.len), protocol.maximum_payload);
|
const n = @min(@min(nd.size - off, request.len), protocol.maximum_payload);
|
||||||
@@ -119,7 +260,12 @@ fn handle(message: []const u8, out: []u8, sender: u32, capability: ?runtime.ipc.
|
|||||||
},
|
},
|
||||||
.write => {
|
.write => {
|
||||||
const of = openAt(request.node) orelse return fail(out);
|
const of = openAt(request.node) orelse return fail(out);
|
||||||
const nd = &nodes[of.node];
|
if (of.backend) |backend| {
|
||||||
|
var forwarded = request;
|
||||||
|
forwarded.node = of.node;
|
||||||
|
return forwardRequest(out, backend, forwarded, payload);
|
||||||
|
}
|
||||||
|
const nd = &nodes[@intCast(of.node)];
|
||||||
const off: usize = @intCast(request.offset);
|
const off: usize = @intCast(request.offset);
|
||||||
if (off > nd.data.len) return fail(out);
|
if (off > nd.data.len) return fail(out);
|
||||||
const n = @min(@min(payload.len, request.len), nd.data.len - off);
|
const n = @min(@min(payload.len, request.len), nd.data.len - off);
|
||||||
@@ -129,11 +275,30 @@ fn handle(message: []const u8, out: []u8, sender: u32, capability: ?runtime.ipc.
|
|||||||
},
|
},
|
||||||
.status => {
|
.status => {
|
||||||
const of = openAt(request.node) orelse return fail(out);
|
const of = openAt(request.node) orelse return fail(out);
|
||||||
const st = protocol.FileStatus{ .size = nodes[of.node].size, .kind = 0 };
|
if (of.backend) |backend| {
|
||||||
|
var forwarded = request;
|
||||||
|
forwarded.node = of.node;
|
||||||
|
return forwardRequest(out, backend, forwarded, payload);
|
||||||
|
}
|
||||||
|
const st = protocol.FileStatus{ .size = nodes[@intCast(of.node)].size, .kind = @intFromEnum(protocol.NodeKind.regular) };
|
||||||
return writeReply(out, .{ .status = 0, .len = @sizeOf(protocol.FileStatus) }, std.mem.asBytes(&st));
|
return writeReply(out, .{ .status = 0, .len = @sizeOf(protocol.FileStatus) }, std.mem.asBytes(&st));
|
||||||
},
|
},
|
||||||
|
.readdir => {
|
||||||
|
const of = openAt(request.node) orelse return fail(out);
|
||||||
|
if (of.backend) |backend| {
|
||||||
|
var forwarded = request;
|
||||||
|
forwarded.node = of.node;
|
||||||
|
return forwardRequest(out, backend, forwarded, payload);
|
||||||
|
}
|
||||||
|
// The flat ramfs has no directories: report EOF.
|
||||||
|
return writeReply(out, .{ .status = 0, .len = 0 }, &.{});
|
||||||
|
},
|
||||||
.close => {
|
.close => {
|
||||||
if (request.node < opens.len) opens[@intCast(request.node)].used = false;
|
const of = openAt(request.node);
|
||||||
|
if (of) |o| {
|
||||||
|
if (o.backend) |backend| forwardClose(backend, o.node);
|
||||||
|
o.used = false;
|
||||||
|
}
|
||||||
return writeReply(out, .{ .status = 0 }, &.{});
|
return writeReply(out, .{ .status = 0 }, &.{});
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
@@ -142,7 +307,7 @@ fn handle(message: []const u8, out: []u8, sender: u32, capability: ?runtime.ipc.
|
|||||||
/// Startup, under the harness: subscribe to the published exit events — when a
|
/// Startup, under the harness: subscribe to the published exit events — when a
|
||||||
/// client dies holding open handles, the exit notification is how the VFS learns
|
/// client dies holding open handles, the exit notification is how the VFS learns
|
||||||
/// to release them (docs/process-lifecycle.md).
|
/// to release them (docs/process-lifecycle.md).
|
||||||
fn initialise(endpoint: runtime.ipc.Handle) bool {
|
fn initialise(endpoint: ipc.Handle) bool {
|
||||||
if (!runtime.process.subscribeExits(endpoint)) {
|
if (!runtime.process.subscribeExits(endpoint)) {
|
||||||
_ = runtime.system.write("/system/services/vfs: exit subscription failed\n");
|
_ = runtime.system.write("/system/services/vfs: exit subscription failed\n");
|
||||||
}
|
}
|
||||||
@@ -152,8 +317,8 @@ fn initialise(endpoint: runtime.ipc.Handle) bool {
|
|||||||
|
|
||||||
/// A non-signal notification: the only kind the VFS subscribes to is exit events.
|
/// A non-signal notification: the only kind the VFS subscribes to is exit events.
|
||||||
fn onNotification(badge: u64) void {
|
fn onNotification(badge: u64) void {
|
||||||
if (badge & runtime.ipc.notify_exit_bit != 0) {
|
if (badge & ipc.notify_exit_bit != 0) {
|
||||||
releaseClientHandles(@intCast(badge & ~(runtime.ipc.notify_badge_bit | runtime.ipc.notify_exit_bit)));
|
releaseClientHandles(@intCast(badge & ~(ipc.notify_badge_bit | ipc.notify_exit_bit)));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user