diff --git a/docs/api.md b/docs/api.md index fb74ac4..4193a14 100644 --- a/docs/api.md +++ b/docs/api.md @@ -136,7 +136,10 @@ Disabling stops the service; enabling requests a start (subject to dependencies) `mode` is one of: `reboot`, `poweroff`, `halt`. -**Response:** `ok`, then the daemon begins ordered shutdown (stop all services, run late-unmount script, then reboot/poweroff/halt). +**Response:** `ok`, then the daemon begins ordered shutdown: + +- **`init` mode:** stop all services, run late-unmount script, then reboot/poweroff/halt. +- **`supervise` mode:** stop all services, sync, and exit the process (no unmount script, no `reboot(2)`). The `mode` field is accepted but ignored for machine power state. Operators normally use the companion `shutdown` binary (`shutdown -r now`, …) which sends this request and falls back to BusyBox `/sbin/{poweroff,reboot,halt}` if the socket is missing. diff --git a/docs/architecture.md b/docs/architecture.md index 52bd02d..aa25792 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -23,6 +23,7 @@ The same supervisor logic runs under two thin wrappers: | Console login (getty) | Yes, when PID 1 | No | | Logs on TTYs (tty2 / tty3) | Yes | No (in-memory ring + IPC / optional files) | | Service supervision + control socket | Yes | Yes | +| Late unmount + reboot/poweroff/halt | Yes | No (stop services, sync, exit) | **Architectural rule:** there are not two separate implementations. Both modes call the same startup path with different switches (`InitOpts`). A fix in the supervisor therefore applies on the device and in the container. @@ -116,7 +117,7 @@ Mount policy therefore lives in a script / distro overlay, not hard-coded in Rus ## Late unmount (shutdown) -At the end of ordered shutdown — after all supervised services have been stopped, and before `reboot(2)` / power-off / halt — `init` mode runs an **unmount** script (same search order as early-boot): +At the end of ordered shutdown in **`init` mode only** — after all supervised services have been stopped, and before `reboot(2)` / power-off / halt — microinit runs an **unmount** script (same search order as early-boot): 1. `$DATA_DIR/etc/microinit/unmount.sh` 2. `/etc/microinit/unmount.sh` @@ -124,6 +125,8 @@ At the end of ordered shutdown — after all supervised services have been stopp Failures are logged and **do not block** reboot (a stuck umount must not hang the board forever). Distro overlays typically reverse early-boot (unbind `/etc/shadow`, umount `/data`, `sync`). +**`supervise` mode** never runs this script and never calls `reboot(2)`: it stops services, syncs, and exits the process. + --- ## Android / supervise-only build diff --git a/docs/operator.md b/docs/operator.md index b37a9bd..200525d 100644 --- a/docs/operator.md +++ b/docs/operator.md @@ -207,9 +207,9 @@ Typical hub boot: 5. Console shows `[ OK ]` / `[ FAIL ]` style status; getty on the console TTY. 6. Control socket listens; config files are watched for reload. -On shutdown (`shutdown -r`, IPC `shutdown`, SIGTERM, …): services stop in reverse dependency order, then the **unmount** script runs (unbind mounts / umount `/data`), then reboot or power-off. +On shutdown in **`init`** mode (`shutdown -r`, IPC `shutdown`, SIGTERM, …): services stop in reverse dependency order, then the **unmount** script runs (unbind mounts / umount `/data`), then reboot or power-off. -In **`supervise`** mode there is no early-boot and no getty — only the supervisor + socket (good for containers). Unmount still runs at the end of shutdown if a script is present (or the embedded default). +In **`supervise`** mode there is no early-boot, getty, late unmount, or machine reboot — only the supervisor + socket (good for containers). On shutdown it stops services, syncs, and exits. ### Logs on a device diff --git a/man/man8/microinit.8.mdoc b/man/man8/microinit.8.mdoc index abd4e34..5bc4f54 100644 --- a/man/man8/microinit.8.mdoc +++ b/man/man8/microinit.8.mdoc @@ -56,8 +56,9 @@ API used by the same binary's management subcommands. .Cm supervise uses the same runtime as .Cm init -but skips early-boot, getty, and TTY attach -.Pq suitable for containers / distroless images . +but skips early-boot, getty, TTY attach, late unmount, and machine reboot +.Pq suitable for containers / distroless images ; +on shutdown it only stops services, syncs, and exits. .Pp When invoked as .Pa /sbin/init diff --git a/man/man8/unmount.sh.8.mdoc b/man/man8/unmount.sh.8.mdoc index de94488..5848611 100644 --- a/man/man8/unmount.sh.8.mdoc +++ b/man/man8/unmount.sh.8.mdoc @@ -9,11 +9,15 @@ .Sh DESCRIPTION Shell script executed by .Xr microinit 8 -at the end of ordered shutdown, after all supervised services have stopped and +in +.Cm init +mode at the end of ordered shutdown, after all supervised services have stopped and before .Xr reboot 2 / .Xr poweroff . +.Cm supervise +mode never runs this script. Not hardcoded in Rust so unmount policy can change without recompilation. .Pp Search order: diff --git a/src/init.rs b/src/init.rs index 231b233..4205a72 100644 --- a/src/init.rs +++ b/src/init.rs @@ -38,6 +38,9 @@ pub struct InitOpts { pub attach_ttys: bool, /// Control socket path (overrides JSON after load). pub socket: String, + /// If true (`init`), run late unmount then reboot/poweroff/halt. + /// If false (`supervise`), only stop services, sync, and exit. + pub machine_shutdown: bool, } impl Default for InitOpts { @@ -53,10 +56,34 @@ impl Default for InitOpts { spawn_getty: true, attach_ttys: true, socket: crate::config::default_socket_path().display().to_string(), + machine_shutdown: true, } } } +/// Options for `microinit supervise` (containers / embedded hosts). +#[must_use] +pub fn supervise_opts( + console: String, + paths: Paths, + socket: String, + log_to_files: bool, +) -> InitOpts { + InitOpts { + logs_tty: DEFAULT_LOGS_TTY.to_string(), + init_logs_tty: DEFAULT_INIT_LOGS_TTY.to_string(), + console, + paths, + skip_early_boot: true, + require_early_boot: false, + log_to_files, + spawn_getty: false, + attach_ttys: false, + socket, + machine_shutdown: false, + } +} + pub fn run(opts: InitOpts) -> Result<()> { // Resolve init-logs path early so pre-hub boot notes reach tty3 as well as stderr. let init_logs_preview = if opts.attach_ttys { @@ -233,6 +260,7 @@ pub fn run(opts: InitOpts) -> Result<()> { .unwrap_or_else(|| PathBuf::from("/data/etc")); let dropins_dir = opts.paths.dropins_dir.clone(); let paths_for_reload = opts.paths.clone(); + let machine_shutdown = opts.machine_shutdown; #[cfg(feature = "init")] let paths_for_unmount = opts.paths.clone(); #[cfg(feature = "init")] @@ -287,6 +315,16 @@ pub fn run(opts: InitOpts) -> Result<()> { if let Some(mode) = take_shutdown() { hub.emit_init(LogLevel::Info, format!("shutdown requested: {mode}")); supervisor.stop_all_ordered(); + let _ = std::fs::remove_file(&socket_path); + + // Supervise / Android: stop services only — no unmount script, no reboot(2). + if !machine_shutdown { + let _ = mode; + nix::unistd::sync(); + hub.emit_init(LogLevel::Info, "supervise shutdown complete; exiting"); + std::process::exit(0); + } + #[cfg(feature = "init")] { // Late unmount after all services are stopped; failures must not @@ -303,13 +341,11 @@ pub fn run(opts: InitOpts) -> Result<()> { format!("unmount failed (continuing to {mode}): {e}"), ); } + crate::shutdown::finalize(mode); } - let _ = std::fs::remove_file(&socket_path); - #[cfg(feature = "init")] - crate::shutdown::finalize(mode); #[cfg(not(feature = "init"))] { - // Supervise-only / Android: stop + sync + clean exit (no reboot). + // machine_shutdown without feature init: treat as supervise exit. let _ = mode; nix::unistd::sync(); hub.emit_init(LogLevel::Info, "supervise shutdown complete; exiting"); diff --git a/src/main.rs b/src/main.rs index c1f7dd7..6e4bb06 100644 --- a/src/main.rs +++ b/src/main.rs @@ -5,13 +5,11 @@ use std::process::ExitCode; use clap::{Parser, Subcommand}; use microinit::cli; -use microinit::config::{ - self, default_config_path, default_socket_path, DEFAULT_INIT_LOGS_TTY, DEFAULT_LOGS_TTY, -}; +use microinit::config::{self, default_config_path, default_socket_path}; use microinit::init; #[cfg(feature = "init")] -use microinit::config::DEFAULT_CONSOLE; +use microinit::config::{DEFAULT_CONSOLE, DEFAULT_INIT_LOGS_TTY, DEFAULT_LOGS_TTY}; #[cfg(feature = "init")] use std::thread; #[cfg(feature = "init")] @@ -135,6 +133,7 @@ fn default_init_opts(socket: String) -> init::InitOpts { spawn_getty: true, attach_ttys: true, socket, + machine_shutdown: true, } } @@ -226,23 +225,18 @@ fn main() -> ExitCode { spawn_getty: true, attach_ttys: true, socket, + machine_shutdown: true, }), Commands::Supervise { console, config: config_path, log_to_files, - } => init::run(init::InitOpts { - logs_tty: DEFAULT_LOGS_TTY.to_string(), - init_logs_tty: DEFAULT_INIT_LOGS_TTY.to_string(), + } => init::run(init::supervise_opts( console, - paths: paths_for_config(&config_path), - skip_early_boot: true, - require_early_boot: false, - log_to_files, - spawn_getty: false, - attach_ttys: false, + paths_for_config(&config_path), socket, - }), + log_to_files, + )), Commands::Start { name, force } => cli::cmd_start(&cli.socket, &name, force), Commands::Stop { name } => cli::cmd_stop(&cli.socket, &name), Commands::Restart { name } => cli::cmd_restart(&cli.socket, &name), diff --git a/tests/init_opts_test.rs b/tests/init_opts_test.rs new file mode 100644 index 0000000..be7151c --- /dev/null +++ b/tests/init_opts_test.rs @@ -0,0 +1,33 @@ +//! InitOpts defaults vs supervise wiring (machine_shutdown gate). + +use microinit::config::Paths; +use microinit::init::{supervise_opts, InitOpts}; + +#[test] +fn default_opts_enable_machine_shutdown() { + let opts = InitOpts::default(); + assert!(opts.machine_shutdown); + assert!(!opts.skip_early_boot); + assert!(opts.spawn_getty); + assert!(opts.attach_ttys); +} + +#[test] +fn supervise_opts_disable_machine_shutdown() { + let opts = supervise_opts( + "/dev/null".into(), + Paths::default(), + "/tmp/test.sock".into(), + false, + ); + assert!( + !opts.machine_shutdown, + "supervise must not run late unmount or reboot(2)" + ); + assert!(opts.skip_early_boot); + assert!(!opts.require_early_boot); + assert!(!opts.spawn_getty); + assert!(!opts.attach_ttys); + assert_eq!(opts.socket, "/tmp/test.sock"); + assert_eq!(opts.console, "/dev/null"); +}