init writes /mnt/usb/DANOS.LOG at shutdown and then enters S5 — but the write sat in the USB flash controller's write cache and was lost when power was cut, because nothing issued SCSI SYNCHRONIZE CACHE. Invisible in QEMU (its backing file commits immediately); real on hardware. The boot-time flush survived only because the machine kept running afterward and the cache drained on its own. - block protocol gains a flush op; usb-storage serves it with SYNCHRONIZE CACHE (10); runtime.block gains Device.flush() - the FAT server tracks whether blocks were written and, on a file close, commits the device cache (durable-on-close — the right default for removable media, and it makes init's existing shutdown close() persist the log before S5, no init/VFS change needed) - verified the SYNCHRONIZE CACHE actually reaches the driver on each dirty close; usb-storage/fat-mount/mutations/rename/mtime/log-flush all green
250 lines
12 KiB
Zig
250 lines
12 KiB
Zig
//! system/services/fat — the FAT filesystem server. Spawned as a boot service, it
|
|
//! opens the block device (a USB stick via usb-storage) under `.block`, mounts the
|
|
//! FAT filesystem on it (the pure engine in engine.zig), and mounts itself into
|
|
//! the VFS at /mnt/usb. From then on the VFS forwards every open/read/write/
|
|
//! status/readdir/close under /mnt/usb to this server, which serves the same
|
|
//! vfs-protocol as a backend — turning block reads into file reads.
|
|
//!
|
|
//! The block data path never crosses IPC: a DMA bounce buffer is handed to the
|
|
//! block driver by physical address, and the engine copies sectors in and out of
|
|
//! it.
|
|
|
|
const std = @import("std");
|
|
const runtime = @import("runtime");
|
|
const engine = @import("engine.zig");
|
|
const on_disk = @import("on-disk.zig");
|
|
const protocol = runtime.vfs_protocol;
|
|
const dma = runtime.dma;
|
|
|
|
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);
|
|
}
|
|
|
|
const mount_point = "/mnt/usb";
|
|
|
|
// The engine's BlockDevice, backed by the `.block` driver plus a DMA bounce
|
|
// buffer the driver reads/writes by physical address.
|
|
const IpcBlock = struct {
|
|
device: runtime.block.Device,
|
|
bounce: dma.Region,
|
|
|
|
fn readBlock(context: *anyopaque, lba: u64, buffer: []u8) bool {
|
|
const self: *IpcBlock = @ptrCast(@alignCast(context));
|
|
if (!self.device.read(lba, 1, self.bounce.physical)) return false;
|
|
const source: [*]const u8 = @ptrFromInt(self.bounce.virtual);
|
|
@memcpy(buffer[0..512], source[0..512]);
|
|
return true;
|
|
}
|
|
fn writeBlock(context: *anyopaque, lba: u64, buffer: []const u8) bool {
|
|
const self: *IpcBlock = @ptrCast(@alignCast(context));
|
|
const destination: [*]u8 = @ptrFromInt(self.bounce.virtual);
|
|
@memcpy(destination[0..512], buffer[0..512]);
|
|
if (!self.device.write(lba, 1, self.bounce.physical)) return false;
|
|
device_dirty = true; // a block reached the device; a close will flush it
|
|
return true;
|
|
}
|
|
};
|
|
|
|
var ipc_block: IpcBlock = undefined;
|
|
// Set whenever a block is written, cleared when the device cache is flushed on a
|
|
// file close — so writes are committed to stable media before a power-off.
|
|
var device_dirty: bool = false;
|
|
var filesystem: engine.FileSystem = undefined;
|
|
|
|
// Open handles the VFS holds against this backend: each maps a node id to a
|
|
// resolved engine node.
|
|
const OpenNode = struct { used: bool = false, node: engine.Node = undefined, owner: u32 = 0 };
|
|
var open_nodes = [_]OpenNode{.{}} ** 32;
|
|
|
|
fn allocOpen() ?usize {
|
|
for (&open_nodes, 0..) |*o, i| {
|
|
if (!o.used) return i;
|
|
}
|
|
return null;
|
|
}
|
|
|
|
fn openAt(id: u64) ?*OpenNode {
|
|
if (id >= open_nodes.len) return null;
|
|
const o = &open_nodes[@intCast(id)];
|
|
return if (o.used) o else null;
|
|
}
|
|
|
|
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 }, &.{});
|
|
}
|
|
|
|
fn initialise(endpoint: runtime.ipc.Handle) bool {
|
|
_ = runtime.system.write("/system/services/fat: starting, waiting for a block device\n");
|
|
const device = runtime.block.open() orelse {
|
|
_ = runtime.system.write("/system/services/fat: no block device (no storage attached)\n");
|
|
return false; // clean exit: nothing to serve
|
|
};
|
|
const geometry = device.geometry() orelse {
|
|
_ = runtime.system.write("/system/services/fat: block geometry unavailable\n");
|
|
return false;
|
|
};
|
|
ipc_block = .{ .device = device, .bounce = dma.alloc(4096, dma.coherent) orelse return false };
|
|
|
|
const block_device = engine.BlockDevice{
|
|
.context = &ipc_block,
|
|
.block_size = geometry.block_size,
|
|
.block_count = geometry.block_count,
|
|
.readBlockFn = IpcBlock.readBlock,
|
|
.writeBlockFn = IpcBlock.writeBlock,
|
|
};
|
|
filesystem = engine.FileSystem.mount(block_device) orelse {
|
|
_ = runtime.system.write("/system/services/fat: not a FAT filesystem\n");
|
|
return false;
|
|
};
|
|
writeLine("/system/services/fat: mounted FAT ({s}, {d} clusters, partition lba {d})\n", .{ @tagName(filesystem.geometry.fat_type), filesystem.geometry.cluster_count, filesystem.base_lba });
|
|
|
|
// Mount ourselves into the VFS namespace at /mnt/usb (retry while the VFS
|
|
// comes up). From here the VFS routes /mnt/usb/... to this server.
|
|
var tries: u32 = 0;
|
|
while (tries < 100) : (tries += 1) {
|
|
if (runtime.fs.mount(mount_point, endpoint)) {
|
|
writeLine("/system/services/fat: mounted {s}\n", .{mount_point});
|
|
return true;
|
|
}
|
|
runtime.system.sleep(50);
|
|
}
|
|
_ = runtime.system.write("/system/services/fat: could not mount into the VFS\n");
|
|
return true; // still serve directly, even if the namespace mount didn't take
|
|
}
|
|
|
|
const ParentLeaf = struct { parent: []const u8, leaf: []const u8 };
|
|
|
|
// Split a path into its parent directory and final component: "/a/b" -> ("/a",
|
|
// "b"); "/b" -> ("/", "b"); "b" -> ("/", "b").
|
|
fn splitParent(path: []const u8) ParentLeaf {
|
|
const slash = std.mem.lastIndexOfScalar(u8, path, '/');
|
|
return .{
|
|
.parent = if (slash) |s| (if (s == 0) "/" else path[0..s]) else "/",
|
|
.leaf = if (slash) |s| path[s + 1 ..] else path,
|
|
};
|
|
}
|
|
|
|
fn handleOpen(out: []u8, path: []const u8, flags: u32) usize {
|
|
var node = filesystem.resolve(path);
|
|
if (node == null and flags & protocol.create != 0) {
|
|
const split = splitParent(path);
|
|
const parent = filesystem.resolve(split.parent) orelse return fail(out);
|
|
node = filesystem.createFile(parent, split.leaf);
|
|
}
|
|
var resolved = node orelse return fail(out);
|
|
// O_TRUNC: replace an existing file's contents rather than overwriting in place
|
|
// (frees the old chain, so a shorter rewrite leaves no stale tail).
|
|
if (flags & protocol.truncate != 0 and !resolved.is_directory) {
|
|
filesystem.truncate(&resolved);
|
|
}
|
|
const index = allocOpen() orelse return fail(out);
|
|
open_nodes[index] = .{ .used = true, .node = resolved };
|
|
return writeReply(out, .{ .status = 0, .node = index }, &.{});
|
|
}
|
|
|
|
fn onMessage(message: []const u8, out: []u8, sender: u32, capability: ?runtime.ipc.Handle) usize {
|
|
_ = capability;
|
|
_ = sender;
|
|
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..];
|
|
|
|
// Stamp create/write with the current wall-clock time (mtime). Cheap, and it
|
|
// keeps the engine pure (it takes the time as data, not a syscall).
|
|
filesystem.current_time_epoch = runtime.system.wallClock();
|
|
|
|
switch (request.operation) {
|
|
.open => return handleOpen(out, payload[0..@min(payload.len, request.len)], request.flags),
|
|
.read => {
|
|
const o = openAt(request.node) orelse return fail(out);
|
|
var buffer: [protocol.maximum_payload]u8 = undefined;
|
|
const want = @min(@as(usize, request.len), buffer.len);
|
|
const n = filesystem.readFile(o.node, @intCast(request.offset), buffer[0..want]);
|
|
return writeReply(out, .{ .status = 0, .len = @intCast(n) }, buffer[0..n]);
|
|
},
|
|
.write => {
|
|
const o = openAt(request.node) orelse return fail(out);
|
|
const data = payload[0..@min(payload.len, request.len)];
|
|
const n = filesystem.writeFile(&o.node, @intCast(request.offset), data);
|
|
return writeReply(out, .{ .status = 0, .len = @intCast(n) }, &.{});
|
|
},
|
|
.status => {
|
|
const o = openAt(request.node) orelse return fail(out);
|
|
const kind: protocol.NodeKind = if (o.node.is_directory) .directory else .regular;
|
|
const status = protocol.FileStatus{ .size = o.node.size, .kind = @intFromEnum(kind), .mtime = o.node.mtime };
|
|
return writeReply(out, .{ .status = 0, .len = @sizeOf(protocol.FileStatus) }, std.mem.asBytes(&status));
|
|
},
|
|
.readdir => {
|
|
const o = openAt(request.node) orelse return fail(out);
|
|
if (!o.node.is_directory) return writeReply(out, .{ .status = 0, .len = 0 }, &.{});
|
|
const listing = filesystem.listEntry(o.node, @intCast(request.offset)) orelse return writeReply(out, .{ .status = 0, .len = 0 }, &.{});
|
|
const kind: protocol.NodeKind = if (listing.is_directory) .directory else .regular;
|
|
const header = protocol.DirectoryEntry{ .kind = @intFromEnum(kind), .name_len = @intCast(listing.name_len), .size = listing.size };
|
|
var buffer: [protocol.maximum_payload]u8 = undefined;
|
|
@memcpy(buffer[0..protocol.directory_entry_size], std.mem.asBytes(&header));
|
|
const nlen = @min(listing.name_len, buffer.len - protocol.directory_entry_size);
|
|
@memcpy(buffer[protocol.directory_entry_size..][0..nlen], listing.name_buffer[0..nlen]);
|
|
const total = protocol.directory_entry_size + nlen;
|
|
return writeReply(out, .{ .status = 0, .len = @intCast(total) }, buffer[0..total]);
|
|
},
|
|
.close => {
|
|
if (openAt(request.node)) |o| o.used = false;
|
|
// Durable-on-close: if any block reached the device since the last
|
|
// flush, commit its cache to stable media now (best-effort). This is
|
|
// what makes init's shutdown log flush survive a real power-off, and is
|
|
// the right default for removable media the user may unplug.
|
|
if (device_dirty) {
|
|
_ = ipc_block.device.flush();
|
|
device_dirty = false;
|
|
}
|
|
return writeReply(out, .{ .status = 0 }, &.{});
|
|
},
|
|
.mkdir => {
|
|
const split = splitParent(payload[0..@min(payload.len, request.len)]);
|
|
const parent = filesystem.resolve(split.parent) orelse return fail(out);
|
|
if (filesystem.createDirectory(parent, split.leaf) == null) return fail(out);
|
|
return writeReply(out, .{ .status = 0 }, &.{});
|
|
},
|
|
.unlink => {
|
|
const split = splitParent(payload[0..@min(payload.len, request.len)]);
|
|
const parent = filesystem.resolve(split.parent) orelse return fail(out);
|
|
if (!filesystem.removeFile(parent, split.leaf)) return fail(out);
|
|
return writeReply(out, .{ .status = 0 }, &.{});
|
|
},
|
|
.rename => {
|
|
const both = payload[0..@min(payload.len, request.len)];
|
|
const sep = std.mem.indexOfScalar(u8, both, 0) orelse return fail(out);
|
|
const old_split = splitParent(both[0..sep]);
|
|
const new_split = splitParent(both[sep + 1 ..]);
|
|
// Same-directory rename only.
|
|
if (!std.mem.eql(u8, old_split.parent, new_split.parent)) return fail(out);
|
|
const parent = filesystem.resolve(old_split.parent) orelse return fail(out);
|
|
if (!filesystem.rename(parent, old_split.leaf, new_split.leaf)) return fail(out);
|
|
return writeReply(out, .{ .status = 0 }, &.{});
|
|
},
|
|
// A backend is never itself a mount target.
|
|
.mount, .unmount => return fail(out),
|
|
}
|
|
}
|
|
|
|
pub fn main() void {
|
|
runtime.service.run(protocol.message_maximum, .{
|
|
.service = .fat,
|
|
.init = initialise,
|
|
.on_message = onMessage,
|
|
});
|
|
}
|
|
|
|
pub const panic = runtime.panic;
|
|
comptime {
|
|
_ = &runtime.start._start;
|
|
}
|