176 lines
6.8 KiB
Zig
176 lines
6.8 KiB
Zig
//! 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("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("vfs: exit subscription failed\n");
|
|
}
|
|
_ = runtime.system.write("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;
|
|
}
|