crates/appliance-pid1/src/stop.rs
//! **What ends a serve, and what `main` does after a clean stop** (Gunnar's
//! `pid1::{StopSource, stop_sources, on_appliance, after_clean_stop}`, gunnar-acpi
//! 2026-09-28, moved here so both products decide it ONE way).
//!
//! On the appliance a cloud's soft stop is the ACPI power button, never a signal; and
//! after a clean stop PID 1 powers off, because a PID 1 that returns from `main` is
//! `Attempted to kill init!` and `panic=10` reboots the box it was asked to stop. Off
//! the appliance (a laptop, a test, a container whose init is somebody else) only a
//! signal ends the serve and `main` returns. The waiting itself is
//! `crate::runtime::stop_request` (feature `runtime`).
/// **What can end a serve.** A signal (a supervisor, the console) or, on the
/// appliance, the ACPI power button: a cloud's soft stop (UpCloud `stop`, QEMU
/// `system_powerdown`) is a button press the kernel reports on the `acpi_event`
/// generic-netlink group and never turns into a signal.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum StopSource {
/// `SIGTERM`, `SIGINT`, `SIGQUIT` (or `SIGPWR`).
Signal,
/// The ACPI power button.
AcpiPowerButton,
}
/// **What `main` does after a clean stop.**
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum AfterStop {
/// Quiesce and power the machine off (`crate::power::power_off_cleanly`): PID 1
/// must never return from `main`.
PowerOff,
/// Return from `main`: a laptop, a test, a container whose init is somebody else.
Return,
}
/// Pure: the appliance from its two facts — PID 1, and a kernel command line that
/// carries korp-start's `korp.entry=` contract.
pub fn appliance_from(pid1: bool, cmdline: &str) -> bool {
pid1 && cmdline.contains("korp.entry=")
}
/// **Is this the appliance?** PID 1 booted by korp, not merely PID 1: in a container
/// the product is PID 1 too, and there the power button is the host's (or absent) and
/// `reboot(2)` is refused, so a power-off would park the container instead of ending it.
pub fn on_appliance() -> bool {
appliance_from(std::process::id() == 1, &std::fs::read_to_string("/proc/cmdline").unwrap_or_default())
}
/// **What ends the serve**, on the appliance (`true`, see [`on_appliance`]) or anywhere
/// else: the appliance hears the ACPI power button as well as the signals; anything else
/// hears signals only.
pub fn stop_sources(appliance: bool) -> &'static [StopSource] {
if appliance {
&[StopSource::Signal, StopSource::AcpiPowerButton]
} else {
&[StopSource::Signal]
}
}
/// **What `main` does after a clean stop**: only the appliance powers off.
pub fn after_clean_stop(appliance: bool) -> AfterStop {
if appliance {
AfterStop::PowerOff
} else {
AfterStop::Return
}
}
#[cfg(test)]
mod tests {
use super::*;
/// gunnar-acpi (finalization 2026-09-28): on the appliance a soft stop is the ACPI
/// power button, and after a clean stop PID 1 powers off.
#[test]
fn on_pid1_the_acpi_power_button_ends_the_serve_and_a_clean_stop_powers_off() {
assert!(stop_sources(true).contains(&StopSource::AcpiPowerButton), "a soft stop (UpCloud stop, QEMU system_powerdown) is the power button; PID 1 must hear it: {:?}", stop_sources(true));
assert!(stop_sources(true).contains(&StopSource::Signal), "the signals still stop the box");
assert_eq!(after_clean_stop(true), AfterStop::PowerOff, "main returning as PID 1 is `Attempted to kill init!` and a panic=10 REBOOT");
}
/// Off the appliance nothing changes: a laptop's power button is not the product's,
/// and `main` returns.
#[test]
fn off_the_appliance_only_a_signal_ends_the_serve_and_main_returns() {
assert_eq!(stop_sources(false), &[StopSource::Signal]);
assert_eq!(after_clean_stop(false), AfterStop::Return);
}
/// The appliance is PID 1 booted by korp, not merely PID 1 (a container entrypoint
/// is PID 1 too); a test process is neither.
#[test]
fn the_appliance_is_pid1_booted_by_korp_and_a_test_is_not() {
assert!(appliance_from(true, "BOOT_IMAGE=/vmlinuz korp.entry=gunnar console=ttyS0"));
assert!(!appliance_from(true, "BOOT_IMAGE=/vmlinuz console=ttyS0"), "a container's PID 1 reads the host's cmdline");
assert!(!appliance_from(false, "korp.entry=gunnar"), "not PID 1");
assert!(!on_appliance(), "this test process is not the appliance");
}
/// The docs said `system_powerdown` arrives as `SIGTERM`; on the appliance kernel
/// nothing turns the button press into a signal (Gunnar's doc guard, over edda's files).
#[test]
fn no_doc_says_a_powerdown_arrives_as_a_signal() {
let claims = [format!("`system_powerdown` becomes {}", "`SIGTERM`"), format!("from {}", "`system_powerdown`)")];
for (file, text) in [("stop.rs", include_str!("stop.rs")), ("power.rs", include_str!("power.rs")), ("runtime.rs", include_str!("runtime.rs")), ("net.rs", include_str!("net.rs"))] {
for c in &claims {
assert!(!text.contains(c.as_str()), "{file} still says {c:?}");
}
}
}
}