volume-manager: N-volume device+volume tables, behavior-preserving (S3)

Replace the single `var volume: ?Volume` and file-global supervision
state with two fixed tables: devices[maximum_devices] owning each adopted
block channel once, and volumes[maximum_volumes] each carrying its own
identity, id, mount prefix, and supervision fields (restarts, spawn_ns,
failed, restart_pending, restart_due_ns). A monotonic next_volume_id
never reuses ids, so a stale hello can't address the wrong child.

Lookups (deviceById, volumeById, volumeByPid, firstUsedVolume) and
claims (claimDevice, claimVolumeIndex) replace the ad-hoc singletons.
pollTick reconciles devices first (removeDevice drops their volumes),
then per-volume restarts, then idle bring-up.

This step stays one-device/one-volume on purpose: bringUpVolume adopts
the first device and caps allVolumes to a single partition, so behavior
is identical and the full suite stays 128/128. Uncapping and adopt-all
land next.
This commit is contained in:
Daniel Samson
2026-08-10 01:13:52 +01:00
parent d4b544d66b
commit 7efe7b72d8
+240 -153
View File
@@ -8,10 +8,13 @@
//! supervises the filesystems it spawns, exactly as the device manager //! supervises the filesystems it spawns, exactly as the device manager
//! supervises drivers. //! supervises drivers.
//! //!
//! This increment (V3b) is the flip: the FAT service stops acquiring its own //! The manager holds a table of adopted storage DEVICES and a table of the
//! volume and is spawned here instead, confined to its partition, and handed //! VOLUMES on them: one filesystem process per volume, each confined to its
//! its channel over the volume-manager protocol. Single volume for now; the //! partition's badge-scoped block range, each supervised with its own budget. A
//! mount map (volumes.csv) and multi-volume land next. //! device leaving the tree takes its volumes with it. (This S3 increment lays the
//! tables in; multi-device adoption and per-partition spawn land in the next
//! step — for now it adopts one device and its first volume, behavior-identical
//! to before.)
const std = @import("std"); const std = @import("std");
const channel = @import("channel"); const channel = @import("channel");
@@ -35,22 +38,37 @@ const Serve = volume_manager_protocol.Protocol.Provider(void);
const Invocation = envelope.Invocation; const Invocation = envelope.Invocation;
const Answer = envelope.Answer; const Answer = envelope.Answer;
/// The single volume this increment handles: its provider channel, its block /// One adopted storage device: the block channel to its provider (opened once and
/// sub-range, its identity, the id it is addressed by, and the filesystem /// shared — refcounted per confined filesystem via the hello reply) and the
/// process serving it (0 until spawned; reset on death for respawn). /// device-manager id it serves. A device leaving the tree takes its volumes.
const Volume = struct { const StorageDevice = struct {
storage: block.Device, used: bool = false,
storage_device_id: u64, // the device-manager id this volume's provider serves device_id: u64 = 0,
base_lba: u64, channel: block.Device = undefined,
block_count: u64,
identity: partition.Identity,
id: u64,
binary: []const u8, // the service binary, from filesystems.csv by signature
mount_prefix: []const u8, // the volume-root mount path (its id-path, or a volumes.csv override)
filesystem_pid: u32 = 0,
}; };
const volume_id: u64 = 1; /// One volume: which device serves it, its block sub-range, its content
/// identity, the id it is addressed by, the service binary + mount path it was
/// spawned with, the filesystem process serving it, and its own supervision
/// budget (so one volume's crash loop never touches another's).
const Volume = struct {
used: bool = false,
device_id: u64 = 0,
base_lba: u64 = 0,
block_count: u64 = 0,
identity: partition.Identity = .{ .rung = .anonymous },
id: u64 = 0,
binary: []const u8 = "",
mount_prefix: []const u8 = "",
filesystem_pid: u32 = 0,
// Per-volume supervision, mirroring the device manager's: a clean exit is not
// restarted, a fault restarts with backoff, a fast crash loop gives up.
restarts: u32 = 0,
spawn_ns: u64 = 0,
failed: bool = false,
restart_pending: bool = false,
restart_due_ns: u64 = 0,
};
// The mount map, read from configuration at boot (the policy home, storage- // The mount map, read from configuration at boot (the policy home, storage-
// architecture.md): filesystems.csv (content signature -> service binary) and // architecture.md): filesystems.csv (content signature -> service binary) and
@@ -80,47 +98,82 @@ var filesystem_rules: [maximum_filesystem_rules]filesystem_map.Rule = undefined;
var filesystem_rule_count: usize = 0; var filesystem_rule_count: usize = 0;
var volume_rules: [maximum_volume_rules]volume_map.Override = undefined; var volume_rules: [maximum_volume_rules]volume_map.Override = undefined;
var volume_rule_count: usize = 0; var volume_rule_count: usize = 0;
/// The composed default mount path (/volumes/<id>) for the current volume; a
/// volumes.csv override is used in place and needs no buffer (it is already a
/// slice into volumes_source). One buffer suffices while the manager serves one
/// volume (multi-volume gives each its own in S3).
/// bound: bytes of a composed /volumes/<id> mount path /// bound: bytes of a composed /volumes/<id> mount path
/// decided-by: ours /// decided-by: ours
/// protects: the mount_prefix_buf below /// protects: the per-volume mount_prefix buffers below
/// at-limit: truncate - bufPrint fails; the volume mounts at a fallback path (logged) /// at-limit: truncate - bufPrint fails; the volume mounts at a fallback path (logged)
/// observed-by: the fallback path in the log /// observed-by: the fallback path in the log
const mount_path_maximum = 64; const mount_path_maximum = 64;
var mount_prefix_buf: [mount_path_maximum]u8 = undefined;
/// bound: volumes the manager serves at once
/// decided-by: ours
/// protects: the volumes table and its per-volume mount-path buffers
/// at-limit: truncate - a further partition is left unserved and logged (real
/// machines carry a handful of volumes, far under this)
/// observed-by: the "volume table full" log line
const maximum_volumes = 16;
/// bound: storage devices the manager adopts at once
/// decided-by: ours
/// protects: the devices table
/// at-limit: truncate - a further device is left unadopted and logged
/// observed-by: the "device table full" log line
const maximum_devices = 8;
var devices = [_]StorageDevice{.{}} ** maximum_devices;
var volumes = [_]Volume{.{}} ** maximum_volumes;
/// Each volume's composed default mount path lives in its slot's buffer; a
/// volumes.csv override is used in place (a slice into volumes_source, no buffer).
var mount_prefix_bufs: [maximum_volumes][mount_path_maximum]u8 = undefined;
var next_volume_id: u64 = 1; // monotonic — never reused, so a stale id can't address the wrong child
var service_endpoint: ipc.Handle = 0; var service_endpoint: ipc.Handle = 0;
var manager_handle: ?ipc.Handle = null; var manager_handle: ?ipc.Handle = null;
var bounce: memory.DmaRegion = undefined; var bounce: memory.DmaRegion = undefined;
var bounce_ready = false; var bounce_ready = false;
/// The currently-mounted volume, or null while no storage is present. The whole
/// removal lifecycle is this field going null and back: the poll sees the
/// storage provider leave the device tree (a pulled stick), kills the filesystem
/// and clears this; when it returns, the poll re-acquires and re-mounts.
var volume: ?Volume = null;
var logged_no_volume = false; var logged_no_volume = false;
/// How often the poll checks whether the storage provider is present. Fast /// How often the poll checks device presence and fires due restarts. Fast enough
/// enough that an unplug unmounts promptly; the poll is a bare device-manager /// that an unplug unmounts promptly; the poll is a bare device-manager enumerate,
/// enumerate, no channel work, so it is cheap to run continuously. /// no channel work, so it is cheap to run continuously.
const poll_interval_ms = 500; const poll_interval_ms = 500;
// Filesystem supervision, mirroring the device manager's (device-manager.zig): // Filesystem supervision, mirroring the device manager's (device-manager.zig).
// a clean exit is not restarted, a fault restarts with backoff, and a fast
// crash loop gives up rather than spinning. Without this a faulting filesystem
// respawns in a zero-delay loop.
const fast_death_ns: u64 = 2_000_000_000; const fast_death_ns: u64 = 2_000_000_000;
const crash_loop_cap: u32 = 3; const crash_loop_cap: u32 = 3;
const backoff_base_ms: u64 = 300; const backoff_base_ms: u64 = 300;
var fs_restarts: u32 = 0; /// bytes to format a u64 volume id as decimal (20 digits fit)
var fs_spawn_ns: u64 = 0; const id_decimal_bytes = 24;
var fs_failed = false;
/// A fat restart is due at `restart_due_ns`; the poll loop performs it once the // --- table lookups -----------------------------------------------------------
/// backoff has elapsed (one timer, folded into the poll — no second timer).
var restart_pending = false; fn deviceById(id: u64) ?*StorageDevice {
var restart_due_ns: u64 = 0; for (&devices) |*d| if (d.used and d.device_id == id) return d;
return null;
}
fn claimDevice() ?*StorageDevice {
for (&devices) |*d| if (!d.used) return d;
return null;
}
fn anyDeviceUsed() bool {
for (&devices) |*d| if (d.used) return true;
return false;
}
fn volumeById(id: u64) ?*Volume {
for (&volumes) |*v| if (v.used and v.id == id) return v;
return null;
}
fn volumeByPid(pid: u32) ?*Volume {
for (&volumes) |*v| if (v.used and v.filesystem_pid == pid) return v;
return null;
}
fn firstUsedVolume() ?*Volume {
for (&volumes) |*v| if (v.used) return v;
return null;
}
fn claimVolumeIndex() ?usize {
for (&volumes, 0..) |*v, i| if (!v.used) return i;
return null;
}
// --- device-manager plumbing -------------------------------------------------
fn deviceManager() ?ipc.Handle { fn deviceManager() ?ipc.Handle {
if (manager_handle) |h| return h; if (manager_handle) |h| return h;
@@ -131,13 +184,12 @@ fn deviceManager() ?ipc.Handle {
const OpenedStorage = struct { device_id: u64, device: block.Device }; const OpenedStorage = struct { device_id: u64, device: block.Device };
/// The first mass-storage provider whose block channel actually opens, with its /// The first mass-storage provider whose block channel opens and is NOT already
/// device id. A device-manager tree can carry more than one entry of the /// adopted, with its device id. A device-manager tree can carry more than one
/// mass-storage identity — a phantom that no driver is bound to answers a /// entry of the mass-storage identity — a phantom that no driver is bound to
/// consumer hello with NO channel — so this tries each and takes the first that /// answers a consumer hello with NO channel — so this tries each and takes the
/// yields a channel, exactly as a filesystem's own acquisition loop does. /// first that yields a channel. Skips already-adopted devices so a re-poll does
/// Called only when there is no volume (an insertion), so the hellos it makes /// not re-open a device it already serves.
/// are not per-poll churn.
fn openAnyStorage() ?OpenedStorage { fn openAnyStorage() ?OpenedStorage {
const manager = deviceManager() orelse return null; const manager = deviceManager() orelse return null;
const Entry = device_manager_protocol.ChildEntry; const Entry = device_manager_protocol.ChildEntry;
@@ -157,6 +209,7 @@ fn openAnyStorage() ?OpenedStorage {
const entry = std.mem.bytesToValue(Entry, tail[index * @sizeOf(Entry) ..][0..@sizeOf(Entry)]); 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.device_id == device_manager_protocol.no_device) continue;
if ((entry.identity >> 16) & 0xff != 0x08 or (entry.identity >> 8) & 0xff != 0x06) continue; if ((entry.identity >> 16) & 0xff != 0x08 or (entry.identity >> 8) & 0xff != 0x06) continue;
if (deviceById(entry.device_id) != null) continue; // already adopted
const exchanged = driver.helloOn(manager, .consumer, entry.device_id, null, true) orelse continue; const exchanged = driver.helloOn(manager, .consumer, entry.device_id, null, true) orelse continue;
const provider = exchanged.channel orelse continue; // a phantom / not-yet-bound entry const provider = exchanged.channel orelse continue; // a phantom / not-yet-bound entry
return .{ .device_id = entry.device_id, .device = .{ .endpoint = provider } }; return .{ .device_id = entry.device_id, .device = .{ .endpoint = provider } };
@@ -167,7 +220,7 @@ fn openAnyStorage() ?OpenedStorage {
/// Whether `device_id` is still in the device-manager tree — a bare enumerate, /// Whether `device_id` is still in the device-manager tree — a bare enumerate,
/// no consumer-hello, so it is cheap to call every poll. This is how removal is /// no consumer-hello, so it is cheap to call every poll. This is how removal is
/// detected: the specific device the mounted volume sits on disappears. /// detected: the specific device a mounted volume sits on disappears.
fn isDevicePresent(device_id: u64) bool { fn isDevicePresent(device_id: u64) bool {
const manager = deviceManager() orelse return false; const manager = deviceManager() orelse return false;
const Entry = device_manager_protocol.ChildEntry; const Entry = device_manager_protocol.ChildEntry;
@@ -191,58 +244,79 @@ fn isDevicePresent(device_id: u64) bool {
} }
} }
/// Spawn the filesystem for `v`, confine it to the volume's range, and record // --- lifecycle ---------------------------------------------------------------
/// its pid. The confinement is defined for the fresh pid BEFORE the filesystem
/// runs, so its first read is already bounded; the volume manager is the /// Spawn the filesystem for `v`, confine it to the volume's range on its device's
/// confinement controller (it defines the first range on the device). /// channel, and record its pid. The confinement is defined for the fresh pid
/// BEFORE the filesystem runs, so its first read is already bounded; the volume
/// manager is the confinement controller (it defines the first range on the
/// device).
fn spawnFilesystem(v: *Volume) void { fn spawnFilesystem(v: *Volume) void {
if (fs_failed) return; if (v.failed) return;
const pid = process.spawnSupervised(v.binary, &.{ "1", v.mount_prefix }, service_endpoint) orelse { const dev = deviceById(v.device_id) orelse return; // its device left — poll will clean up
var id_str_buf: [id_decimal_bytes]u8 = undefined;
const id_str = std.fmt.bufPrint(&id_str_buf, "{d}", .{v.id}) catch "1";
const pid = process.spawnSupervised(v.binary, &.{ id_str, v.mount_prefix }, service_endpoint) orelse {
_ = logging.write("volume-manager: could not spawn the filesystem; retrying\n"); _ = logging.write("volume-manager: could not spawn the filesystem; retrying\n");
armRestart(); armRestart(v);
return; return;
}; };
if (!v.storage.defineRange(pid, v.base_lba, v.block_count)) { if (!dev.channel.defineRange(pid, v.base_lba, v.block_count)) {
_ = logging.write("volume-manager: could not confine the filesystem to its volume; retrying\n"); _ = logging.write("volume-manager: could not confine the filesystem to its volume; retrying\n");
_ = process.kill(pid); _ = process.kill(pid);
armRestart(); armRestart(v);
return; return;
} }
v.filesystem_pid = pid; v.filesystem_pid = pid;
fs_spawn_ns = time.clock(); v.spawn_ns = time.clock();
std.log.info("volume 0x{x} -> {s} (pid {d}), lba {d}, {d} blocks", .{ v.identity.key, v.binary, pid, v.base_lba, v.block_count }); std.log.info("volume 0x{x} -> {s} (pid {d}), lba {d}, {d} blocks", .{ v.identity.key, v.binary, pid, v.base_lba, v.block_count });
} }
/// Schedule a fat restart after backoff; the poll loop performs it once due. /// Schedule a restart for `v` after backoff; the poll loop performs it once due.
fn armRestart() void { fn armRestart(v: *Volume) void {
const delay = if (fs_restarts == 0) backoff_base_ms else backoff_base_ms << @intCast(@min(fs_restarts - 1, 5)); const delay = if (v.restarts == 0) backoff_base_ms else backoff_base_ms << @intCast(@min(v.restarts - 1, 5));
restart_due_ns = time.clock() + delay * 1_000_000; v.restart_due_ns = time.clock() + delay * 1_000_000;
restart_pending = true; v.restart_pending = true;
} }
/// A storage provider just appeared: open its channel, read block 0, parse the /// Compose a volume's mount path (its id-path `/volumes/<id>`, or a volumes.csv
/// volume, and spawn its filesystem. On any failure the channel is closed (so a /// override) into its slot's buffer, and return the slice.
/// present-but-unreadable device does not leak a handle every poll) and `volume` fn composeMountPrefix(slot: usize, identity: partition.Identity) []const u8 {
/// stays null — the next poll retries. A fresh medium gets a fresh supervision var id_buf: [volume_map.id_maximum]u8 = undefined;
/// budget. const id = volume_map.idString(identity, &id_buf);
return volume_map.overrideFor(volume_rules[0..volume_rule_count], id) orelse
(std.fmt.bufPrint(&mount_prefix_bufs[slot], "/volumes/{s}", .{id}) catch "/volumes/unknown");
}
/// A storage device appeared: adopt its channel, probe its partition table, and
/// spawn a filesystem per volume it carries. On any failure the channel is
/// dropped (so a present-but-unreadable device does not leak a handle every poll)
/// and the device stays unadopted — the next poll retries. This increment probes
/// only the first volume (behavior-identical to before); the next step lifts the
/// cap.
fn bringUpVolume() void { fn bringUpVolume() void {
if (!bounce_ready) { if (!bounce_ready) {
bounce = memory.dmaAlloc(512, memory.dma_coherent | memory.dma_shareable) orelse return; bounce = memory.dmaAlloc(512, memory.dma_coherent | memory.dma_shareable) orelse return;
bounce_ready = true; bounce_ready = true;
} }
const opened = openAnyStorage() orelse return; const opened = openAnyStorage() orelse return;
const dev = claimDevice() orelse {
_ = logging.write("volume-manager: device table full; a storage device is left unadopted\n");
_ = ipc.close(opened.device.endpoint);
return;
};
dev.* = .{ .used = true, .device_id = opened.device_id, .channel = opened.device };
const device = opened.device; const device = opened.device;
// Attach the read buffer to THIS device (a no-op without an enforcing IOMMU). // Attach the read buffer to THIS device (a no-op without an enforcing IOMMU).
// The handle is kept, not closed, so it can be re-attached to the next // The handle is kept, not closed, so it can be re-attached after a replug.
// device after a replug.
if (bounce.handle) |handle| { if (bounce.handle) |handle| {
if (!device.attach(handle)) { if (!device.attach(handle)) {
_ = ipc.close(device.endpoint); dropDevice(dev);
return; return;
} }
} }
const geometry = device.geometry() orelse { const geometry = device.geometry() orelse {
_ = ipc.close(device.endpoint); dropDevice(dev);
return; return;
}; };
const ProbeReader = struct { const ProbeReader = struct {
@@ -257,77 +331,91 @@ fn bringUpVolume() void {
}; };
var probe = ProbeReader{ .device = device }; var probe = ProbeReader{ .device = device };
const reader = partition.SectorReader{ .context = &probe, .readFn = ProbeReader.readSector }; const reader = partition.SectorReader{ .context = &probe, .readFn = ProbeReader.readSector };
const found = partition.firstVolume(reader, geometry.block_count) orelse { var found: [maximum_volumes]partition.Volume = undefined;
const n = partition.allVolumes(reader, geometry.block_count, found[0..1]); // cap 1 this step
if (n == 0) {
if (!logged_no_volume) { if (!logged_no_volume) {
_ = logging.write("volume-manager: storage present but no recognizable volume\n"); _ = logging.write("volume-manager: storage present but no recognizable volume\n");
logged_no_volume = true; logged_no_volume = true;
} }
_ = ipc.close(device.endpoint); dropDevice(dev);
return; return;
}; }
// Pick the service binary from the volume's content signature. A signature
// no filesystems.csv row serves goes unserved (logged), like an unbound
// device — the manager does not guess.
const binary = filesystem_map.match(filesystem_rules[0..filesystem_rule_count], found.signature) orelse {
if (!logged_no_volume) {
_ = logging.write("volume-manager: no filesystem serves this volume's content; unserved\n");
logged_no_volume = true;
}
_ = ipc.close(device.endpoint);
return;
};
// The mount path is the volume's identity id (/volumes/<id>), or a
// volumes.csv override pinning it to a chosen path. The id is content-derived,
// so the path is stable and never a port or a label.
var id_buf: [volume_map.id_maximum]u8 = undefined;
const id = volume_map.idString(found.identity, &id_buf);
const mount_prefix = volume_map.overrideFor(volume_rules[0..volume_rule_count], id) orelse
(std.fmt.bufPrint(&mount_prefix_buf, "/volumes/{s}", .{id}) catch "/volumes/unknown");
logged_no_volume = false; logged_no_volume = false;
fs_restarts = 0; var spawned = false;
fs_failed = false; for (found[0..n]) |fv| {
restart_pending = false; // Pick the service binary from the volume's content signature. A signature
volume = .{ .storage = device, .storage_device_id = opened.device_id, .base_lba = found.base_lba, .block_count = found.block_count, .identity = found.identity, .id = volume_id, .binary = binary, .mount_prefix = mount_prefix }; // no filesystems.csv row serves goes unserved (logged), like an unbound
spawnFilesystem(&volume.?); // device — the manager does not guess.
const binary = filesystem_map.match(filesystem_rules[0..filesystem_rule_count], fv.signature) orelse {
_ = logging.write("volume-manager: no filesystem serves this volume's content; unserved\n");
continue;
};
const slot = claimVolumeIndex() orelse {
_ = logging.write("volume-manager: volume table full; a volume is left unserved\n");
break;
};
volumes[slot] = .{
.used = true,
.device_id = dev.device_id,
.base_lba = fv.base_lba,
.block_count = fv.block_count,
.identity = fv.identity,
.id = next_volume_id,
.binary = binary,
.mount_prefix = composeMountPrefix(slot, fv.identity),
};
next_volume_id += 1;
spawnFilesystem(&volumes[slot]);
spawned = true;
}
// The device carried nothing we could serve — drop it so a re-poll retries.
if (!spawned) dropDevice(dev);
} }
/// The storage provider left the device tree (a pulled stick): kill the /// Close a device's channel and free its slot. No volumes are touched (the caller
/// filesystem so its mounts are retired. Retirement is lazy, not an eager /// ensures none remain, or there never were any).
/// death-time sweep — killing the process marks the filesystem's backend fn dropDevice(dev: *StorageDevice) void {
/// endpoint dead, and the VFS router drops each mount that endpoint backed on _ = ipc.close(dev.channel.endpoint);
/// the next path resolution under it (that resolve frees the slot and returns dev.* = .{};
/// not_found). Then drop the now-dead channel and clear the volume; the next }
/// poll that sees storage return re-mounts.
fn removeVolume() void { /// Retire one volume: kill its filesystem so its mounts are retired. Retirement
const v = volume orelse return; /// is lazy, not an eager death-time sweep — killing the process marks the
/// filesystem's backend endpoint dead, and the VFS router drops each mount that
/// endpoint backed on the next path resolution under it (that resolve frees the
/// slot and returns not_found). Then free the volume slot.
fn removeVolumeState(v: *Volume) void {
std.log.info("storage for volume {d} removed; unmounting", .{v.id}); std.log.info("storage for volume {d} removed; unmounting", .{v.id});
if (v.filesystem_pid != 0) _ = process.kill(v.filesystem_pid); if (v.filesystem_pid != 0) _ = process.kill(v.filesystem_pid);
_ = ipc.close(v.storage.endpoint); v.* = .{};
volume = null;
restart_pending = false;
fs_restarts = 0;
fs_failed = false;
} }
/// One poll tick. Removal is checked FIRST and supersedes a pending restart: if /// A storage device left the tree (a pulled stick): retire every volume it served
/// the device is gone there is nothing to restart fat onto, and respawning it /// and drop its channel. One removal path, whether the device is pulled cleanly
/// against the dead channel would just churn until the crash cap. Only once the /// or vanishes.
/// device is confirmed present does a due restart fire. fn removeDevice(dev: *StorageDevice) void {
fn pollTick() void { for (&volumes) |*v| {
if (volume) |v| { if (v.used and v.device_id == dev.device_id) removeVolumeState(v);
// Serving: watch for the specific device leaving (a pulled stick).
if (!isDevicePresent(v.storage_device_id)) {
removeVolume();
return;
}
if (restart_pending and time.clock() >= restart_due_ns) {
restart_pending = false;
spawnFilesystem(&volume.?);
}
} else {
// Idle: try to bring a present storage device up.
bringUpVolume();
} }
dropDevice(dev);
}
/// One poll tick. Device removal is reconciled FIRST and supersedes a pending
/// restart: a volume whose device left is retired before its restart could fire,
/// so nothing respawns against a dead channel. Then due restarts fire for present
/// volumes; then, if no device is adopted, a present device is brought up.
fn pollTick() void {
for (&devices) |*dev| {
if (dev.used and !isDevicePresent(dev.device_id)) removeDevice(dev);
}
for (&volumes) |*v| {
if (v.used and v.restart_pending and time.clock() >= v.restart_due_ns) {
v.restart_pending = false;
spawnFilesystem(v);
}
}
if (!anyDeviceUsed()) bringUpVolume();
} }
/// A filesystem announces itself for the volume it was spawned to serve. Reply /// A filesystem announces itself for the volume it was spawned to serve. Reply
@@ -335,26 +423,26 @@ fn pollTick() void {
/// badge) as the call's returned capability. No channel means the volume is not /// badge) as the call's returned capability. No channel means the volume is not
/// ready — the filesystem retries. /// ready — the filesystem retries.
fn onHello(_: void, invocation: Invocation(volume_manager_protocol.Hello), _: Answer(void)) isize { fn onHello(_: void, invocation: Invocation(volume_manager_protocol.Hello), _: Answer(void)) isize {
const v = volume orelse return 0; // not probed yet — retryable, no cap const v = volumeById(invocation.target) orelse return 0; // not probed yet — retryable, no cap
if (invocation.target != v.id) return 0; // unknown volume — retryable
if (invocation.sender != v.filesystem_pid) { if (invocation.sender != v.filesystem_pid) {
// Not the filesystem we spawned for this volume. Refuse: only the // Not the filesystem we spawned for this volume. Refuse: only the confined
// confined filesystem gets the channel. // filesystem gets the channel.
std.log.info("refused hello for volume {d} from process {d}", .{ invocation.target, invocation.sender }); std.log.info("refused hello for volume {d} from process {d}", .{ invocation.target, invocation.sender });
return -envelope.EPERM; return -envelope.EPERM;
} }
service.replyWithCapability(v.storage.endpoint); const dev = deviceById(v.device_id) orelse return 0; // its device left — retryable
service.replyWithCapability(dev.channel.endpoint);
std.log.info("handed volume {d} to pid {d}", .{ v.id, invocation.sender }); std.log.info("handed volume {d} to pid {d}", .{ v.id, invocation.sender });
return 0; return 0;
} }
/// Answer a `volumes` query with the mounted volume's descriptor — its id (its /// Answer a `volumes` query with a mounted volume's descriptor — its id (its
/// mount path is /volumes/<id> unless overridden), its actual mount path, and /// mount path is /volumes/<id> unless overridden), its actual mount path, and its
/// its display label. This is how a shell or file manager reads a volume's /// display label. Software keys on the id; a UI shows the label. Returns the first
/// friendly name: software keys on the id, a UI shows the label. An empty reply /// mounted volume for now; a full enumerate is a later refinement. Empty reply
/// means no volume is mounted. /// means no volume is mounted.
fn onVolumes(_: void, _: Invocation(volume_manager_protocol.Volumes), answer: Answer(void)) isize { fn onVolumes(_: void, _: Invocation(volume_manager_protocol.Volumes), answer: Answer(void)) isize {
const v = volume orelse return 0; const v = firstUsedVolume() orelse return 0;
var id_buf: [volume_map.id_maximum]u8 = undefined; var id_buf: [volume_map.id_maximum]u8 = undefined;
const info = volume_manager_protocol.VolumeInfo{ const info = volume_manager_protocol.VolumeInfo{
.id = volume_map.idString(v.identity, &id_buf), .id = volume_map.idString(v.identity, &id_buf),
@@ -381,9 +469,9 @@ fn readConfig(path: []const u8, buf: []u8) usize {
defer file.close(); defer file.close();
var used: usize = 0; var used: usize = 0;
while (used < buf.len) { while (used < buf.len) {
const n = file.read(buf[used..]) orelse break; const nn = file.read(buf[used..]) orelse break;
if (n == 0) break; if (nn == 0) break;
used += n; used += nn;
} }
return used; return used;
} }
@@ -427,23 +515,22 @@ fn onNotification(badge: u64) void {
// reclaimed by the driver on the same death; the respawn confines afresh. // reclaimed by the driver on the same death; the respawn confines afresh.
if (got.isChildExit()) { if (got.isChildExit()) {
const dead = got.childProcessId(); const dead = got.childProcessId();
const v = &(volume orelse return); const v = volumeByPid(dead) orelse return;
if (v.filesystem_pid != dead) return;
v.filesystem_pid = 0; v.filesystem_pid = 0;
const reason = process.exitReason(dead) orelse .fault; const reason = process.exitReason(dead) orelse .fault;
if (reason == .exited) { if (reason == .exited) {
std.log.info("filesystem for volume {d} exited cleanly; not restarting", .{v.id}); std.log.info("filesystem for volume {d} exited cleanly; not restarting", .{v.id});
return; return;
} }
const alive = time.clock() -| fs_spawn_ns; const alive = time.clock() -| v.spawn_ns;
fs_restarts = if (alive < fast_death_ns) fs_restarts + 1 else 1; v.restarts = if (alive < fast_death_ns) v.restarts + 1 else 1;
if (fs_restarts >= crash_loop_cap) { if (v.restarts >= crash_loop_cap) {
fs_failed = true; v.failed = true;
std.log.info("filesystem for volume {d} is failing repeatedly; giving up", .{v.id}); std.log.info("filesystem for volume {d} is failing repeatedly; giving up", .{v.id});
return; return;
} }
std.log.info("filesystem for volume {d} died ({s}); restarting", .{ v.id, @tagName(reason) }); std.log.info("filesystem for volume {d} died ({s}); restarting", .{ v.id, @tagName(reason) });
armRestart(); armRestart(v);
} }
} }