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
5 changes: 4 additions & 1 deletion docs/api.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down
5 changes: 4 additions & 1 deletion docs/architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down Expand Up @@ -116,14 +117,16 @@ 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`
3. script embedded in the binary

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
Expand Down
4 changes: 2 additions & 2 deletions docs/operator.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
5 changes: 3 additions & 2 deletions man/man8/microinit.8.mdoc
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
6 changes: 5 additions & 1 deletion man/man8/unmount.sh.8.mdoc
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
44 changes: 40 additions & 4 deletions src/init.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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 {
Expand Down Expand Up @@ -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")]
Expand Down Expand Up @@ -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
Expand All @@ -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");
Expand Down
22 changes: 8 additions & 14 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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")]
Expand Down Expand Up @@ -135,6 +133,7 @@ fn default_init_opts(socket: String) -> init::InitOpts {
spawn_getty: true,
attach_ttys: true,
socket,
machine_shutdown: true,
}
}

Expand Down Expand Up @@ -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),
Expand Down
33 changes: 33 additions & 0 deletions tests/init_opts_test.rs
Original file line number Diff line number Diff line change
@@ -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");
}
Loading