init: data-driven boot service list via /etc/init.csv
Replace init's hardcoded boot_services array with an authoritative, human-readable service list read at boot, mirroring /etc/devices.csv. Each row is a service binary path followed by its argv; startup order is file order, shutdown the reverse. There is no hardcoded fallback — a missing file starts nothing (the no-ramdisk isolation behavior). - library/csv: shared CSV helpers (comment strip, field iteration) with unit tests; device-registry is refactored onto them so both /etc/*.csv files parse through one place. - init reads /etc/init.csv into fixed-max static tables (the same pattern as the device registry) and passes each row's argv straight to spawnSupervised. This also makes boot-time modes (e.g. device-manager test-usb-restart) expressible as data rather than hardcoded. - Diagnose mode (-Ddiagnose omits the display stack) becomes build-time file selection between etc/init.csv and etc/init-diagnose.csv, so init carries no comptime service logic; the diagnose build option is dropped from init. - build.zig: csv module wired; /etc/init.csv bundled into the initrd; the device-registry tests move to a dedicated block since they now import csv. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KJqSiLLchDUUCoXn5jsiwd
This commit is contained in:
@@ -25,42 +25,88 @@ const memory = @import("memory");
|
||||
const logging = @import("logging");
|
||||
const power_protocol = @import("power-protocol");
|
||||
const build_options = @import("build_options");
|
||||
const fs = @import("file-system");
|
||||
const csv = @import("csv");
|
||||
|
||||
/// The system services init brings up at boot, in order, by binary path. This is
|
||||
/// init's policy — the microkernel keeps such choices in user space, not the
|
||||
/// kernel. Drivers are absent on purpose: the device manager owns those. (A
|
||||
/// future init reads this from a manifest under /system/services instead of a
|
||||
/// hardcoded list.)
|
||||
const boot_services = if (build_options.diagnose) [_][]const u8{
|
||||
// The diagnose boot: no display service, so the kernel's on-screen boot
|
||||
// transcript is never suppressed — the timestamped timeline (USB bring-up,
|
||||
// storage, logger) stays readable on real hardware with no serial.
|
||||
"/system/services/input",
|
||||
"/system/services/device-manager",
|
||||
"/system/services/fat",
|
||||
"/system/services/logger",
|
||||
} else [_][]const u8{
|
||||
"/system/services/input",
|
||||
"/system/services/device-manager",
|
||||
"/system/services/fat",
|
||||
"/system/services/display",
|
||||
"/system/services/display-demo",
|
||||
// Last: at shutdown children stop in reverse order, so the logger goes down
|
||||
// FIRST — its final drain still has the fat server (and the whole storage
|
||||
// chain) alive underneath it.
|
||||
"/system/services/logger",
|
||||
/// 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
|
||||
/// 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
|
||||
/// 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
|
||||
/// and argv, both slices into `init_csv` (held for the life of the process).
|
||||
const Service = struct {
|
||||
path: []const u8 = "",
|
||||
arg_buffer: [max_service_args][]const u8 = undefined,
|
||||
arg_count: usize = 0,
|
||||
fn arguments(self: *const Service) []const []const u8 {
|
||||
return self.arg_buffer[0..self.arg_count];
|
||||
}
|
||||
};
|
||||
|
||||
/// The live process id of each boot service (0 = not running), indexed by its position
|
||||
/// in `boot_services`, plus how many times init has restarted it. init supervises these:
|
||||
/// it spawns them against `supervision_endpoint` and, on a child's death, restarts it (up
|
||||
/// to `maximum_restarts`) — the reincarnation half of resilience (docs/resilience.md), the
|
||||
/// service-level counterpart to the device manager's driver restarts.
|
||||
var child_ids: [boot_services.len]u32 = .{0} ** boot_services.len;
|
||||
var restart_counts: [boot_services.len]u32 = .{0} ** boot_services.len;
|
||||
/// The `/etc/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;
|
||||
|
||||
/// The live process id of each service (0 = not running) and its restart count,
|
||||
/// indexed by position in `services`. init supervises these: it spawns them against
|
||||
/// `supervision_endpoint` and, on a child's death, restarts it (up to
|
||||
/// `maximum_restarts`) — the reincarnation half of resilience (docs/resilience.md),
|
||||
/// the service-level counterpart to the device manager's driver restarts.
|
||||
var child_ids: [max_services]u32 = .{0} ** max_services;
|
||||
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
|
||||
/// 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
|
||||
/// 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");
|
||||
return;
|
||||
};
|
||||
defer file.close();
|
||||
var used: usize = 0;
|
||||
while (used < init_csv.len) {
|
||||
const n = file.read(init_csv[used..]) orelse break;
|
||||
if (n == 0) break;
|
||||
used += n;
|
||||
}
|
||||
var lines = std.mem.splitScalar(u8, init_csv[0..used], '\n');
|
||||
while (lines.next()) |line| {
|
||||
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");
|
||||
break;
|
||||
}
|
||||
var it = csv.fields(body);
|
||||
const path = it.next() orelse continue;
|
||||
if (path.len == 0) continue;
|
||||
var service: Service = .{ .path = path };
|
||||
while (it.next()) |argument| {
|
||||
if (argument.len == 0) continue; // padding, or a trailing comma
|
||||
if (service.arg_count >= max_service_args) break;
|
||||
service.arg_buffer[service.arg_count] = argument;
|
||||
service.arg_count += 1;
|
||||
}
|
||||
services[service_count] = service;
|
||||
service_count += 1;
|
||||
}
|
||||
}
|
||||
|
||||
/// Give up restarting a service after this many crashes — a crash-loop cap, so a service
|
||||
/// that faults immediately on every spawn doesn't respawn forever.
|
||||
const maximum_restarts = 3;
|
||||
@@ -89,11 +135,12 @@ pub fn main() void {
|
||||
};
|
||||
_ = process.bindSignals(supervision_endpoint);
|
||||
|
||||
// Bring up the boot services, supervised so init can stop them cleanly.
|
||||
// Best-effort and silent: each service announces its own readiness, and in
|
||||
// an isolation test with no initial-ramdisk the spawns simply no-op.
|
||||
for (boot_services, 0..) |service, i| {
|
||||
if (process.spawnSupervised(service, &.{}, supervision_endpoint)) |id| child_ids[i] = id;
|
||||
// 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.
|
||||
loadServices();
|
||||
for (services[0..service_count], 0..) |*service, i| {
|
||||
if (process.spawnSupervised(service.path, service.arguments(), supervision_endpoint)) |id| child_ids[i] = id;
|
||||
}
|
||||
|
||||
// Subscribe to power events (retry: the power service registers well after
|
||||
@@ -141,22 +188,22 @@ pub fn main() void {
|
||||
/// iron rule 1); init only decides whether to bring it back.
|
||||
fn restartChild(id: u32) void {
|
||||
if (shutting_down) return; // deaths during the stop sequence are expected, not crashes
|
||||
for (boot_services, 0..) |service, i| {
|
||||
for (services[0..service_count], 0..) |*service, i| {
|
||||
if (child_ids[i] != id) continue;
|
||||
child_ids[i] = 0;
|
||||
// An unknown reason (the record aged out) is treated as a crash worth restarting.
|
||||
const reason = process.exitReason(id) orelse .fault;
|
||||
if (reason == .exited) {
|
||||
std.log.info("{s} exited cleanly; not restarting", .{service});
|
||||
std.log.info("{s} exited cleanly; not restarting", .{service.path});
|
||||
return;
|
||||
}
|
||||
restart_counts[i] += 1;
|
||||
if (restart_counts[i] > maximum_restarts) {
|
||||
std.log.info("{s} keeps crashing; giving up after {d} restarts", .{ service, maximum_restarts });
|
||||
std.log.info("{s} keeps crashing; giving up after {d} restarts", .{ service.path, maximum_restarts });
|
||||
return;
|
||||
}
|
||||
std.log.info("{s} died ({s}); restarting ({d}/{d})", .{ service, @tagName(reason), restart_counts[i], maximum_restarts });
|
||||
if (process.spawnSupervised(service, &.{}, supervision_endpoint)) |new_id| child_ids[i] = new_id;
|
||||
std.log.info("{s} died ({s}); restarting ({d}/{d})", .{ service.path, @tagName(reason), restart_counts[i], maximum_restarts });
|
||||
if (process.spawnSupervised(service.path, service.arguments(), supervision_endpoint)) |new_id| child_ids[i] = new_id;
|
||||
return;
|
||||
}
|
||||
// An untracked child (e.g. the log-flush one-shot): nothing to restart.
|
||||
@@ -190,7 +237,7 @@ fn shutDown() void {
|
||||
// Log persistence is the logger service's job: it is the LAST boot service,
|
||||
// so the reverse-order stop below terminates it first and its final drain
|
||||
// runs while the whole storage chain is still alive.
|
||||
var i = boot_services.len;
|
||||
var i = service_count;
|
||||
while (i > 0) {
|
||||
i -= 1;
|
||||
if (child_ids[i] != 0) process.stop(child_ids[i], 2000, supervision_endpoint);
|
||||
|
||||
Reference in New Issue
Block a user