Your browser sucks, JavaScript disabled! Gunnar's console needs JavaScript and WebAssembly. Git over SSH on port 2222 does not.

nordisk/edda

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:?}");
            }
        }
    }
}