exfat: the service + build wiring (S4 step 4)
exfat.zig is a near-clone of fat.zig — the reuse the architecture promised, now real: same IpcBlock DMA-bounce wrapper, same acquire-volume-by-hello to the volume manager, same content-conditional boot rewrites (resolve /system/configuration to decide the system volume), all differences confined to the engine it wraps. That the harness's Server(engine.FileSystem) compiles is the proof the exFAT engine meets the same pub-fn contract as FAT — a comptime check, not a hope. The exfat package (build.zig + build.zig.zon) mirrors fat's: it ships in production_ship, its on-disk + engine host tests run through the root test aggregate (replacing the temporary standalone entry), and the root zon declares it. build + zig build test + bounds all green.
This commit is contained in:
@@ -122,6 +122,7 @@ fn driverArtifact(comptime package: []const u8, comptime artifact: []const u8) S
|
|||||||
/// (docs/build-packages-plan.md).
|
/// (docs/build-packages-plan.md).
|
||||||
const production_ship = [_]ShipRow{
|
const production_ship = [_]ShipRow{
|
||||||
service("fat"),
|
service("fat"),
|
||||||
|
service("exfat"),
|
||||||
service("display"),
|
service("display"),
|
||||||
service("display-demo"),
|
service("display-demo"),
|
||||||
service("device-manager"),
|
service("device-manager"),
|
||||||
@@ -422,10 +423,6 @@ pub fn build(b: *std.Build) void {
|
|||||||
"system/boot-handoff.zig",
|
"system/boot-handoff.zig",
|
||||||
"system/abi.zig",
|
"system/abi.zig",
|
||||||
"system/initial-ramdisk.zig", // v2 path-named entries: find/basename/magic
|
"system/initial-ramdisk.zig", // v2 path-named entries: find/basename/magic
|
||||||
// The exFAT engine (S4), std-only, before its package exists — the
|
|
||||||
// engine imports on-disk.zig, so this one entry runs both files' tests.
|
|
||||||
// Moves into the `exfat` package's own test step once that lands (step 4).
|
|
||||||
"system/services/exfat/engine.zig",
|
|
||||||
}) |root| {
|
}) |root| {
|
||||||
const mod_tests = b.addTest(.{
|
const mod_tests = b.addTest(.{
|
||||||
.root_module = b.createModule(.{
|
.root_module = b.createModule(.{
|
||||||
@@ -451,6 +448,7 @@ pub fn build(b: *std.Build) void {
|
|||||||
csv_library,
|
csv_library,
|
||||||
xkeyboard_config_library,
|
xkeyboard_config_library,
|
||||||
b.dependency("fat", .{}),
|
b.dependency("fat", .{}),
|
||||||
|
b.dependency("exfat", .{}),
|
||||||
b.dependency("volume-manager", .{}),
|
b.dependency("volume-manager", .{}),
|
||||||
b.dependency("display", .{}),
|
b.dependency("display", .{}),
|
||||||
b.dependency("ps2-bus", .{}),
|
b.dependency("ps2-bus", .{}),
|
||||||
|
|||||||
@@ -46,6 +46,7 @@
|
|||||||
.@"pci-bus" = .{ .path = "system/drivers/pci-bus" },
|
.@"pci-bus" = .{ .path = "system/drivers/pci-bus" },
|
||||||
.init = .{ .path = "system/services/init" },
|
.init = .{ .path = "system/services/init" },
|
||||||
.fat = .{ .path = "system/services/fat" },
|
.fat = .{ .path = "system/services/fat" },
|
||||||
|
.exfat = .{ .path = "system/services/exfat" },
|
||||||
.display = .{ .path = "system/services/display" },
|
.display = .{ .path = "system/services/display" },
|
||||||
.@"display-demo" = .{ .path = "system/services/display-demo" },
|
.@"display-demo" = .{ .path = "system/services/display-demo" },
|
||||||
.@"device-manager" = .{ .path = "system/services/device-manager" },
|
.@"device-manager" = .{ .path = "system/services/device-manager" },
|
||||||
|
|||||||
@@ -0,0 +1,34 @@
|
|||||||
|
//! The exfat service as a binary package (docs/build-packages-plan.md):
|
||||||
|
//! this file names the binary and EXACTLY the modules its source imports —
|
||||||
|
//! build-support resolves each name from the domains this zon declares.
|
||||||
|
|
||||||
|
const std = @import("std");
|
||||||
|
const build_support = @import("build-support");
|
||||||
|
|
||||||
|
pub fn build(b: *std.Build) void {
|
||||||
|
const exe = build_support.userBinary(b, .{
|
||||||
|
.name = "exfat",
|
||||||
|
.root_source_file = b.path("exfat.zig"),
|
||||||
|
.imports = &.{
|
||||||
|
"block", "channel", "envelope", "file-system-harness",
|
||||||
|
"ipc", "logging", "memory", "process",
|
||||||
|
"time", "volume-manager-protocol",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
b.installArtifact(exe);
|
||||||
|
|
||||||
|
// Standalone `zig build test`; the root aggregate depends on this step.
|
||||||
|
const test_step = b.step("test", "Run the exfat unit tests");
|
||||||
|
for ([_][]const u8{
|
||||||
|
"on-disk.zig", // exFAT on-disk struct sizes + geometry + checksums
|
||||||
|
"engine.zig", // exFAT read/write over a RAM-backed image
|
||||||
|
}) |test_root| {
|
||||||
|
const unit_tests = b.addTest(.{
|
||||||
|
.root_module = b.createModule(.{
|
||||||
|
.root_source_file = b.path(test_root),
|
||||||
|
.target = b.resolveTargetQuery(.{}),
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
test_step.dependOn(&b.addRunArtifact(unit_tests).step);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
.{
|
||||||
|
.name = .exfat,
|
||||||
|
.version = "0.0.0",
|
||||||
|
.fingerprint = 0x5eafdf02d20f93dd, // Changing this has security and trust implications.
|
||||||
|
.minimum_zig_version = "0.16.0",
|
||||||
|
.dependencies = .{
|
||||||
|
// build-support supplies the shared recipe; kernel is implicit in
|
||||||
|
// every binary (the root shim + link script live there). The rest
|
||||||
|
// are exactly the homes of this binary's declared imports.
|
||||||
|
.@"build-support" = .{ .path = "../../../build-support" },
|
||||||
|
.kernel = .{ .path = "../../../library/kernel" },
|
||||||
|
.device = .{ .path = "../../../library/device" },
|
||||||
|
.protocol = .{ .path = "../../../library/protocol" },
|
||||||
|
},
|
||||||
|
.paths = .{""},
|
||||||
|
}
|
||||||
@@ -0,0 +1,200 @@
|
|||||||
|
//! system/services/exfat — the exFAT filesystem service. Like fat.zig, this is
|
||||||
|
//! only the format-specific half: it finds its block device, sets up the DMA
|
||||||
|
//! bounce buffer, mounts the exFAT engine on it, and hands the mounted volume to
|
||||||
|
//! the shared filesystem harness (library/kernel/file-system-harness), which owns
|
||||||
|
//! everything else — vfs 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.
|
||||||
|
//!
|
||||||
|
//! This service is a near-clone of fat.zig: the second engine reuses the harness
|
||||||
|
//! wholesale, which is the reuse the storage architecture promised
|
||||||
|
//! (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 volume_manager_protocol = @import("volume-manager-protocol");
|
||||||
|
const ipc = @import("ipc");
|
||||||
|
const process = @import("process");
|
||||||
|
const block = @import("block");
|
||||||
|
const memory = @import("memory");
|
||||||
|
const logging = @import("logging");
|
||||||
|
const time = @import("time");
|
||||||
|
const engine = @import("engine.zig");
|
||||||
|
const envelope = @import("envelope");
|
||||||
|
const harness = @import("file-system-harness");
|
||||||
|
|
||||||
|
/// The serving harness, specialized for the exFAT 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 volume this exFAT process serves, its id given as argv[1] by the volume
|
||||||
|
/// manager that spawned it. The startup hello names it so the manager returns the
|
||||||
|
/// right volume's channel.
|
||||||
|
var my_volume_id: u64 = 0;
|
||||||
|
|
||||||
|
/// The volume's own mount path, handed in as argv[2] by the volume manager: the
|
||||||
|
/// volume's content id-path (e.g. /volumes/exfat-12345678). Defaults to
|
||||||
|
/// /volumes/exfat only for a bare launch with no argument; the manager always
|
||||||
|
/// passes it. The slice points into the entry block, valid for the process life.
|
||||||
|
var volume_mount_prefix: []const u8 = "/volumes/exfat";
|
||||||
|
|
||||||
|
/// The mounts this volume installs: its own root, plus — only if it is the boot
|
||||||
|
/// volume (it resolves /system/configuration) — the two FHS rewrites, so the
|
||||||
|
/// logger's /system/logs stays decoupled from which volume backs it. Boot-volume
|
||||||
|
/// detection is by content, so it works no matter which volume carries /system.
|
||||||
|
/// bound: mounts one volume installs (its root + the two boot rewrites)
|
||||||
|
/// decided-by: ours
|
||||||
|
/// protects: the mount_specs array
|
||||||
|
/// at-limit: truncate - unreachable today (fixed at 3); more configured mounts
|
||||||
|
/// would need this raised, a deliberate change
|
||||||
|
/// observed-by: a mount silently missing from the harness's mount log
|
||||||
|
const maximum_mounts_per_volume = 4;
|
||||||
|
var mount_specs: [maximum_mounts_per_volume]harness.MountSpec = undefined;
|
||||||
|
|
||||||
|
/// Get this volume's block channel from the volume manager (establishment by
|
||||||
|
/// lineage — `block` is not a registry name). The manager spawned this process,
|
||||||
|
/// confined it to its partition, and answers the hello with the channel; the
|
||||||
|
/// channel is range-confined to this process's badge. Null until the manager has
|
||||||
|
/// the volume ready — this retries.
|
||||||
|
fn acquireVolume() ?block.Device {
|
||||||
|
var attempts: u32 = 0;
|
||||||
|
const vm = while (attempts < 500) : (attempts += 1) {
|
||||||
|
if (channel.openEndpoint("volume-manager")) |handle| break handle;
|
||||||
|
time.sleepMillis(20);
|
||||||
|
} else return null;
|
||||||
|
|
||||||
|
attempts = 0;
|
||||||
|
while (attempts < 500) : (attempts += 1) {
|
||||||
|
var packet: [volume_manager_protocol.message_maximum]u8 = undefined;
|
||||||
|
const framed = volume_manager_protocol.Protocol.encodeRequest(.hello, my_volume_id, .{}, &.{}, &packet) orelse return null;
|
||||||
|
var reply: [volume_manager_protocol.message_maximum]u8 = undefined;
|
||||||
|
const answered = ipc.callCap(vm, framed, &reply, null) catch return null;
|
||||||
|
const status = envelope.statusOf(reply[0..answered.len]) orelse return null;
|
||||||
|
if (status.status != 0) {
|
||||||
|
if (answered.cap) |stray| _ = ipc.close(stray);
|
||||||
|
_ = logging.write("/system/services/exfat: volume manager refused the hello\n");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
if (answered.cap) |bus| return .{ .endpoint = bus };
|
||||||
|
// Acked with no channel: the volume is not ready yet — retry.
|
||||||
|
time.sleepMillis(20);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 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.
|
||||||
|
fn flushIfDirty() void {
|
||||||
|
if (device_dirty) {
|
||||||
|
_ = ipc_block.device.flush();
|
||||||
|
device_dirty = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// exFAT 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 exfatBringUp(endpoint: ipc.Handle) ?Harness.Volume {
|
||||||
|
_ = endpoint;
|
||||||
|
const device = acquireVolume() orelse return null;
|
||||||
|
const geometry = device.geometry() orelse {
|
||||||
|
_ = logging.write("/system/services/exfat: block geometry unavailable\n");
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
// Shareable so the buffer's capability can be attached down the chain, making
|
||||||
|
// its physical addresses reachable under an enforcing IOMMU. No-op 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 on every boot.
|
||||||
|
if (!device.attach(handle)) {
|
||||||
|
_ = logging.write("/system/services/exfat: could not attach the DMA bounce buffer\n");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
if (!device.detach(handle)) {
|
||||||
|
_ = logging.write("/system/services/exfat: could not detach the DMA bounce buffer\n");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
if (!device.attach(handle)) {
|
||||||
|
_ = logging.write("/system/services/exfat: 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/exfat: not an exFAT filesystem\n");
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
std.log.info("mounted exFAT ({d} clusters, {d} sectors/cluster, serial 0x{x})", .{ filesystem.geometry.cluster_count, filesystem.geometry.sectors_per_cluster, filesystem.geometry.volume_serial_number });
|
||||||
|
|
||||||
|
// The volume mounts at its id-path (argv[2]). The boot/system volume — the one
|
||||||
|
// carrying the /system tree — additionally installs the two FHS rewrites, by
|
||||||
|
// CONTENT: it resolves /system/configuration on its own media. A data volume
|
||||||
|
// mounts only at its id-path and never shadows the running system.
|
||||||
|
mount_specs[0] = .{ .prefix = volume_mount_prefix };
|
||||||
|
var mount_count: usize = 1;
|
||||||
|
if (filesystem.resolve("/system/configuration") != null) {
|
||||||
|
std.log.info("volume {d} carries the system tree; backing /system/configuration and /system/logs", .{my_volume_id});
|
||||||
|
mount_specs[1] = .{ .prefix = "/system/configuration", .rewrite = "/system/configuration" };
|
||||||
|
mount_specs[2] = .{ .prefix = "/system/logs", .rewrite = "/system/logs" };
|
||||||
|
mount_count = 3;
|
||||||
|
} else {
|
||||||
|
std.log.info("volume {d} is a data volume; mounted at {s}", .{ my_volume_id, volume_mount_prefix });
|
||||||
|
}
|
||||||
|
return .{ .engine = &filesystem, .mounts = mount_specs[0..mount_count], .flush = flushIfDirty };
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn main(init: process.Init) void {
|
||||||
|
// The volume manager spawns this process with its volume id as argv[1] and the
|
||||||
|
// volume's mount path (its id-path) as argv[2].
|
||||||
|
if (init.arguments.get(1)) |id| {
|
||||||
|
my_volume_id = std.fmt.parseInt(u64, id, 10) catch 0;
|
||||||
|
}
|
||||||
|
if (init.arguments.get(2)) |prefix| {
|
||||||
|
volume_mount_prefix = prefix;
|
||||||
|
}
|
||||||
|
_ = logging.write("/system/services/exfat: starting, waiting for a block device\n");
|
||||||
|
Harness.run(.{ .bringUp = exfatBringUp });
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user