Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
name = "microinit"
version = "0.1.0"
edition = "2021"
rust-version = "1.87"
description = "PID 1 init system and service supervisor for BigFred OS"
license = "MIT"
authors = ["BigFred"]
Expand Down
2 changes: 2 additions & 0 deletions docs/architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,8 @@ Mount policy therefore lives in a script / distro overlay, not hard-coded in Rus

**Configuration is always loaded (or re-loaded) from disk only after early-boot returns**, so seeding of `$DATA_DIR/etc/microinit.json` and drop-ins by the script is visible to the supervisor. microinit does not create the config file before the script runs (that would race with mounting `/data`).

Script stdout/stderr are teed live as raw bytes (the child sees a pipe, so `isatty` is false) and captured into a bounded RAM buffer. The buffer is flushed to `earlyBoot.logsPath` after the script exits when `earlyBoot.captureLogs` is true. If the script fails before JSON can be loaded, the buffer is still written when `--early-boot-logs-path` is set or an existing live/image config enables capture.

---

## Late unmount (shutdown)
Expand Down
7 changes: 7 additions & 0 deletions docs/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,10 @@ You do not have to put every service in one big `microinit.json`. Extra JSON und
"logToFiles": false,
"dir": "/data/logs"
},
"earlyBoot": {
"captureLogs": false,
"logsPath": "/var/log/early-boot.log"
},
"services": []
}
```
Expand All @@ -83,6 +87,8 @@ You do not have to put every service in one big `microinit.json`. Extra JSON und
| `logs.tty` | Service logs (init mode) |
| `logs.initTty` | microinit’s own messages |
| `logs.logToFiles` | If `true`, also files under `$DATA_DIR/logs/` |
| `earlyBoot.captureLogs` | If `true`, write the RAM-buffered early-boot script output to `earlyBoot.logsPath` after the script exits. Does **not** skip early-boot. Default `false`. The file is `fsync`ed. If early-boot fails before this JSON is loaded, microinit still tries `--early-boot-logs-path`, then an existing live config, then the image JSON next to `early-boot.sh`. |
| `earlyBoot.logsPath` | Absolute path for that file (default `/var/log/early-boot.log`). Opened only after early-boot returns, so a script that remounts `$DATA_DIR` (NVMe migration) still writes to the final mount. Must sit on a filesystem the script left writable — the root is typically remounted read-only. |
| `openTelemetry` | Optional metrics (see README); also `$DATA_DIR/etc/otel.env` |

Most operators only edit **`services`**.
Expand Down Expand Up @@ -211,6 +217,7 @@ Requires **microinit restart** (on PID 1 hosts: reboot):
still be able to open a `0660` socket for that group (put the intended
socket-group owner first).
- `logs.*` (TTYs, `logToFiles`, buffer size)
- `earlyBoot.*` (capture is applied once at boot, after the script has already run)
- `console`

---
Expand Down
10 changes: 6 additions & 4 deletions docs/operator.md
Original file line number Diff line number Diff line change
Expand Up @@ -285,11 +285,12 @@ microinit start --force redis # debugging only
## Boot sequence (init mode as PID 1)

1. Kernel starts `/sbin/init` (microinit).
2. **Early-boot** (mount `/data`, seed config, …).
2. **Early-boot** (mount `/data`, seed config, …). stdout/stderr are teed live to the console (raw bytes; the script sees a pipe, not a TTY) and buffered in RAM (bounded).
3. Config loaded from disk.
4. Enabled services start in topological order (`dependsOn` hard edges; among ready services lower `orderPriority` first, then name). `background: true` services are started first (in that order), then foreground sequentially. Details: [Service ordering](configuration.md#service-ordering).
5. Console `[ OK ]` / `[ FAIL ]`; getty.
6. IPC socket; JSON files watched for reload.
4. If `earlyBoot.captureLogs` is true, the buffer is truncate-written and `fsync`ed to `earlyBoot.logsPath` (best-effort; a write failure is a warning, not a boot abort). If early-boot itself fails, the same write is attempted via `--early-boot-logs-path` or an existing JSON with `captureLogs: true` before aborting.
5. Enabled services start in topological order (`dependsOn` hard edges; among ready services lower `orderPriority` first, then name). `background: true` services are started first (in that order), then foreground sequentially. Details: [Service ordering](configuration.md#service-ordering).
6. Console `[ OK ]` / `[ FAIL ]`; getty.
7. IPC socket; JSON files watched for reload.

On shutdown in **`init`** mode (`shutdown -r`, IPC `shutdown`, SIGTERM, …): services stop in **reverse** of that start order, then the **unmount** script runs (unbind mounts / umount `/data`), then reboot or power-off.

Expand All @@ -303,6 +304,7 @@ In **`supervise`** mode there is no early-boot, getty, late unmount, or machine
| `/dev/tty3` | microinit messages |
| `microinit logs …` | Same via socket |
| `$DATA_DIR/logs/` | Files when `logs.logToFiles: true` |
| `earlyBoot.logsPath` | Early-boot script stdout/stderr when `earlyBoot.captureLogs: true` (one file per boot, truncated) |

---

Expand Down
4 changes: 4 additions & 0 deletions examples/microinit.json.example
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,10 @@
"dir": "/data/logs",
"logToFiles": false
},
"earlyBoot": {
"captureLogs": false,
"logsPath": "/var/log/early-boot.log"
},
"socket": "/data/run/microinit.sock",
"console": "/dev/tty1",
"services": [
Expand Down
37 changes: 36 additions & 1 deletion man/man5/microinit.json.5.mdoc
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,39 @@ files when
.Cm logs.logToFiles
is true (default
.Pa /data/logs )
.It Cm earlyBoot.captureLogs
If true, write the RAM-buffered early-boot script output to
.Cm earlyBoot.logsPath
after the script exits
.Pq default false .
Does
.Em not
skip early-boot; the script stdout/stderr are always teed live to the console
as raw bytes and always buffered in RAM
.Pq bounded by line count and total bytes .
The on-disk write is followed by
.Xr fsync 2 .
If the script fails before this file is loaded, capture is still persisted
when
.Nm microinit Cm init Fl -early-boot-logs-path
is set, or when an existing copy of this file
.Pq or the image JSON next to
.Pa early-boot.sh
.Pc
has
.Cm captureLogs
true.
.It Cm earlyBoot.logsPath
Absolute path for the captured early-boot log
.Pq default
.Pa /var/log/early-boot.log .
Opened only after the script returns, so a distro overlay that remounts
.Pa $DATA_DIR
.Pq for example NVMe migration
still writes to the final mount.
The path must sit on a filesystem the early-boot script mounted and left
writable; the root filesystem is typically remounted read-only before the
script exits.
.It Cm socket
Unix control socket path
.It Cm console
Expand Down Expand Up @@ -71,8 +104,10 @@ then applies
Changes to these JSON files are picked up via Linux inotify
.Pq hot-reload without restarting microinit ;
.Cm socket
and
,
.Cm logs.*
, and
.Cm earlyBoot.*
changes require a process restart.
.Ss Service fields
.Bl -tag -width successExitCodes
Expand Down
33 changes: 33 additions & 0 deletions man/man8/early-boot.sh.8.mdoc
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,39 @@ Product images may install
with a distro-specific script
.Pq for example BigFred OS also fscks the Pa /data candidates, mounts Pa /data , seeds configs, bind-mounts Pa /etc/shadow
or an override under the data root; otherwise the embedded portable script runs.
.Pp
stdout and stderr of the script are teed live to the
.Nm microinit
console
.Pq kernel serial / HDMI on PID 1
as raw bytes and captured into a bounded RAM buffer
.Pq line count and total bytes .
Invalid UTF-8 is replaced in the on-disk copy and does not stop capture.
The child sees a pipe, not a TTY:
.Fn isatty
on stdout/stderr is false, which may change the output of tools that detect a
terminal.
The buffer is flushed to
.Cm earlyBoot.logsPath
only after the script exits
.Pq and only when Cm earlyBoot.captureLogs is true ,
so a script that migrates or remounts the log target is safe.
The write is followed by
.Xr fsync 2
so a hard power cut shortly after boot still leaves the file.
If early-boot fails before configuration is loaded, microinit still tries to
write the buffer
.Pq
.Nm microinit Cm init Fl -early-boot-logs-path ,
an existing live
.Pa microinit.json
with
.Cm captureLogs
true, or the image JSON next to this script
.Pc ;
if
.Pa /data
never mounted, that write fails and only the console copy remains.
.Ss Environment
.Bl -tag -width MICROINIT_INIT_LOGS_TTY
.It Ev DATA_DIR
Expand Down
12 changes: 12 additions & 0 deletions man/man8/microinit.8.mdoc
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
.Op Fl -no-early-boot
.Op Fl -allow-no-early-boot
.Op Fl -log-to-files
.Op Fl -early-boot-logs-path Ns = Ns Ar path
.Nm
.Op Fl -socket Ns = Ns Ar path
.Cm supervise
Expand Down Expand Up @@ -145,6 +146,17 @@ is off by default
or
.Cm logs.logToFiles true .
.Pp
.Fl -early-boot-logs-path
is a fallback used only when early-boot fails before
.Xr microinit.json 5
can be loaded.
PID 1 otherwise peeks an existing live or image JSON for
.Cm earlyBoot.logsPath
when
.Cm captureLogs
is true.
A write failure is logged and does not change the fatal early-boot error.
.Pp
During boot, operational messages are written both to the console/stderr and to
.Fl -init-logs-tty .
After
Expand Down
85 changes: 85 additions & 0 deletions src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ pub const DEFAULT_INIT_LOGS_TTY: &str = "/dev/tty3";
pub const DEFAULT_LOG_LINES: usize = 300;
pub const DEFAULT_EARLY_BOOT: &str = "/etc/microinit/early-boot.sh";
pub const DEFAULT_UNMOUNT: &str = "/etc/microinit/unmount.sh";
pub const DEFAULT_EARLY_BOOT_LOGS_PATH: &str = "/var/log/early-boot.log";

/// Hub-default config path (`/data/etc/...` when data root is unset).
/// Prefer [`default_config_path`] which honors `DATA_DIR`.
Expand Down Expand Up @@ -120,6 +121,78 @@ impl LogsConfig {
}
}

/// Early-boot phase options. Applied at boot only (never hot-reloaded):
/// the script has already run by the time this file is read.
///
/// Script stdout/stderr are always buffered in RAM (bounded). [`Self::capture_logs`]
/// only gates the truncate-write to [`Self::logs_path`] after the script exits,
/// so a distro overlay that remounts `$DATA_DIR` (for example NVMe migration)
/// still lands the file on the final mount.
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct EarlyBootConfig {
/// Persist the buffered early-boot script output to [`Self::logs_path`].
/// Output is always buffered in RAM; this only gates the disk write.
#[serde(default)]
pub capture_logs: bool,
#[serde(default = "default_early_boot_logs_path")]
pub logs_path: String,
}

fn default_early_boot_logs_path() -> String {
DEFAULT_EARLY_BOOT_LOGS_PATH.to_string()
}

impl Default for EarlyBootConfig {
fn default() -> Self {
Self {
capture_logs: false,
logs_path: default_early_boot_logs_path(),
}
}
}

/// Result of reading `earlyBoot` from an existing JSON file without creating
/// or validating the rest of the config. Used when early-boot failed and we
/// must not seed a default `microinit.json`.
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum EarlyBootCapturePeek {
/// Path does not exist, is unreadable, or is not JSON.
Absent,
/// File exists and `captureLogs` is false (or `logsPath` is empty).
Disabled,
/// `captureLogs` is true; persist to this path.
Enabled(PathBuf),
}

/// Read only the `earlyBoot` object from `config_path`. Never creates files.
#[must_use]
pub fn peek_early_boot_capture(config_path: &Path) -> EarlyBootCapturePeek {
if !config_path.is_file() {
return EarlyBootCapturePeek::Absent;
}
let Ok(data) = fs::read_to_string(config_path) else {
return EarlyBootCapturePeek::Absent;
};
#[derive(Deserialize)]
#[serde(rename_all = "camelCase")]
struct Peek {
#[serde(default)]
early_boot: EarlyBootConfig,
}
let Ok(peek) = serde_json::from_str::<Peek>(&data) else {
return EarlyBootCapturePeek::Absent;
};
if !peek.early_boot.capture_logs {
return EarlyBootCapturePeek::Disabled;
}
let p = peek.early_boot.logs_path.trim();
if p.is_empty() {
return EarlyBootCapturePeek::Disabled;
}
EarlyBootCapturePeek::Enabled(PathBuf::from(p))
}

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct LivenessProbe {
Expand Down Expand Up @@ -465,6 +538,8 @@ pub struct Config {
pub version: u32,
#[serde(default)]
pub logs: LogsConfig,
#[serde(default)]
pub early_boot: EarlyBootConfig,
#[serde(default = "default_socket")]
pub socket: String,
#[serde(default = "default_console")]
Expand Down Expand Up @@ -496,6 +571,7 @@ impl Default for Config {
Self {
version: 1,
logs: LogsConfig::default(),
early_boot: EarlyBootConfig::default(),
socket: default_socket(),
console: default_console(),
socket_allow_users: Vec::new(),
Expand All @@ -507,6 +583,14 @@ impl Default for Config {

impl Config {
pub fn validate(&self) -> Result<()> {
if self.early_boot.capture_logs {
let p = self.early_boot.logs_path.trim();
if p.is_empty() || !Path::new(p).is_absolute() {
return Err(Error::Config(
"earlyBoot.logsPath must be an absolute path".into(),
));
}
}
let mut names = std::collections::HashSet::new();
for svc in &self.services {
if svc.name.is_empty() {
Expand Down Expand Up @@ -848,6 +932,7 @@ pub fn example_config() -> Config {
dir: Some(default_logs_dir().display().to_string()),
log_to_files: false,
},
early_boot: EarlyBootConfig::default(),
socket: DEFAULT_SOCKET.to_string(),
console: DEFAULT_CONSOLE.to_string(),
socket_allow_users: Vec::new(),
Expand Down
7 changes: 7 additions & 0 deletions src/constants.rs
Original file line number Diff line number Diff line change
Expand Up @@ -46,3 +46,10 @@ pub const TERMINATE_POLL: Duration = Duration::from_millis(100);
pub const EVENT_RING_CAP: usize = 16;
/// How many recent lifecycle events `describe` returns (= ring capacity).
pub const EVENT_RETURN: usize = EVENT_RING_CAP;
/// Max early-boot script lines retained for `earlyBoot.logsPath` (bounded RAM on PID 1).
pub const MAX_EARLY_BOOT_CAPTURE_LINES: usize = 2000;
/// Total captured-text budget (UTF-8 bytes of retained lines) on PID 1.
pub const MAX_EARLY_BOOT_CAPTURE_BYTES: usize = 256 * 1024;
/// Max single captured line length before truncation. Enforced while reading so a
/// line without a newline cannot grow without bound.
pub const MAX_EARLY_BOOT_LINE_BYTES: usize = 4096;
Loading
Loading