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:
@@ -284,12 +284,18 @@ pub fn build(b: *std.Build) void {
|
||||
const pci_class_module = b.addModule("pci-class", .{
|
||||
.root_source_file = b.path("library/device/pci/pci-class.zig"),
|
||||
});
|
||||
// Shared CSV helpers (comment stripping, field iteration) for the /etc/*.csv
|
||||
// config files — the device registry and the init service list both parse them.
|
||||
const csv_module = b.addModule("csv", .{
|
||||
.root_source_file = b.path("library/csv/csv.zig"),
|
||||
});
|
||||
// The device registry: parse /etc/devices.csv into match rules and bind a
|
||||
// reported device to a driver — the data-driven, authoritative replacement for
|
||||
// the manager's hand-written switch tables. Pure logic (no hardware, no
|
||||
// syscalls), so it unit-tests with plain `zig test`; the manager imports it.
|
||||
// syscalls), so it unit-tests on the host; the manager imports it.
|
||||
const device_registry_module = b.addModule("device-registry", .{
|
||||
.root_source_file = b.path("library/device/registry/device-registry.zig"),
|
||||
.imports = &.{.{ .name = "csv", .module = csv_module }},
|
||||
});
|
||||
// ACPI/PnP hardware-ID (_HID) names — the flat analog of pci-class for acpi_device
|
||||
// nodes. Also shared reference data.
|
||||
@@ -646,6 +652,8 @@ pub fn build(b: *std.Build) void {
|
||||
// started in ring 3 by the kernel's user-ELF loader.
|
||||
const init_exe = addUserBinary(b, kernel_target, &default_imports, "init", "system/services/init/init.zig");
|
||||
programModule(init_exe).addImport("power-protocol", power_protocol_module);
|
||||
// init parses its boot service list from /etc/init.csv with the shared csv helpers.
|
||||
programModule(init_exe).addImport("csv", csv_module);
|
||||
// init reads the same `serial` flag the kernel does: its liveness heartbeat is a
|
||||
// serial/test-build diagnostic (the QEMU harness's init tests assert on it, and
|
||||
// -Dserial images emit it), so a flashable image runs a purely event-driven PID 1
|
||||
@@ -653,7 +661,8 @@ pub fn build(b: *std.Build) void {
|
||||
// the heartbeat stays present under test.
|
||||
const init_options = b.addOptions();
|
||||
init_options.addOption(bool, "serial", serial);
|
||||
init_options.addOption(bool, "diagnose", diagnose);
|
||||
// Which services init starts is no longer a comptime option: it reads /etc/init.csv,
|
||||
// and -Ddiagnose selects which init.csv is bundled (see the `bundled` list below).
|
||||
programModule(init_exe).addImport("build_options", init_options.createModule());
|
||||
|
||||
// --- the rest of the boot tree: /system services and drivers, /test fixtures ---
|
||||
@@ -760,6 +769,10 @@ pub fn build(b: *std.Build) void {
|
||||
// structure is the single source of truth. Entry names (and hence argv[0] and
|
||||
// task names) are these paths with a leading slash. Test fixtures mirror their
|
||||
// repo home: test/system/services/<name> in the source tree IS the boot path.
|
||||
// init's boot service list is data (/etc/init.csv). -Ddiagnose selects the
|
||||
// variant that omits the display stack (so the kernel's boot transcript stays
|
||||
// on screen); both are bundled at the same /etc/init.csv path.
|
||||
const init_csv_source = if (diagnose) "etc/init-diagnose.csv" else "etc/init.csv";
|
||||
const bundled = [_]BundledBinary{
|
||||
.{ .path = "system/services/init", .binary = init_exe.getEmittedBin() },
|
||||
.{ .path = "system/services/fat", .binary = fat_exe.getEmittedBin() },
|
||||
@@ -774,6 +787,8 @@ pub fn build(b: *std.Build) void {
|
||||
// initrd tree (system/kernel/vfs.zig setInitialRamdisk), so the manager can
|
||||
// fs.open("/etc/devices.csv") with no filesystem service running.
|
||||
.{ .path = "etc/devices.csv", .binary = b.path("etc/devices.csv") },
|
||||
// init's service list, likewise read from the kernel-served initrd /etc.
|
||||
.{ .path = "etc/init.csv", .binary = b.path(init_csv_source) },
|
||||
.{ .path = "system/drivers/ps2-bus", .binary = ps2_bus_exe.getEmittedBin() },
|
||||
.{ .path = "system/drivers/ps2-keyboard", .binary = ps2_keyboard_exe.getEmittedBin() },
|
||||
.{ .path = "system/drivers/ps2-mouse", .binary = ps2_mouse_exe.getEmittedBin() },
|
||||
@@ -1088,7 +1103,7 @@ pub fn build(b: *std.Build) void {
|
||||
"library/device/acpi/aml/aml.zig", // AML parse + interpret, incl. Notify dispatch (M21)
|
||||
"library/device/usb/usb-abi.zig", // wire sizes + bit packings + set-up packet encodings
|
||||
"library/device/usb/usb-ids.zig", // class/subclass/protocol code assignments
|
||||
"library/device/registry/device-registry.zig", // /etc/devices.csv parse + most-specific driver match
|
||||
"library/csv/csv.zig", // shared /etc/*.csv comment-strip + field-split helpers
|
||||
"library/device/mmio/mmio.zig", // barriers assemble + registers round-trip
|
||||
"system/drivers/ps2-bus/scancode.zig", // set-2 decode + keyboard state machine
|
||||
"system/drivers/ps2-bus/mouse-packet.zig", // 3-byte mouse packet assembly
|
||||
@@ -1113,6 +1128,19 @@ pub fn build(b: *std.Build) void {
|
||||
test_step.dependOn(&b.addRunArtifact(mod_tests).step);
|
||||
}
|
||||
|
||||
// The device registry imports the shared `csv` module, so its tests need that
|
||||
// import wired and don't fit the plain loop above. These prove the /etc/devices.csv
|
||||
// parse + most-specific driver match (incl. virtio 1AF4:1050 beating a class rule).
|
||||
const device_registry_tests = b.addTest(.{
|
||||
.root_module = b.createModule(.{
|
||||
.root_source_file = b.path("library/device/registry/device-registry.zig"),
|
||||
.target = target,
|
||||
.optimize = optimize,
|
||||
.imports = &.{.{ .name = "csv", .module = csv_module }},
|
||||
}),
|
||||
});
|
||||
test_step.dependOn(&b.addRunArtifact(device_registry_tests).step);
|
||||
|
||||
// The xkeyboard-config keymap tests need its generated `layouts` import wired, so they
|
||||
// don't fit the plain loop above. Its keycode->character assertions are the end-to-end
|
||||
// proof that the xkb-data -> generator -> Zig-lookup pipeline is correct.
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
# /etc/init.csv — diagnose variant (-Ddiagnose), bundled at /etc/init.csv.
|
||||
#
|
||||
# The display stack (display, display-demo) is omitted so the kernel's timestamped
|
||||
# on-screen boot transcript is never suppressed — the bring-up timeline (USB,
|
||||
# storage, logger) stays readable on real hardware with no serial. See etc/init.csv
|
||||
# for the format; this file must otherwise track it.
|
||||
#
|
||||
# service args...
|
||||
/system/services/input
|
||||
/system/services/device-manager
|
||||
/system/services/fat
|
||||
/system/services/logger
|
||||
|
@@ -0,0 +1,20 @@
|
||||
# /etc/init.csv — the services init (PID 1) starts at boot, in order.
|
||||
#
|
||||
# init reads this at startup and spawns each service supervised (restarting it on
|
||||
# a crash, up to a cap). Startup order is top->bottom; shutdown is the reverse, so
|
||||
# the logger (last) goes down first and its final drain still has the fat server
|
||||
# and the whole storage chain alive underneath it. It is AUTHORITATIVE — there is
|
||||
# no hardcoded fallback list; a missing file means no services are started.
|
||||
#
|
||||
# '#' starts a comment (whole-line or trailing); blank lines are ignored. The
|
||||
# first field is the service binary path; any fields after it are the service's
|
||||
# argv. Drivers are absent on purpose — the device manager discovers hardware and
|
||||
# spawns those (see /etc/devices.csv).
|
||||
#
|
||||
# service args...
|
||||
/system/services/input
|
||||
/system/services/device-manager
|
||||
/system/services/fat
|
||||
/system/services/display
|
||||
/system/services/display-demo
|
||||
/system/services/logger
|
||||
|
@@ -0,0 +1,56 @@
|
||||
//! Minimal CSV helpers shared by the `/etc/*.csv` config files — the device
|
||||
//! registry (`/etc/devices.csv`) and the init service list (`/etc/init.csv`).
|
||||
//! Freestanding, no allocator: returned fields are slices into the source line,
|
||||
//! so the source must outlive them. `#` starts a comment (whole-line or trailing);
|
||||
//! whitespace around a field is trimmed, so columns may be padded for alignment.
|
||||
|
||||
const std = @import("std");
|
||||
|
||||
/// Strip a trailing `#` comment and surrounding whitespace from one raw line.
|
||||
/// A blank or comment-only line returns "" (length 0) — the caller's skip signal.
|
||||
pub fn stripComment(raw: []const u8) []const u8 {
|
||||
const body = if (std.mem.indexOfScalar(u8, raw, '#')) |hash| raw[0..hash] else raw;
|
||||
return std.mem.trim(u8, body, " \t\r\n");
|
||||
}
|
||||
|
||||
/// Iterate the comma-separated fields of a line body, each trimmed of spaces and
|
||||
/// tabs. Build it from a `stripComment`ed body.
|
||||
pub const Fields = struct {
|
||||
inner: std.mem.SplitIterator(u8, .scalar),
|
||||
|
||||
/// The next field, trimmed, or null when the row is exhausted.
|
||||
pub fn next(self: *Fields) ?[]const u8 {
|
||||
const field = self.inner.next() orelse return null;
|
||||
return std.mem.trim(u8, field, " \t");
|
||||
}
|
||||
};
|
||||
|
||||
pub fn fields(body: []const u8) Fields {
|
||||
return .{ .inner = std.mem.splitScalar(u8, body, ',') };
|
||||
}
|
||||
|
||||
// --- tests -------------------------------------------------------------------
|
||||
|
||||
const testing = std.testing;
|
||||
|
||||
test "stripComment trims and drops comments" {
|
||||
try testing.expectEqualStrings("a, b", stripComment(" a, b # trailing\r\n"));
|
||||
try testing.expectEqualStrings("", stripComment(" # whole-line comment"));
|
||||
try testing.expectEqualStrings("", stripComment(" \t "));
|
||||
try testing.expectEqualStrings("x", stripComment("x"));
|
||||
}
|
||||
|
||||
test "fields splits and trims each column" {
|
||||
var it = fields(stripComment("pci, 03 , 80 , /system/drivers/x # note"));
|
||||
try testing.expectEqualStrings("pci", it.next().?);
|
||||
try testing.expectEqualStrings("03", it.next().?);
|
||||
try testing.expectEqualStrings("80", it.next().?);
|
||||
try testing.expectEqualStrings("/system/drivers/x", it.next().?);
|
||||
try testing.expect(it.next() == null);
|
||||
}
|
||||
|
||||
test "a single field yields one column then null" {
|
||||
var it = fields(stripComment("/system/services/input"));
|
||||
try testing.expectEqualStrings("/system/services/input", it.next().?);
|
||||
try testing.expect(it.next() == null);
|
||||
}
|
||||
@@ -23,6 +23,7 @@
|
||||
//! (e.g. "PNP0303") with the triple left blank. `driver` is a full ramdisk path.
|
||||
|
||||
const std = @import("std");
|
||||
const csv = @import("csv");
|
||||
|
||||
/// Which bus a rule or a reported device belongs to. `unknown` is what an
|
||||
/// unrecognised `bus` token parses to — such a rule never matches (its bus
|
||||
@@ -168,12 +169,6 @@ pub const ParseResult = struct {
|
||||
truncated: bool,
|
||||
};
|
||||
|
||||
/// Strip a trailing `#` comment and surrounding whitespace from one raw line.
|
||||
fn stripComment(raw: []const u8) []const u8 {
|
||||
const body = if (std.mem.indexOfScalar(u8, raw, '#')) |hash| raw[0..hash] else raw;
|
||||
return std.mem.trim(u8, body, " \t\r\n");
|
||||
}
|
||||
|
||||
/// Parse one hex field into `T`, honouring `*`/empty as a wildcard (`null`) and
|
||||
/// an optional `0x` prefix. Returns an error only for a genuinely unparsable
|
||||
/// non-wildcard token, so the caller can mark the whole line malformed.
|
||||
@@ -197,36 +192,36 @@ fn parseStringField(field: []const u8) ?[]const u8 {
|
||||
/// Classify and (if a rule) parse one line. Split out from `parse` so it can be
|
||||
/// unit-tested directly. `line` is the raw line including no newline.
|
||||
fn parseLine(line: []const u8) Line {
|
||||
const body = stripComment(line);
|
||||
const body = csv.stripComment(line);
|
||||
if (body.len == 0) return .ignorable;
|
||||
|
||||
// Nine comma-separated fields: bus, base, class, prog_if, vendor, device,
|
||||
// subsystem, hid, driver.
|
||||
var fields: [9][]const u8 = undefined;
|
||||
// Nine comma-separated fields (csv.fields trims each): bus, base, class,
|
||||
// prog_if, vendor, device, subsystem, hid, driver.
|
||||
var cols: [9][]const u8 = undefined;
|
||||
var count: usize = 0;
|
||||
var it = std.mem.splitScalar(u8, body, ',');
|
||||
var it = csv.fields(body);
|
||||
while (it.next()) |field| {
|
||||
if (count >= fields.len) return .malformed; // too many columns
|
||||
fields[count] = field;
|
||||
if (count >= cols.len) return .malformed; // too many columns
|
||||
cols[count] = field;
|
||||
count += 1;
|
||||
}
|
||||
if (count != fields.len) return .malformed; // too few columns
|
||||
if (count != cols.len) return .malformed; // too few columns
|
||||
|
||||
const bus = Bus.fromToken(std.mem.trim(u8, fields[0], " \t"));
|
||||
const bus = Bus.fromToken(cols[0]);
|
||||
if (bus == .unknown) return .malformed;
|
||||
|
||||
const driver = std.mem.trim(u8, fields[8], " \t");
|
||||
const driver = cols[8];
|
||||
if (driver.len == 0) return .malformed;
|
||||
|
||||
return .{ .rule = .{
|
||||
.bus = bus,
|
||||
.base = parseHexField(u8, fields[1]) catch return .malformed,
|
||||
.subclass = parseHexField(u8, fields[2]) catch return .malformed,
|
||||
.prog_if = parseHexField(u8, fields[3]) catch return .malformed,
|
||||
.vendor = parseHexField(u16, fields[4]) catch return .malformed,
|
||||
.device = parseHexField(u16, fields[5]) catch return .malformed,
|
||||
.subsystem = parseHexField(u32, fields[6]) catch return .malformed,
|
||||
.hid = parseStringField(fields[7]),
|
||||
.base = parseHexField(u8, cols[1]) catch return .malformed,
|
||||
.subclass = parseHexField(u8, cols[2]) catch return .malformed,
|
||||
.prog_if = parseHexField(u8, cols[3]) catch return .malformed,
|
||||
.vendor = parseHexField(u16, cols[4]) catch return .malformed,
|
||||
.device = parseHexField(u16, cols[5]) catch return .malformed,
|
||||
.subsystem = parseHexField(u32, cols[6]) catch return .malformed,
|
||||
.hid = parseStringField(cols[7]),
|
||||
.driver = driver,
|
||||
} };
|
||||
}
|
||||
@@ -262,13 +257,13 @@ const testing = std.testing;
|
||||
// the specific one must win. And a plain VGA adapter still falls to the generic
|
||||
// rule. This is the whole point of widening the ABI to carry vendor/device.
|
||||
test "virtio device rule beats the generic display rule" {
|
||||
const csv =
|
||||
const text =
|
||||
\\# bus, base, class, prog_if, vendor, device, subsystem, hid, driver
|
||||
\\pci, 03, 00, 00, *, *, *, *, /system/drivers/display
|
||||
\\pci, 03, 80, *, 1AF4, 1050, *, *, /system/drivers/virtio-gpu
|
||||
;
|
||||
var rules: [8]Rule = undefined;
|
||||
const parsed = parse(csv, &rules);
|
||||
const parsed = parse(text, &rules);
|
||||
try testing.expectEqual(@as(usize, 2), parsed.count);
|
||||
try testing.expectEqual(@as(usize, 0), parsed.malformed);
|
||||
|
||||
@@ -289,9 +284,9 @@ test "virtio device rule beats the generic display rule" {
|
||||
}
|
||||
|
||||
test "no matching row leaves the device unbound" {
|
||||
const csv = "pci, 0C, 03, 30, *, *, *, *, /system/drivers/usb-xhci-bus\n";
|
||||
const text = "pci, 0C, 03, 30, *, *, *, *, /system/drivers/usb-xhci-bus\n";
|
||||
var rules: [8]Rule = undefined;
|
||||
const parsed = parse(csv, &rules);
|
||||
const parsed = parse(text, &rules);
|
||||
try testing.expectEqual(@as(usize, 1), parsed.count);
|
||||
|
||||
// An AHCI controller (mass storage / SATA / AHCI) has no row — unbound.
|
||||
@@ -302,12 +297,12 @@ test "no matching row leaves the device unbound" {
|
||||
}
|
||||
|
||||
test "acpi rows match on hid" {
|
||||
const csv =
|
||||
const text =
|
||||
\\acpi, *, *, *, *, *, *, PNP0303, /system/drivers/ps2-bus
|
||||
\\acpi, *, *, *, *, *, *, PNP0F13, /system/drivers/ps2-bus
|
||||
;
|
||||
var rules: [8]Rule = undefined;
|
||||
const parsed = parse(csv, &rules);
|
||||
const parsed = parse(text, &rules);
|
||||
try testing.expectEqual(@as(usize, 2), parsed.count);
|
||||
|
||||
const keyboard = matchDriver(rules[0..parsed.count], .{ .bus = .acpi, .hid = "PNP0303" }).?;
|
||||
@@ -317,12 +312,12 @@ test "acpi rows match on hid" {
|
||||
}
|
||||
|
||||
test "equally specific rules flag ambiguity" {
|
||||
const csv =
|
||||
const text =
|
||||
\\pci, 03, 00, 00, *, *, *, *, /system/drivers/display-a
|
||||
\\pci, 03, 00, 00, *, *, *, *, /system/drivers/display-b
|
||||
;
|
||||
var rules: [8]Rule = undefined;
|
||||
const parsed = parse(csv, &rules);
|
||||
const parsed = parse(text, &rules);
|
||||
const hit = matchDriver(rules[0..parsed.count], .{
|
||||
.bus = .pci, .base = 0x03, .subclass = 0x00, .prog_if = 0x00,
|
||||
}).?;
|
||||
@@ -331,7 +326,7 @@ test "equally specific rules flag ambiguity" {
|
||||
}
|
||||
|
||||
test "comments, blanks, and malformed lines" {
|
||||
const csv =
|
||||
const text =
|
||||
\\# a header comment
|
||||
\\
|
||||
\\pci, 0C, 03, 30, *, *, *, *, /system/drivers/usb-xhci-bus # trailing comment
|
||||
@@ -340,7 +335,7 @@ test "comments, blanks, and malformed lines" {
|
||||
\\bogus-bus, *, *, *, *, *, *, *, /system/drivers/x
|
||||
;
|
||||
var rules: [8]Rule = undefined;
|
||||
const parsed = parse(csv, &rules);
|
||||
const parsed = parse(text, &rules);
|
||||
try testing.expectEqual(@as(usize, 1), parsed.count); // only the xhci row is valid
|
||||
try testing.expectEqual(@as(usize, 3), parsed.malformed); // bad hex, empty driver, bad bus
|
||||
try testing.expectEqualStrings("/system/drivers/usb-xhci-bus", rules[0].driver);
|
||||
|
||||
@@ -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