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:
2026-07-26 17:13:51 +01:00
co-authored by Claude Opus 4.8
parent 203528c8a7
commit 081ba1d74e
6 changed files with 235 additions and 77 deletions
+31 -3
View File
@@ -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.
+12
View File
@@ -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
1 # /etc/init.csv — diagnose variant (-Ddiagnose), bundled at /etc/init.csv.
2 #
3 # The display stack (display, display-demo) is omitted so the kernel's timestamped
4 # on-screen boot transcript is never suppressed — the bring-up timeline (USB,
5 # storage, logger) stays readable on real hardware with no serial. See etc/init.csv
6 # for the format; this file must otherwise track it.
7 #
8 # service args...
9 /system/services/input
10 /system/services/device-manager
11 /system/services/fat
12 /system/services/logger
+20
View File
@@ -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
1 # /etc/init.csv — the services init (PID 1) starts at boot, in order.
2 #
3 # init reads this at startup and spawns each service supervised (restarting it on
4 # a crash, up to a cap). Startup order is top->bottom; shutdown is the reverse, so
5 # the logger (last) goes down first and its final drain still has the fat server
6 # and the whole storage chain alive underneath it. It is AUTHORITATIVE — there is
7 # no hardcoded fallback list; a missing file means no services are started.
8 #
9 # '#' starts a comment (whole-line or trailing); blank lines are ignored. The
10 # first field is the service binary path; any fields after it are the service's
11 # argv. Drivers are absent on purpose — the device manager discovers hardware and
12 # spawns those (see /etc/devices.csv).
13 #
14 # service args...
15 /system/services/input
16 /system/services/device-manager
17 /system/services/fat
18 /system/services/display
19 /system/services/display-demo
20 /system/services/logger
+56
View File
@@ -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);
}
+28 -33
View File
@@ -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);
+88 -41
View File
@@ -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);