feat: init - dynamic service discovery via init.toml
This commit is contained in:
@@ -12,4 +12,4 @@
|
|||||||
|
|
||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#define MONTAUK_BUILD_NUMBER 103
|
#define MONTAUK_BUILD_NUMBER 104
|
||||||
|
|||||||
@@ -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
|
||||||
+20
-10
@@ -7,25 +7,35 @@
|
|||||||
|
|
||||||
.SH DESCRIPTION
|
.SH DESCRIPTION
|
||||||
init is the first userspace process started by the MontaukOS
|
init is the first userspace process started by the MontaukOS
|
||||||
kernel. It chains system services in sequence, then launches
|
kernel. It discovers system services from 0:/config/init.toml
|
||||||
the interactive shell.
|
and starts them in the order their tables appear in that file.
|
||||||
|
|
||||||
Each service is spawned as a child process. init waits for it
|
Each enabled service is spawned as a child process. Services may
|
||||||
to exit before starting the next one. If a service fails to
|
run in the background or block startup until they exit. Disabled
|
||||||
spawn, init logs an error and continues to the next stage.
|
services are logged and skipped.
|
||||||
|
|
||||||
Log output is timestamped and color-coded:
|
Log output is timestamped and color-coded:
|
||||||
|
|
||||||
HH:MM:SS INFO init Starting dhcp
|
HH:MM:SS INFO init Starting dhcp
|
||||||
HH:MM:SS OK init dhcp finished (pid 1)
|
HH:MM:SS OK init dhcp finished (pid 1)
|
||||||
|
|
||||||
.SH BOOT SEQUENCE
|
.SH CONFIGURATION
|
||||||
The following services are started in order:
|
Each service is declared as a named TOML table:
|
||||||
|
|
||||||
1. 0:/os/dhcp.elf Obtain network configuration via DHCP
|
[services.dhcp]
|
||||||
2. 0:/os/shell.elf Launch the interactive shell
|
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
|
.SH LOG LEVELS
|
||||||
init uses four log levels, each with a distinct color:
|
init uses four log levels, each with a distinct color:
|
||||||
|
|||||||
+109
-16
@@ -1,11 +1,12 @@
|
|||||||
/*
|
/*
|
||||||
* main.cpp
|
* main.cpp
|
||||||
* Init system for MontaukOS (PID 0)
|
* 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
|
* Copyright (c) 2026 Daniel Hammer
|
||||||
*/
|
*/
|
||||||
|
|
||||||
#include <montauk/syscall.h>
|
#include <montauk/syscall.h>
|
||||||
|
#include <montauk/config.h>
|
||||||
#include <libc/stdio.h>
|
#include <libc/stdio.h>
|
||||||
|
|
||||||
// ---- ANSI color codes ----
|
// ---- ANSI color codes ----
|
||||||
@@ -88,31 +89,123 @@ static bool run_service(const char* path, const char* name, bool wait = true) {
|
|||||||
return 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.<id>] 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 ----
|
// ---- Main ----
|
||||||
|
|
||||||
extern "C" void _start() {
|
extern "C" void _start() {
|
||||||
|
|
||||||
log_info("The Montauk Operating System");
|
log_info("The Montauk Operating System");
|
||||||
|
|
||||||
// ---- Stage 1: Network configuration (non-blocking) ----
|
auto config = montauk::config::load("init");
|
||||||
run_service("0:/os/dhcp.elf", "dhcp", false);
|
int discovered = 0;
|
||||||
run_service("0:/os/ntp.elf", "network time", false);
|
|
||||||
|
|
||||||
// Printing is an optional install component; skip quietly when absent.
|
// Table order is file order, making the config itself the startup order.
|
||||||
if (service_installed("0:/os/printd.elf"))
|
for (int i = 0; i < config.entries.count; i++) {
|
||||||
run_service("0:/os/printd.elf", "print spooler", false);
|
auto* service = config.entries.items[i];
|
||||||
else
|
if (!is_service_table(service)) continue;
|
||||||
log_info("Print spooler not installed, skipping");
|
discovered++;
|
||||||
|
|
||||||
// ---- Stage 2: Login screen -> desktop (falls back to desktop, then shell) ----
|
const char* id = service->key + montauk::slen(SERVICE_PREFIX);
|
||||||
if (!run_service("0:/os/login.elf", "login")) {
|
char key[256];
|
||||||
log_warn("Login failed, falling back to desktop");
|
service_key(key, sizeof(key), id, "fallback_only");
|
||||||
if (!run_service("0:/os/desktop.elf", "desktop")) {
|
if (config.get_bool(key, false)) continue;
|
||||||
log_warn("Desktop failed, falling back to shell");
|
|
||||||
run_service("0:/os/shell.elf", "shell");
|
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");
|
log_warn("All services exited");
|
||||||
|
|
||||||
for (;;) {
|
for (;;) {
|
||||||
|
|||||||
Reference in New Issue
Block a user