diff --git a/kernel/src/Api/BuildNo.hpp b/kernel/src/Api/BuildNo.hpp index 7c6de1d..1c68e2b 100644 --- a/kernel/src/Api/BuildNo.hpp +++ b/kernel/src/Api/BuildNo.hpp @@ -12,4 +12,4 @@ #pragma once -#define MONTAUK_BUILD_NUMBER 103 +#define MONTAUK_BUILD_NUMBER 104 diff --git a/programs/data/config/init.toml b/programs/data/config/init.toml new file mode 100644 index 0000000..eb2f199 --- /dev/null +++ b/programs/data/config/init.toml @@ -0,0 +1,44 @@ +# MontaukOS init services +# +# Services are started in declaration order. Available fields: +# path executable to spawn (required) +# name display name used in the boot log (defaults to the table id) +# enabled whether init should consider the service (defaults to true) +# wait wait for the process to exit before continuing (defaults to true) +# optional quietly skip the service when its executable is absent +# fallback service id to start if spawning this service fails +# fallback_only define the service for fallback use without starting it normally + +[services.dhcp] +path = "0:/os/dhcp.elf" +enabled = true +wait = false + +[services.ntp] +path = "0:/os/ntp.elf" +name = "network time" +enabled = true +wait = false + +[services.printd] +path = "0:/os/printd.elf" +name = "print spooler" +enabled = true +wait = false +optional = true + +[services.login] +path = "0:/os/login.elf" +enabled = true +fallback = "desktop" + +[services.desktop] +path = "0:/os/desktop.elf" +enabled = true +fallback = "shell" +fallback_only = true + +[services.shell] +path = "0:/os/shell.elf" +enabled = true +fallback_only = true diff --git a/programs/man/init.1 b/programs/man/init.1 index 7cd35cc..819aaf3 100644 --- a/programs/man/init.1 +++ b/programs/man/init.1 @@ -7,25 +7,35 @@ .SH DESCRIPTION init is the first userspace process started by the MontaukOS - kernel. It chains system services in sequence, then launches - the interactive shell. + kernel. It discovers system services from 0:/config/init.toml + and starts them in the order their tables appear in that file. - Each service is spawned as a child process. init waits for it - to exit before starting the next one. If a service fails to - spawn, init logs an error and continues to the next stage. + Each enabled service is spawned as a child process. Services may + run in the background or block startup until they exit. Disabled + services are logged and skipped. Log output is timestamped and color-coded: HH:MM:SS INFO init Starting dhcp HH:MM:SS OK init dhcp finished (pid 1) -.SH BOOT SEQUENCE - The following services are started in order: +.SH CONFIGURATION + Each service is declared as a named TOML table: - 1. 0:/os/dhcp.elf Obtain network configuration via DHCP - 2. 0:/os/shell.elf Launch the interactive shell + [services.dhcp] + path = "0:/os/dhcp.elf" + enabled = true + wait = false - After the shell exits, init enters an idle loop. + path is required. name controls the name shown in the boot log and + defaults to the table name. enabled and wait both default to true. + optional skips a service whose executable is not installed. + + fallback names another configured service to start if spawning the + service fails. A service with fallback_only = true is available as + a fallback but is not started during the normal pass. + + After all foreground services exit, init enters an idle loop. .SH LOG LEVELS init uses four log levels, each with a distinct color: diff --git a/programs/src/init/main.cpp b/programs/src/init/main.cpp index 3d387ad..3da4849 100644 --- a/programs/src/init/main.cpp +++ b/programs/src/init/main.cpp @@ -1,11 +1,12 @@ /* * main.cpp * Init system for MontaukOS (PID 0) - * Chains system services then launches the shell. + * Discovers and starts system services from 0:/config/init.toml. * Copyright (c) 2026 Daniel Hammer */ #include +#include #include // ---- ANSI color codes ---- @@ -88,31 +89,123 @@ static bool run_service(const char* path, const char* name, bool wait = true) { return true; } +// ---- Service configuration ---- + +static constexpr const char* SERVICE_PREFIX = "services."; + +static bool is_service_table(const montauk::toml::Value* value) { + if (!value || value->type != montauk::toml::Type::Table || !value->key) + return false; + if (!montauk::starts_with(value->key, SERVICE_PREFIX)) + return false; + + // Only [services.] declares a service. Nested tables belong to that + // service and must not be treated as additional services. + const char* id = value->key + montauk::slen(SERVICE_PREFIX); + if (!*id) return false; + while (*id) { + if (*id == '.') return false; + id++; + } + return true; +} + +static void service_key(char* out, int out_size, const char* id, const char* field) { + snprintf(out, out_size, "%s%s.%s", SERVICE_PREFIX, id, field); +} + +static montauk::toml::Value* find_service(const montauk::toml::Doc& doc, const char* id) { + char table_key[256]; + snprintf(table_key, sizeof(table_key), "%s%s", SERVICE_PREFIX, id); + return doc.get_table(table_key); +} + +static bool run_configured_service(const montauk::toml::Doc& doc, + montauk::toml::Value* service, + int fallback_depth = 0) { + const char* id = service->key + montauk::slen(SERVICE_PREFIX); + char key[256]; + char msg[192]; + + service_key(key, sizeof(key), id, "name"); + const char* name = doc.get_string(key, id); + + service_key(key, sizeof(key), id, "enabled"); + if (!doc.get_bool(key, true)) { + snprintf(msg, sizeof(msg), "%s disabled, skipping", name); + log_info(msg); + return true; + } + + service_key(key, sizeof(key), id, "path"); + const char* path = doc.get_string(key, ""); + if (!path[0]) { + snprintf(msg, sizeof(msg), "Service %s has no path, skipping", id); + log_err(msg); + return false; + } + + service_key(key, sizeof(key), id, "optional"); + bool optional = doc.get_bool(key, false); + if (optional && !service_installed(path)) { + snprintf(msg, sizeof(msg), "%s not installed, skipping", name); + log_info(msg); + return true; + } + + service_key(key, sizeof(key), id, "wait"); + bool wait = doc.get_bool(key, true); + if (run_service(path, name, wait)) return true; + + service_key(key, sizeof(key), id, "fallback"); + const char* fallback = doc.get_string(key, ""); + if (!fallback[0]) return false; + + if (fallback_depth >= 8) { + log_err("Service fallback chain is too deep"); + return false; + } + + auto* fallback_service = find_service(doc, fallback); + if (!fallback_service) { + snprintf(msg, sizeof(msg), "Fallback service %s is not configured", fallback); + log_err(msg); + return false; + } + + snprintf(msg, sizeof(msg), "%s failed, falling back to %s", name, fallback); + log_warn(msg); + return run_configured_service(doc, fallback_service, fallback_depth + 1); +} + // ---- Main ---- extern "C" void _start() { log_info("The Montauk Operating System"); - // ---- Stage 1: Network configuration (non-blocking) ---- - run_service("0:/os/dhcp.elf", "dhcp", false); - run_service("0:/os/ntp.elf", "network time", false); + auto config = montauk::config::load("init"); + int discovered = 0; - // Printing is an optional install component; skip quietly when absent. - if (service_installed("0:/os/printd.elf")) - run_service("0:/os/printd.elf", "print spooler", false); - else - log_info("Print spooler not installed, skipping"); + // Table order is file order, making the config itself the startup order. + for (int i = 0; i < config.entries.count; i++) { + auto* service = config.entries.items[i]; + if (!is_service_table(service)) continue; + discovered++; - // ---- Stage 2: Login screen -> desktop (falls back to desktop, then shell) ---- - if (!run_service("0:/os/login.elf", "login")) { - log_warn("Login failed, falling back to desktop"); - if (!run_service("0:/os/desktop.elf", "desktop")) { - log_warn("Desktop failed, falling back to shell"); - run_service("0:/os/shell.elf", "shell"); - } + const char* id = service->key + montauk::slen(SERVICE_PREFIX); + char key[256]; + service_key(key, sizeof(key), id, "fallback_only"); + if (config.get_bool(key, false)) continue; + + run_configured_service(config, service); } + if (discovered == 0) + log_err("No services found in 0:/config/init.toml"); + + config.destroy(); + log_warn("All services exited"); for (;;) {