From 081ba1d74e6e2b6f2f1f537d87471c139d31ad3d Mon Sep 17 00:00:00 2001 From: Daniel Samson Date: Sun, 26 Jul 2026 17:13:51 +0100 Subject: [PATCH] init: data-driven boot service list via /etc/init.csv MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) Claude-Session: https://claude.ai/code/session_01KJqSiLLchDUUCoXn5jsiwd --- build.zig | 34 +++++- etc/init-diagnose.csv | 12 ++ etc/init.csv | 20 +++ library/csv/csv.zig | 56 +++++++++ library/device/registry/device-registry.zig | 61 +++++---- system/services/init/init.zig | 129 +++++++++++++------- 6 files changed, 235 insertions(+), 77 deletions(-) create mode 100644 etc/init-diagnose.csv create mode 100644 etc/init.csv create mode 100644 library/csv/csv.zig diff --git a/build.zig b/build.zig index 37afc8c..7aaf95d 100644 --- a/build.zig +++ b/build.zig @@ -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/ 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. diff --git a/etc/init-diagnose.csv b/etc/init-diagnose.csv new file mode 100644 index 0000000..55f387e --- /dev/null +++ b/etc/init-diagnose.csv @@ -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 diff --git a/etc/init.csv b/etc/init.csv new file mode 100644 index 0000000..4bea925 --- /dev/null +++ b/etc/init.csv @@ -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 diff --git a/library/csv/csv.zig b/library/csv/csv.zig new file mode 100644 index 0000000..e327183 --- /dev/null +++ b/library/csv/csv.zig @@ -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); +} diff --git a/library/device/registry/device-registry.zig b/library/device/registry/device-registry.zig index 30ecb27..26ac4fd 100644 --- a/library/device/registry/device-registry.zig +++ b/library/device/registry/device-registry.zig @@ -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); diff --git a/system/services/init/init.zig b/system/services/init/init.zig index 3a577dd..480dd37 100644 --- a/system/services/init/init.zig +++ b/system/services/init/init.zig @@ -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);