//! system/services/vfs — the user-space VFS server. Shipped in the initial_ramdisk, spawned as a //! ring-3 process, and reached by every other process through IPC (the `runtime` //! file API marshals open/read/write/stat/close into calls to this server's //! endpoint, published under the well-known `vfs` service id). //! //! For now the namespace is a small in-memory ramfs (opening a name creates it): //! enough to prove the whole path — client file API -> IPC -> server dispatch -> //! reply. Device nodes backed by user-space drivers (/device) layer on top in M10, //! where `open` on a /device name forwards to the owning driver's endpoint. const std = @import("std"); const runtime = @import("runtime"); const protocol = runtime.vfs_protocol; const Node = struct { used: bool = false, name: [24]u8 = undefined, name_len: usize = 0, data: [512]u8 = undefined, size: usize = 0, }; const OpenFile = struct { used: bool = false, node: usize = 0, // 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 // cleaning up after themselves (docs/process-lifecycle.md). owner: u32 = 0, }; var nodes = [_]Node{.{}} ** 8; var opens = [_]OpenFile{.{}} ** 16; fn findNode(name: []const u8) ?usize { for (&nodes, 0..) |*n, i| { if (n.used and std.mem.eql(u8, n.name[0..n.name_len], name)) return i; } return null; } fn createNode(name: []const u8) ?usize { for (&nodes, 0..) |*n, i| { if (!n.used) { const l = @min(name.len, n.name.len); @memcpy(n.name[0..l], name[0..l]); n.* = .{ .used = true, .name = n.name, .name_len = l, .size = 0 }; return i; } } return null; } fn openAt(id: u64) ?*OpenFile { if (id >= opens.len) return null; const o = &opens[@intCast(id)]; return if (o.used) o else null; } /// Serialise a reply header + payload into `out`; returns the total length. fn writeReply(out: []u8, reply: protocol.Reply, payload: []const u8) usize { @memcpy(out[0..protocol.reply_size], std.mem.asBytes(&reply)); const n = @min(payload.len, out.len - protocol.reply_size); @memcpy(out[protocol.reply_size..][0..n], payload[0..n]); return protocol.reply_size + n; } fn fail(out: []u8) usize { return writeReply(out, .{ .status = -1 }, &.{}); } /// Format one whole log line and emit it in a single `debug_write`, so lines from /// concurrent processes can never land in the middle of it. fn writeLine(comptime fmt: []const u8, arguments: anytype) void { var line: [96]u8 = undefined; _ = runtime.system.write(std.fmt.bufPrint(&line, fmt, arguments) catch return); } /// Release every open handle `client` held — called on that client's published /// exit event. The nodes (the files) stay: ramfs contents outlive their writers, /// only the dead client's handles go. fn releaseClientHandles(client: u32) void { var released: u32 = 0; for (&opens) |*o| { if (o.used and o.owner == client) { o.used = false; released += 1; } } if (released != 0) writeLine("system/services/vfs: released {d} handle(s) for dead client {d}\n", .{ released, client }); } /// 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 { _ = capability; if (message.len < protocol.request_size) return fail(out); const request = std.mem.bytesToValue(protocol.Request, message[0..protocol.request_size]); const payload = message[protocol.request_size..]; switch (request.operation) { .open => { const name = payload[0..@min(payload.len, request.len)]; const ni = findNode(name) orelse createNode(name) orelse return fail(out); for (&opens, 0..) |*o, i| { if (!o.used) { o.* = .{ .used = true, .node = ni, .owner = sender }; return writeReply(out, .{ .status = 0, .node = i }, &.{}); } } return fail(out); }, .read => { const of = openAt(request.node) orelse return fail(out); const nd = &nodes[of.node]; const off: usize = @intCast(request.offset); if (off >= nd.size) return writeReply(out, .{ .status = 0, .len = 0 }, &.{}); // EOF const n = @min(@min(nd.size - off, request.len), protocol.maximum_payload); return writeReply(out, .{ .status = 0, .len = @intCast(n) }, nd.data[off .. off + n]); }, .write => { const of = openAt(request.node) orelse return fail(out); const nd = &nodes[of.node]; const off: usize = @intCast(request.offset); if (off > nd.data.len) return fail(out); const n = @min(@min(payload.len, request.len), nd.data.len - off); @memcpy(nd.data[off .. off + n], payload[0..n]); if (off + n > nd.size) nd.size = off + n; return writeReply(out, .{ .status = 0, .len = @intCast(n) }, &.{}); }, .status => { const of = openAt(request.node) orelse return fail(out); const st = protocol.FileStatus{ .size = nodes[of.node].size, .kind = 0 }; return writeReply(out, .{ .status = 0, .len = @sizeOf(protocol.FileStatus) }, std.mem.asBytes(&st)); }, .close => { if (request.node < opens.len) opens[@intCast(request.node)].used = false; return writeReply(out, .{ .status = 0 }, &.{}); }, } } /// 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 /// to release them (docs/process-lifecycle.md). fn initialise(endpoint: runtime.ipc.Handle) bool { if (!runtime.process.subscribeExits(endpoint)) { _ = runtime.system.write("system/services/vfs: exit subscription failed\n"); } _ = runtime.system.write("system/services/vfs: ready\n"); return true; } /// A non-signal notification: the only kind the VFS subscribes to is exit events. fn onNotification(badge: u64) void { if (badge & runtime.ipc.notify_exit_bit != 0) { releaseClientHandles(@intCast(badge & ~(runtime.ipc.notify_badge_bit | runtime.ipc.notify_exit_bit))); } } pub fn main() void { // The harness owns the loop: requests dispatch to handle(), exit events to // onNotification(), ping and terminate are answered for free — this service // gained the whole lifecycle contract by deleting its hand-rolled loop. runtime.service.run(protocol.message_maximum, .{ .service = .vfs, .init = initialise, .on_message = handle, .on_notification = onNotification, }); } pub const panic = runtime.panic; comptime { _ = &runtime.start._start; }