fat was one binary doing four jobs; the three that are not FAT-specific move to library/kernel/file-system-harness, a Server(comptime Engine) generic over the engine type: the badge-scoped open-node table, the nine vfs handlers, the not-mounted politeness, the exit sweep, mount registration, and durable-on- close. A filesystem is now an engine plus a main that hands the harness a mounted volume; a second engine reuses the harness wholesale. Placement note: the plan said library/file-system, but the harness is a specialization of `service` (its sibling) and needs nothing from the device domain, so it lives beside service in library/kernel and stays block-free — durability rides a caller closure (Volume.flush), no backwards kernel->device dependency, no new-domain scaffolding. The engine type is inferred from resolve()'s return, so engine.zig is untouched (its Node stays module-scope). fat keeps only its FAT-specific bring-up (acquireVolume, DMA, engine.mount, the attach/detach round trip) and the three mount prefixes as data. Behavior- neutral: 13/13 across the fat/vfs/logger/IOMMU surface, nothing observable changed. This lands first so every later phase touches the harness once.
186 lines
9.2 KiB
Zig
186 lines
9.2 KiB
Zig
//! system/services/fat — the FAT filesystem service. This is FAT's FAT-specific
|
|
//! half: it finds its block device, sets up the DMA bounce buffer, mounts the
|
|
//! FAT engine on it, and hands the mounted volume to the shared filesystem
|
|
//! harness (library/kernel/file-system-harness), which owns everything else —
|
|
//! the vfs-protocol serving, the open-node table, mount registration, the exit
|
|
//! sweep, durable-on-close. The engine (engine.zig) is the pure, host-testable
|
|
//! format code; on-disk.zig its byte layout. A second filesystem reuses the
|
|
//! harness and supplies its own engine
|
|
//! (docs/file-system-development/storage-architecture.md).
|
|
//!
|
|
//! 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.
|
|
|
|
const std = @import("std");
|
|
const channel = @import("channel");
|
|
const device_manager_protocol = @import("device-manager-protocol");
|
|
const driver = @import("driver");
|
|
const ipc = @import("ipc");
|
|
const block = @import("block");
|
|
const memory = @import("memory");
|
|
const logging = @import("logging");
|
|
const engine = @import("engine.zig");
|
|
const envelope = @import("envelope");
|
|
const harness = @import("file-system-harness");
|
|
|
|
/// The serving harness, specialized for the FAT engine. One volume per process.
|
|
const Harness = harness.Server(engine.FileSystem);
|
|
|
|
// 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: block.Device,
|
|
bounce: memory.DmaRegion, // engine.max_transfer_sectors * 512 bytes
|
|
|
|
fn readBlocks(context: *anyopaque, lba: u64, count: u32, buffer: []u8) bool {
|
|
const self: *IpcBlock = @ptrCast(@alignCast(context));
|
|
if (count == 0 or count > engine.max_transfer_sectors) return false;
|
|
const len = count * 512;
|
|
if (!self.device.read(lba, count, self.bounce.physical)) return false;
|
|
const source: [*]const u8 = @ptrFromInt(self.bounce.virtual);
|
|
@memcpy(buffer[0..len], source[0..len]);
|
|
return true;
|
|
}
|
|
fn writeBlocks(context: *anyopaque, lba: u64, count: u32, buffer: []const u8) bool {
|
|
const self: *IpcBlock = @ptrCast(@alignCast(context));
|
|
if (count == 0 or count > engine.max_transfer_sectors) return false;
|
|
const len = count * 512;
|
|
const destination: [*]u8 = @ptrFromInt(self.bounce.virtual);
|
|
@memcpy(destination[0..len], buffer[0..len]);
|
|
if (!self.device.write(lba, count, 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;
|
|
/// The one channel to the device manager, opened on first need and kept — the
|
|
/// poll retries on it, never spending a handle-table slot per attempt.
|
|
var manager_handle: ?ipc.Handle = null;
|
|
|
|
/// The prefixes this volume installs: /volumes/usb from the volume root, plus
|
|
/// the two hierarchy subtrees the boot volume carries (rewrite == prefix), so
|
|
/// hierarchy paths (the logger's /system/logs) stay decoupled from which volume
|
|
/// backs them.
|
|
const fat_mounts = [_]harness.MountSpec{
|
|
.{ .prefix = "/volumes/usb" },
|
|
.{ .prefix = "/system/configuration", .rewrite = "/system/configuration" },
|
|
.{ .prefix = "/system/logs", .rewrite = "/system/logs" },
|
|
};
|
|
|
|
/// Find the volume's provider through the device manager (establishment by
|
|
/// lineage, communication.md "Establishment: two planes" — `block` is not a
|
|
/// registry name; one storage process serves each stick): enumerate the
|
|
/// manager's tree, take the FIRST usb mass-storage child by enumeration order
|
|
/// (deterministic within a boot; single-volume by construction, and choosing
|
|
/// the BOOT volume by content when two sticks are present is the volume-manager
|
|
/// track), and consumer-hello for the channel of the driver bound to it.
|
|
/// Null until the chain is up — the harness's poll retries.
|
|
fn acquireVolume() ?block.Device {
|
|
const manager = manager_handle orelse opened: {
|
|
const handle = channel.openEndpoint("device-manager") orelse return null;
|
|
manager_handle = handle;
|
|
break :opened handle;
|
|
};
|
|
|
|
// The envelope's reserved `enumerate` verb, PAGED: one reply carries only
|
|
// a handful of entries and a real tree (a dozen ACPI nodes before the
|
|
// first USB child) is bigger, so `Header.target` is the start cursor and
|
|
// a short page is the end. Identity is the bus's native triple, for USB
|
|
// (base << 16) | (class << 8) | protocol — mass storage is base 0x08,
|
|
// subclass 0x06 (SCSI transparent), the same key devices.csv matches on.
|
|
const Entry = device_manager_protocol.ChildEntry;
|
|
var start: u64 = 0;
|
|
while (true) {
|
|
const enumerate = envelope.Header{ .operation = envelope.operation_enumerate, .target = start };
|
|
var reply: [device_manager_protocol.message_maximum]u8 = undefined;
|
|
const length = ipc.call(manager, std.mem.asBytes(&enumerate), &reply) catch return null;
|
|
const status = envelope.statusOf(reply[0..length]) orelse return null;
|
|
if (status.status != 0) return null;
|
|
|
|
const carried = @min(@as(usize, status.len), length -| envelope.prefix_size);
|
|
const tail = reply[envelope.prefix_size..][0..carried];
|
|
const count = tail.len / @sizeOf(Entry);
|
|
if (count == 0) return null; // the tree is exhausted; no volume yet
|
|
var index: usize = 0;
|
|
while (index < count) : (index += 1) {
|
|
const entry = std.mem.bytesToValue(Entry, tail[index * @sizeOf(Entry) ..][0..@sizeOf(Entry)]);
|
|
if (entry.device_id == device_manager_protocol.no_device) continue;
|
|
if ((entry.identity >> 16) & 0xff != 0x08 or (entry.identity >> 8) & 0xff != 0x06) continue;
|
|
const exchanged = driver.helloOn(manager, .consumer, entry.device_id, null, true) orelse return null;
|
|
const provider = exchanged.channel orelse continue; // its driver not up yet — next tick
|
|
return .{ .endpoint = provider };
|
|
}
|
|
start += count;
|
|
}
|
|
}
|
|
|
|
/// Durable-on-close: commit the device write cache if any block reached it since
|
|
/// the last flush. The harness calls this on every close; the dirty check keeps
|
|
/// it cheap. `device_dirty` lives here because `IpcBlock.writeBlocks` sets it.
|
|
fn flushIfDirty() void {
|
|
if (device_dirty) {
|
|
_ = ipc_block.device.flush();
|
|
device_dirty = false;
|
|
}
|
|
}
|
|
|
|
/// FAT bring-up: find the block device, set up DMA, mount the engine, and hand
|
|
/// the volume to the harness — or null to retry on the harness's timer.
|
|
fn fatBringUp(endpoint: ipc.Handle) ?Harness.Volume {
|
|
_ = endpoint;
|
|
const device = acquireVolume() orelse return null;
|
|
const geometry = device.geometry() orelse {
|
|
_ = logging.write("/system/services/fat: block geometry unavailable\n");
|
|
return null;
|
|
};
|
|
// Shareable so the buffer's capability can be attached down the chain (block
|
|
// server -> controller), making its physical addresses reachable by the
|
|
// device under an enforcing IOMMU. No-op binding otherwise.
|
|
const bounce = memory.dmaAlloc(engine.max_transfer_sectors * 512, memory.dma_coherent | memory.dma_shareable) orelse return null;
|
|
if (bounce.handle) |handle| {
|
|
// Attach, detach, and attach again: the round trip exercises BOTH verbs
|
|
// of the DMA-window lifecycle through the whole chain (fat -> storage ->
|
|
// bus -> kernel) on every boot, so a broken detach fails every fat case
|
|
// rather than lying dormant until the first buffer replacement.
|
|
if (!device.attach(handle)) {
|
|
_ = logging.write("/system/services/fat: could not attach the DMA bounce buffer\n");
|
|
return null;
|
|
}
|
|
if (!device.detach(handle)) {
|
|
_ = logging.write("/system/services/fat: could not detach the DMA bounce buffer\n");
|
|
return null;
|
|
}
|
|
if (!device.attach(handle)) {
|
|
_ = logging.write("/system/services/fat: could not re-attach the DMA bounce buffer\n");
|
|
return null;
|
|
}
|
|
_ = ipc.close(handle); // the binding holds its own reference now
|
|
}
|
|
ipc_block = .{ .device = device, .bounce = bounce };
|
|
|
|
const block_device = engine.BlockDevice{
|
|
.context = &ipc_block,
|
|
.block_size = geometry.block_size,
|
|
.block_count = geometry.block_count,
|
|
.readBlocksFn = IpcBlock.readBlocks,
|
|
.writeBlocksFn = IpcBlock.writeBlocks,
|
|
};
|
|
filesystem = engine.FileSystem.mount(block_device) orelse {
|
|
_ = logging.write("/system/services/fat: not a FAT filesystem\n");
|
|
return null;
|
|
};
|
|
std.log.info("mounted FAT ({s}, {d} clusters, partition lba {d})", .{ @tagName(filesystem.geometry.fat_type), filesystem.geometry.cluster_count, filesystem.base_lba });
|
|
|
|
return .{ .engine = &filesystem, .mounts = &fat_mounts, .flush = flushIfDirty };
|
|
}
|
|
|
|
pub fn main() void {
|
|
_ = logging.write("/system/services/fat: starting, waiting for a block device\n");
|
|
Harness.run(.{ .bringUp = fatBringUp });
|
|
}
|