build: the unix paths retire — configuration, logs, and volumes move into the danos tree

/etc/init.csv and /etc/devices.csv become /system/configuration/*.csv (the
repo's etc/ moves to system/configuration/, mirroring the runtime tree),
/var/log becomes /system/logs, and /mnt/usb becomes /volumes/usb. The
kernel VFS gains a carve-out so FAT may serve exactly /system/configuration
and /system/logs beneath the initrd-backed /system while /system and /test
themselves stay unshadowable; FAT's single /var mount splits into those two
rewritten mounts. The kvfs readdir check learns /system's third child and
the ramdisk spawn sweep skips the configuration tree.

Suite 106/106.
This commit is contained in:
Daniel Samson
2026-07-31 19:41:35 +01:00
parent e4da4e0610
commit c4f16a5448
31 changed files with 161 additions and 128 deletions
+1 -1
View File
@@ -215,7 +215,7 @@ fn onInit(endpoint: ipc.Handle) bool {
const entry = registered[i];
const hid = entry.hid[0..entry.hid_len];
// The devices.csv columns (bus=acpi, hid) then the human-readable name — a
// would-be /etc/devices.csv row read straight off the boot log.
// would-be /system/configuration/devices.csv row read straight off the boot log.
const desc = acpi_ids.description(hid);
if (desc.len != 0)
std.log.info("device {d} bus=acpi hid={s} — {s} ({d} resources)", .{ entry.device_id, hid, desc, entry.resource_count })
@@ -28,7 +28,7 @@ const registry = @import("device-registry");
const fs = @import("file-system");
// --- the device registry ------------------------------------------------------
// Driver matching is data-driven and authoritative: /etc/devices.csv (parsed by
// Driver matching is data-driven and authoritative: /system/configuration/devices.csv (parsed by
// the device-registry module) names, per bus, which driver binds a reported
// device, the most-specific match winning. There is no compiled-in fallback — a
// device no row matches goes unbound and is logged. This retired the hand-kept
@@ -41,12 +41,12 @@ var registry_source: [8192]u8 = undefined;
var registry_rules: [64]registry.Rule = undefined;
var registry_count: usize = 0;
/// Read and parse /etc/devices.csv once at boot. The file lives in the initial
/// Read and parse /system/configuration/devices.csv once at boot. The file lives in the initial
/// ramdisk, which the kernel serves directly — no filesystem service need be up
/// (fat is spawned after the manager), so this is a plain fs.open + read.
fn loadRegistry() void {
var file = fs.open("/etc/devices.csv", .{}) orelse {
_ = logging.write("/system/services/device-manager: /etc/devices.csv missing — nothing will match\n");
var file = fs.open("/system/configuration/devices.csv", .{}) orelse {
_ = logging.write("/system/services/device-manager: /system/configuration/devices.csv missing — nothing will match\n");
return;
};
defer file.close();
@@ -58,9 +58,9 @@ fn loadRegistry() void {
}
const result = registry.parse(registry_source[0..used], &registry_rules);
registry_count = result.count;
if (result.malformed != 0) std.log.info("/etc/devices.csv: {d} malformed line(s) skipped", .{result.malformed});
if (result.truncated) _ = logging.write("/system/services/device-manager: /etc/devices.csv has more rules than the table holds\n");
std.log.info("/etc/devices.csv: {d} rule(s) loaded", .{registry_count});
if (result.malformed != 0) std.log.info("/system/configuration/devices.csv: {d} malformed line(s) skipped", .{result.malformed});
if (result.truncated) _ = logging.write("/system/services/device-manager: /system/configuration/devices.csv has more rules than the table holds\n");
std.log.info("/system/configuration/devices.csv: {d} rule(s) loaded", .{registry_count});
}
/// Build a registry Identity from a bus driver's report: the bus it named, the
@@ -443,7 +443,7 @@ fn onChildAdded(message: []const u8, reply: []u8, sender: u32) usize {
if (!addChild(report.parent, report.bus_address, report.identity, report.device_id, sender)) status = -1;
std.log.info("child added (device {d} port {d}, identity {d}) by {s}", .{ report.parent, report.bus_address, report.identity, driver.name() });
if (status == 0) publishEvent(message[0..device_manager_protocol.child_added_size]);
// Matching from reports (M19.3), now data-driven via the /etc/devices.csv
// Matching from reports (M19.3), now data-driven via the /system/configuration/devices.csv
// registry: a registered child gets the most-specific driver its identity
// matches, once — re-reports after a bus restart dedupe on the registered
// id, exactly like the registrations do.
@@ -451,7 +451,7 @@ fn onChildAdded(message: []const u8, reply: []u8, sender: u32) usize {
const id = identityFromReport(report);
if (registry.matchDriver(registry_rules[0..registry_count], id)) |match| {
if (match.ambiguous)
std.log.info("/etc/devices.csv: multiple equally-specific rules match the device {s} reported; binding {s}", .{ driver.name(), match.driver });
std.log.info("/system/configuration/devices.csv: multiple equally-specific rules match the device {s} reported; binding {s}", .{ driver.name(), match.driver });
if (id.bus == .acpi) {
// An hid-matched driver (ps2-bus) is a singleton that finds its
// own devices once spawned — spawn it once, no device assignment.
+1 -1
View File
@@ -73,7 +73,7 @@ const entries_per_sector = sector_size / @sizeOf(on_disk.DirectoryEntry); // 16
// A small write-through cache of single-sector (metadata) accesses: FAT sectors,
// directory sectors, and directory-entry writebacks. Its payoff is repeated scans
// — resolving many paths under the same directory (a logging burst opening dozens
// of files under /var/log/<stamp>/) re-reads the same directory and FAT sectors,
// of files under /system/logs/<stamp>/) re-reads the same directory and FAT sectors,
// which now come from RAM instead of a USB round trip each. Bulk file data (the
// multi-sector run path) bypasses the cache — it is large and not re-read — and a
// run write invalidates any overlapping cached sector to stay coherent.
+17 -10
View File
@@ -1,8 +1,8 @@
//! 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
//! the VFS at /volumes/usb. From then on the VFS forwards every open/read/write/
//! status/readdir/close under /volumes/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
@@ -22,7 +22,7 @@ const engine = @import("engine.zig");
const on_disk = @import("on-disk.zig");
const vfs_protocol = @import("vfs-protocol");
const mount_point = "/mnt/usb";
const mount_point = "/volumes/usb";
// The engine's BlockDevice, backed by the `.block` driver plus a DMA bounce
// buffer the driver reads/writes by physical address.
@@ -144,18 +144,25 @@ fn tryBringUp() void {
};
std.log.info("mounted FAT ({s}, {d} clusters, partition lba {d})", .{ @tagName(filesystem.geometry.fat_type), filesystem.geometry.cluster_count, filesystem.base_lba });
// Mount ourselves into the kernel VFS at /mnt/usb — and serve /var from the
// volume's /var subtree, so FHS paths (the logger's /var/log) stay decoupled
// from which volume carries them.
// Mount ourselves into the kernel VFS at /volumes/usb — and serve
// /system/configuration and /system/logs from the volume's identically-named
// subtrees (the boot volume is hierarchy-shaped, so rewrite == prefix), so
// hierarchy paths (the logger's /system/logs) stay decoupled from which
// volume carries them.
if (file_system.mount(mount_point, endpointForMount())) {
std.log.info("mounted {s}", .{mount_point});
} else {
_ = logging.write("/system/services/fat: could not mount /mnt/usb\n");
_ = logging.write("/system/services/fat: could not mount /volumes/usb\n");
}
if (file_system.mountRewritten("/var", endpointForMount(), "/var")) {
std.log.info("mounted /var", .{});
if (file_system.mountRewritten("/system/configuration", endpointForMount(), "/system/configuration")) {
std.log.info("mounted /system/configuration", .{});
} else {
_ = logging.write("/system/services/fat: could not mount /var\n");
_ = logging.write("/system/services/fat: could not mount /system/configuration\n");
}
if (file_system.mountRewritten("/system/logs", endpointForMount(), "/system/logs")) {
std.log.info("mounted /system/logs", .{});
} else {
_ = logging.write("/system/services/fat: could not mount /system/logs\n");
}
mounted = true;
}
+10 -10
View File
@@ -29,17 +29,17 @@ const fs = @import("file-system");
const csv = @import("csv");
/// The system services init brings up at boot are init's policy, not the kernel's —
/// and that policy is now data: `/etc/init.csv` (see `loadServices`), read at
/// and that policy is now data: `/system/configuration/init.csv` (see `loadServices`), read at
/// startup instead of a hardcoded list. Drivers are absent on purpose: the device
/// manager owns those.
///
/// The most services `/etc/init.csv` can list, and the most argv entries (beyond the
/// The most services `/system/configuration/init.csv` can list, and the most argv entries (beyond the
/// path) each may carry. Fixed caps because init parses the list into static storage —
/// the freestanding, no-allocator counterpart to the device manager's registry table.
const max_services = 16;
const max_service_args = 4;
/// One service init starts, parsed from a row of `/etc/init.csv`: its binary path
/// One service init starts, parsed from a row of `/system/configuration/init.csv`: its binary path
/// and argv, both slices into `init_csv` (held for the life of the process).
const Service = struct {
path: []const u8 = "",
@@ -50,7 +50,7 @@ const Service = struct {
}
};
/// The `/etc/init.csv` bytes, held because the parsed services slice into them.
/// The `/system/configuration/init.csv` bytes, held because the parsed services slice into them.
var init_csv: [4096]u8 = undefined;
var services: [max_services]Service = .{Service{}} ** max_services;
var service_count: usize = 0;
@@ -65,16 +65,16 @@ var restart_counts: [max_services]u32 = .{0} ** max_services;
var shutting_down = false;
var supervision_endpoint: ipc.Handle = 0;
/// Parse `/etc/init.csv` into `services`, in file order (startup order; shutdown is
/// Parse `/system/configuration/init.csv` into `services`, in file order (startup order; shutdown is
/// the reverse). Each row is a binary path followed by its argv, comma-separated;
/// `#` comments and blank lines are ignored. The file lives in the initial ramdisk,
/// which the kernel serves directly, so init — PID 1, running before any filesystem
/// service — reads it with a plain fs.open, the same mechanism the device manager
/// uses for /etc/devices.csv. A missing file means no services (the no-ramdisk
/// uses for /system/configuration/devices.csv. A missing file means no services (the no-ramdisk
/// isolation test): loud, but not fatal.
fn loadServices() void {
var file = fs.open("/etc/init.csv", .{}) orelse {
_ = logging.write("/system/services/init: /etc/init.csv missing — no services started\n");
var file = fs.open("/system/configuration/init.csv", .{}) orelse {
_ = logging.write("/system/services/init: /system/configuration/init.csv missing — no services started\n");
return;
};
defer file.close();
@@ -89,7 +89,7 @@ fn loadServices() void {
const body = csv.stripComment(line);
if (body.len == 0) continue;
if (service_count >= services.len) {
_ = logging.write("/system/services/init: /etc/init.csv has more services than the table holds\n");
_ = logging.write("/system/services/init: /system/configuration/init.csv has more services than the table holds\n");
break;
}
var it = csv.fields(body);
@@ -137,7 +137,7 @@ pub fn main() void {
// Load the service list, then bring each up supervised so init can stop them
// cleanly. Best-effort and silent: each service announces its own readiness,
// and with no /etc/init.csv (an isolation test) the loop starts nothing.
// and with no /system/configuration/init.csv (an isolation test) the loop starts nothing.
loadServices();
for (services[0..service_count], 0..) |*service, i| {
if (process.spawnSupervised(service.path, service.arguments(), supervision_endpoint)) |id| child_ids[i] = id;
+7 -7
View File
@@ -4,7 +4,7 @@
//! demultiplexes it into **one file per process** on the flash volume:
//!
//! <base>/<boot-stamp>/<binary-path>.log
//! e.g. /mnt/usb/var/log/2026-07-21T101530Z/system/services/fat.log
//! e.g. /volumes/usb/system/logs/2026-07-21T101530Z/system/services/fat.log
//!
//! The boot stamp is the wall-clock time of boot (from klog_status), so one
//! boot session is one self-contained directory; the kernel's own records go to
@@ -37,11 +37,11 @@ const time = @import("time");
const logging = @import("logging");
/// Where log trees live: the FHS path. The kernel VFS routes /var to whatever
/// volume the fat server mounted there (today: the /var subtree of the USB
/// flash volume) — swapping the persistent medium later touches fat's two
/// mount calls, never this constant.
const base = "/var/log";
/// Where log trees live: the hierarchy path. The kernel VFS routes /system/logs
/// to whatever volume the fat server mounted there (today: the /system/logs
/// subtree of the USB flash volume) — swapping the persistent medium later
/// touches fat's mount calls, never this constant.
const base = "/system/logs";
/// Drain cadence and the quiet period after which files are closed (flushed).
const tick_ms = 250;
@@ -128,7 +128,7 @@ fn onTerminate() void {
fn tick() void {
if (!storage_ready) {
// makePath doubles as the readiness probe: while /var is unmounted the
// makePath doubles as the readiness probe: while /system/logs is unmounted the
// resolve fails fast (no storage round trip) and the ring buffers; the
// first success creates the whole per-boot tree.
if (!fs.makePath(boot_directory[0..boot_directory_len])) return;