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/power.rs

//! **The power button, and the power-off after it.**
//!
//! A cloud's soft stop (UpCloud `stop_type: soft`, QEMU `system_powerdown`,
//! libvirt `shutdown`) is an **ACPI power-button press**, not a signal. On a
//! distribution, systemd-logind (or acpid) hears the press and starts the
//! shutdown. An OS-less appliance has neither, and the kernel itself does
//! NOTHING with the press except report it — so a PID 1 that only waits for
//! `SIGTERM` never learns it was asked to stop, and the cloud waits out its
//! whole soft-stop timeout before it pulls the plug (MEASURED on UpCloud,
//! 2026-09-28: a holger box took about 20 minutes to "stop"; the mock's
//! `system_powerdown` answered the same way — nothing).
//!
//! # Where the press is reported
//!
//! The ACPI button driver (`CONFIG_ACPI_BUTTON`, `drivers/acpi/button.c`,
//! `acpi_button_notify`) reports every press twice: as `KEY_POWER` on an input
//! device (which needs `CONFIG_INPUT_EVDEV` and a `/dev/input` node to read),
//! and as a generic-netlink multicast on the `acpi_event` family, group
//! `acpi_mc_group` (`drivers/acpi/event.c`, compiled with `CONFIG_NET`) — the
//! road acpid listens on. The netlink road needs no device node and no evdev,
//! so this module takes it: the nordisk appliance kernel (tunnr-kernel, preset
//! `nuked`) has `CONFIG_ACPI_BUTTON=y`, `CONFIG_NET=y` and
//! `# CONFIG_INPUT_EVDEV is not set` (MEASURED 2026-09-28, the built
//! `kernel.config`).
//!
//! # The power-off
//!
//! A PID 1 that RETURNS from `main` is the kernel's `Attempted to kill init!`
//! panic — with `panic=10` the box reboots and comes back, which is the opposite
//! of stopped. [`power_off`] is the end of a clean stop: `sync(2)`, then
//! `reboot(LINUX_REBOOT_CMD_POWER_OFF)`, and a park if the kernel refuses
//! (gunnar-server's `narrate::power_off_now`, the pattern; libc here).
//!
//! The builders and the parsers are pure (bytes in, values out) and tested
//! against hand-built frames; [`PowerButton`] is the one socket.

use std::io;

/// The generic-netlink family the ACPI event driver registers.
pub const ACPI_FAMILY: &str = "acpi_event";
/// Its one multicast group.
pub const ACPI_GROUP: &str = "acpi_mc_group";
/// `acpi_device_class` of the power button (`ACPI_BUTTON_CLASS "/" power`).
pub const POWER_BUTTON_CLASS: &str = "button/power";

// ── the netlink constants this module speaks (uapi/linux/netlink.h, genetlink.h) ──

const NLMSG_HDRLEN: usize = 16;
const GENL_HDRLEN: usize = 4;
const NLA_HDRLEN: usize = 4;
const NLMSG_ERROR: u16 = 2;
const NLMSG_DONE: u16 = 3;
const NLM_F_REQUEST: u16 = 1;
const GENL_ID_CTRL: u16 = 0x10;
const CTRL_CMD_GETFAMILY: u8 = 3;
const CTRL_ATTR_FAMILY_ID: u16 = 1;
const CTRL_ATTR_FAMILY_NAME: u16 = 2;
const CTRL_ATTR_MCAST_GROUPS: u16 = 7;
const CTRL_ATTR_MCAST_GRP_NAME: u16 = 1;
const CTRL_ATTR_MCAST_GRP_ID: u16 = 2;
const NLA_TYPE_MASK: u16 = 0x3fff;
/// `ACPI_GENL_CMD_EVENT`.
const ACPI_GENL_CMD_EVENT: u8 = 1;
/// `ACPI_GENL_ATTR_EVENT`.
const ACPI_GENL_ATTR_EVENT: u16 = 1;
/// `sizeof(struct acpi_genl_event)`: `char device_class[20]; char bus_id[15];
/// u32 type; u32 data;` — 35 bytes of names, one byte of padding, two u32.
const ACPI_GENL_EVENT_LEN: usize = 44;

fn align4(n: usize) -> usize {
    (n + 3) & !3
}

fn u16_at(b: &[u8], at: usize) -> Option<u16> {
    Some(u16::from_ne_bytes(b.get(at..at + 2)?.try_into().ok()?))
}

fn u32_at(b: &[u8], at: usize) -> Option<u32> {
    Some(u32::from_ne_bytes(b.get(at..at + 4)?.try_into().ok()?))
}

fn c_str(b: &[u8]) -> String {
    let end = b.iter().position(|&c| c == 0).unwrap_or(b.len());
    String::from_utf8_lossy(&b[..end]).into_owned()
}

/// Pure: **`CTRL_CMD_GETFAMILY` for `family`** — one netlink request to the
/// generic-netlink controller, the family's name as a NUL-terminated attribute.
pub fn getfamily_request(family: &str, seq: u32) -> Vec<u8> {
    let name_len = family.len() + 1;
    let attr_len = NLA_HDRLEN + name_len;
    let total = NLMSG_HDRLEN + GENL_HDRLEN + align4(attr_len);
    let mut m = Vec::with_capacity(total);
    m.extend_from_slice(&(total as u32).to_ne_bytes());
    m.extend_from_slice(&GENL_ID_CTRL.to_ne_bytes());
    m.extend_from_slice(&NLM_F_REQUEST.to_ne_bytes());
    m.extend_from_slice(&seq.to_ne_bytes());
    m.extend_from_slice(&0u32.to_ne_bytes());
    m.push(CTRL_CMD_GETFAMILY);
    m.push(1); // version
    m.extend_from_slice(&0u16.to_ne_bytes());
    m.extend_from_slice(&(attr_len as u16).to_ne_bytes());
    m.extend_from_slice(&CTRL_ATTR_FAMILY_NAME.to_ne_bytes());
    m.extend_from_slice(family.as_bytes());
    m.push(0);
    m.resize(total, 0);
    m
}

/// Every attribute in `b` as `(type, payload)`, stopping at the first that does
/// not fit.
fn attrs(b: &[u8]) -> Vec<(u16, &[u8])> {
    let mut out = Vec::new();
    let mut at = 0;
    while at + NLA_HDRLEN <= b.len() {
        let (Some(len), Some(ty)) = (u16_at(b, at), u16_at(b, at + 2)) else { break };
        let len = len as usize;
        if len < NLA_HDRLEN || at + len > b.len() {
            break;
        }
        out.push((ty & NLA_TYPE_MASK, &b[at + NLA_HDRLEN..at + len]));
        at += align4(len);
    }
    out
}

/// Every netlink message in one datagram as `(type, payload after the netlink header)`.
fn messages(b: &[u8]) -> Vec<(u16, &[u8])> {
    let mut out = Vec::new();
    let mut at = 0;
    while at + NLMSG_HDRLEN <= b.len() {
        let (Some(len), Some(ty)) = (u32_at(b, at), u16_at(b, at + 4)) else { break };
        let len = len as usize;
        if len < NLMSG_HDRLEN || at + len > b.len() {
            break;
        }
        out.push((ty, &b[at + NLMSG_HDRLEN..at + len]));
        at += align4(len);
    }
    out
}

/// Pure: **the controller's answer to [`getfamily_request`]** — the family id
/// and the id of the multicast group named `group`. A netlink error (the family
/// is not registered: no `CONFIG_ACPI` or no `CONFIG_NET`) is said with its errno.
pub fn parse_family_reply(b: &[u8], group: &str) -> Result<(u16, u32), String> {
    for (ty, body) in messages(b) {
        if ty == NLMSG_ERROR {
            let errno = u32_at(body, 0).map(|e| -(e as i32)).unwrap_or(0);
            return Err(format!("the generic-netlink controller refused the family lookup (errno {errno}: {})", io::Error::from_raw_os_error(errno)));
        }
        if ty != GENL_ID_CTRL || body.len() < GENL_HDRLEN {
            continue;
        }
        let mut id = None;
        let mut grp = None;
        for (at, v) in attrs(&body[GENL_HDRLEN..]) {
            match at {
                CTRL_ATTR_FAMILY_ID => id = u16_at(v, 0),
                CTRL_ATTR_MCAST_GROUPS => {
                    for (_, g) in attrs(v) {
                        let fields = attrs(g);
                        let name = fields.iter().find(|(t, _)| *t == CTRL_ATTR_MCAST_GRP_NAME).map(|(_, n)| c_str(n));
                        let gid = fields.iter().find(|(t, _)| *t == CTRL_ATTR_MCAST_GRP_ID).and_then(|(_, n)| u32_at(n, 0));
                        if name.as_deref() == Some(group) {
                            grp = gid;
                        }
                    }
                }
                _ => {}
            }
        }
        return match (id, grp) {
            (Some(i), Some(g)) => Ok((i, g)),
            (None, _) => Err("the controller's answer names no family id".into()),
            (Some(_), None) => Err(format!("the family has no multicast group `{group}`")),
        };
    }
    Err("the controller answered nothing this module reads".into())
}

/// One ACPI event, as `struct acpi_genl_event` carries it.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct AcpiEvent {
    /// `button/power`, `button/sleep`, `ac_adapter`, …
    pub device_class: String,
    /// `LNXPWRBN:00`, `PNP0C0C:00`, …
    pub bus_id: String,
    /// The ACPI notify value (`0x80` for a button press).
    pub kind: u32,
    /// The driver's datum (the button's press counter).
    pub data: u32,
}

impl AcpiEvent {
    /// Is this the power button?
    pub fn is_power_button(&self) -> bool {
        self.device_class.starts_with(POWER_BUTTON_CLASS)
    }
}

/// Pure: **every ACPI event in one datagram** of family `family`.
pub fn parse_acpi_events(b: &[u8], family: u16) -> Vec<AcpiEvent> {
    let mut out = Vec::new();
    for (ty, body) in messages(b) {
        if ty != family || body.len() < GENL_HDRLEN || body[0] != ACPI_GENL_CMD_EVENT {
            continue;
        }
        for (at, v) in attrs(&body[GENL_HDRLEN..]) {
            if at != ACPI_GENL_ATTR_EVENT || v.len() < ACPI_GENL_EVENT_LEN {
                continue;
            }
            out.push(AcpiEvent {
                device_class: c_str(&v[..20]),
                bus_id: c_str(&v[20..35]),
                kind: u32_at(v, 36).unwrap_or(0),
                data: u32_at(v, 40).unwrap_or(0),
            });
        }
    }
    out
}

/// A socket joined to the ACPI event group.
pub struct PowerButton {
    fd: libc::c_int,
    family: u16,
}

impl Drop for PowerButton {
    fn drop(&mut self) {
        // SAFETY: `fd` is this value's own socket, closed once.
        unsafe { libc::close(self.fd) };
    }
}

fn last_error(what: &str) -> io::Error {
    let e = io::Error::last_os_error();
    io::Error::new(e.kind(), format!("{what}: {e}"))
}

impl PowerButton {
    /// **Open the ACPI event group.** `Err` names the step: no netlink (a
    /// kernel without `CONFIG_NET`), no `acpi_event` family (no `CONFIG_ACPI`),
    /// or the join refused. A caller keeps serving on an `Err` — with one
    /// fewer way to be stopped, and said so.
    pub fn open() -> io::Result<PowerButton> {
        // SAFETY: plain socket(2); the fd is owned by the value returned.
        let fd = unsafe { libc::socket(libc::AF_NETLINK, libc::SOCK_RAW | libc::SOCK_CLOEXEC, libc::NETLINK_GENERIC) };
        if fd < 0 {
            return Err(last_error("socket(AF_NETLINK, NETLINK_GENERIC)"));
        }
        let mut me = PowerButton { fd, family: 0 };
        // SAFETY: a zeroed sockaddr_nl is valid; nl_family set below.
        let mut sa: libc::sockaddr_nl = unsafe { std::mem::zeroed() };
        sa.nl_family = libc::AF_NETLINK as libc::sa_family_t;
        // SAFETY: `sa` is a valid sockaddr_nl of the size passed.
        if unsafe { libc::bind(fd, &sa as *const _ as *const libc::sockaddr, std::mem::size_of::<libc::sockaddr_nl>() as u32) } < 0 {
            return Err(last_error("bind(netlink)"));
        }
        let req = getfamily_request(ACPI_FAMILY, 1);
        // SAFETY: `req` is a live buffer of the length passed.
        if unsafe { libc::send(fd, req.as_ptr() as *const libc::c_void, req.len(), 0) } < 0 {
            return Err(last_error("send(CTRL_CMD_GETFAMILY acpi_event)"));
        }
        let mut buf = vec![0u8; 8192];
        loop {
            // SAFETY: `buf` is a live buffer of the length passed.
            let n = unsafe { libc::recv(fd, buf.as_mut_ptr() as *mut libc::c_void, buf.len(), 0) };
            if n < 0 {
                if io::Error::last_os_error().kind() == io::ErrorKind::Interrupted {
                    continue;
                }
                return Err(last_error("recv(the controller's answer)"));
            }
            let got = &buf[..n as usize];
            if messages(got).iter().all(|(t, _)| *t == NLMSG_DONE) {
                continue;
            }
            let (family, group) = parse_family_reply(got, ACPI_GROUP).map_err(|e| io::Error::new(io::ErrorKind::NotFound, format!("{ACPI_FAMILY}: {e}")))?;
            me.family = family;
            let g: u32 = group;
            // SAFETY: `g` is a live u32 of the size passed.
            if unsafe { libc::setsockopt(fd, libc::SOL_NETLINK, libc::NETLINK_ADD_MEMBERSHIP, &g as *const u32 as *const libc::c_void, 4) } < 0 {
                return Err(last_error("setsockopt(NETLINK_ADD_MEMBERSHIP acpi_mc_group)"));
            }
            return Ok(me);
        }
    }

    /// **Block until the power button is pressed**, and answer the event.
    /// Every other ACPI event (a lid, an AC adapter, a sleep button) is passed
    /// over; an interrupted `recv` is retried.
    pub fn wait(&self) -> io::Result<AcpiEvent> {
        let mut buf = vec![0u8; 8192];
        loop {
            // SAFETY: `buf` is a live buffer of the length passed.
            let n = unsafe { libc::recv(self.fd, buf.as_mut_ptr() as *mut libc::c_void, buf.len(), 0) };
            if n < 0 {
                if io::Error::last_os_error().kind() == io::ErrorKind::Interrupted {
                    continue;
                }
                return Err(last_error("recv(acpi_mc_group)"));
            }
            if let Some(e) = parse_acpi_events(&buf[..n as usize], self.family).into_iter().find(AcpiEvent::is_power_button) {
                return Ok(e);
            }
        }
    }
}

/// **`sync(2)`, then power off** — the end of a clean stop for a PID 1, which
/// must never return from `main` (`Attempted to kill init!`). Parks if the
/// kernel refuses the power-off (not PID 1, no `CAP_SYS_BOOT`), so a caller
/// that is not the init of its namespace does not fall through into a panic.
pub fn power_off() -> ! {
    // SAFETY: sync(2) has no preconditions.
    unsafe { libc::sync() };
    // SAFETY: reboot(2) with the power-off command; on success it does not return.
    unsafe { libc::reboot(libc::LINUX_REBOOT_CMD_POWER_OFF) };
    loop {
        std::thread::sleep(std::time::Duration::from_secs(3600));
    }
}

/// **The steps a clean power-off takes on the data disk** — the kernel's ([`Kernel`]) or
/// a test's.
pub trait QuiesceOps: Send + Sync + 'static {
    /// `sync(2)`: everything in flight, every filesystem.
    fn sync_all(&self);
    /// Point every file descriptor of THIS process that is open for writing on the
    /// filesystem holding `data` at /dev/null (`dup2`: atomic, the numbers are not
    /// reused), so a read-only remount is not refused for writers. Answers how many.
    fn release_writers(&self, data: &std::path::Path) -> usize;
    /// Remount the filesystem holding `data` (its mount point) read-only — the remount
    /// writes XFS's unmount record (`xfs_remount_ro` → `xfs_log_clean`), so the next
    /// mount needs no recovery.
    fn remount_readonly(&self, data: &std::path::Path) -> std::io::Result<()>;
    /// FIFREEZE the filesystem holding `data` — the fallback: Linux 6.12's XFS freeze
    /// only quiesces the log (`xfs_fs_freeze` → `xfs_log_quiesce`, no unmount record), so
    /// the next mount still replays it.
    fn freeze(&self, data: &std::path::Path) -> std::io::Result<()>;
}

/// The kernel's [`QuiesceOps`].
pub struct Kernel;

impl QuiesceOps for Kernel {
    fn sync_all(&self) {
        // SAFETY: sync(2) has no preconditions.
        unsafe { libc::sync() };
    }
    fn release_writers(&self, data: &std::path::Path) -> usize {
        use std::os::unix::fs::MetadataExt as _;
        let Ok(dev) = std::fs::metadata(data).map(|m| m.dev()) else { return 0 };
        let null = match std::fs::OpenOptions::new().read(true).write(true).open("/dev/null") {
            Ok(f) => f,
            Err(_) => return 0,
        };
        use std::os::fd::AsRawFd as _;
        let null_fd = null.as_raw_fd();
        let fds: Vec<i32> = std::fs::read_dir("/proc/self/fd").into_iter().flatten().flatten().filter_map(|e| e.file_name().to_str()?.parse().ok()).collect();
        let mut released = 0;
        for fd in fds {
            if fd == null_fd {
                continue;
            }
            // SAFETY: fcntl/fstat/dup2 on a descriptor number; a closed one only errors.
            unsafe {
                let flags = libc::fcntl(fd, libc::F_GETFL);
                if flags < 0 || flags & libc::O_ACCMODE == libc::O_RDONLY {
                    continue;
                }
                let mut st: libc::stat = std::mem::zeroed();
                if libc::fstat(fd, &mut st) != 0 || st.st_dev as u64 != dev {
                    continue;
                }
                if libc::dup2(null_fd, fd) == fd {
                    released += 1;
                }
            }
        }
        released
    }
    fn remount_readonly(&self, data: &std::path::Path) -> std::io::Result<()> {
        use std::os::unix::ffi::OsStrExt as _;
        let target = std::ffi::CString::new(data.as_os_str().as_bytes()).map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidInput, e))?;
        // A writer still inside its write(2) holds the mount a moment longer: a few tries.
        let mut last = std::io::Error::from_raw_os_error(libc::EBUSY);
        for _ in 0..20 {
            // SAFETY: mount(2) MS_REMOUNT|MS_RDONLY on a NUL-terminated path; no data.
            let rc = unsafe { libc::mount(std::ptr::null(), target.as_ptr(), std::ptr::null(), libc::MS_REMOUNT | libc::MS_RDONLY, std::ptr::null()) };
            if rc == 0 {
                return Ok(());
            }
            last = std::io::Error::last_os_error();
            if last.raw_os_error() != Some(libc::EBUSY) {
                break;
            }
            std::thread::sleep(std::time::Duration::from_millis(100));
        }
        Err(last)
    }
    fn freeze(&self, data: &std::path::Path) -> std::io::Result<()> {
        use std::os::fd::AsRawFd as _;
        let dir = std::fs::File::open(data)?;
        // FIFREEZE = _IOWR('X', 119, int)
        const FIFREEZE: libc::c_ulong = 0xC004_5877;
        let mut arg: libc::c_int = 0;
        // SAFETY: an ioctl on a live directory fd with a pointer to a live int.
        if unsafe { libc::ioctl(dir.as_raw_fd(), FIFREEZE as _, &mut arg) } == 0 {
            Ok(())
        } else {
            Err(std::io::Error::last_os_error())
        }
    }
}

/// **Quiesce `data` before the power goes, its log CLEAN** (2026-09-30 the REAL test2, and
/// 2026-10-01 the KVM proof: `XFS (vda3): Starting recovery` after a stop that FROZE /data
/// — Linux 6.12's freeze writes no unmount record). The steps: `sync(2)`; this process's
/// writers on the data mount released ([`QuiesceOps::release_writers`]); the mount
/// REMOUNTED READ-ONLY, which writes XFS's unmount record; only if that is refused, the
/// freeze (said: the next mount may replay the log). Each step on a thread of its own,
/// bounded by `within` — one that does not come back is said and the power goes anyway —
/// and said through `say` as it happens. Call it LAST, after the stop was said and the
/// log flushed; then [`power_off_quiesced`].
pub fn quiesce_for_power_off(ops: std::sync::Arc<dyn QuiesceOps>, data: &std::path::Path, within: std::time::Duration, say: &dyn Fn(&str)) -> Vec<String> {
    let mut steps = Vec::new();
    let mut step = |line: String| {
        say(&line);
        steps.push(line);
    };
    let bounded = |f: Box<dyn FnOnce() -> String + Send>| -> Option<(String, std::time::Duration)> {
        let (tx, rx) = std::sync::mpsc::channel();
        let t0 = std::time::Instant::now();
        // A32-14, stays a thread: one quiesce step of the power-off, bounded by recv_timeout; the box is going down.
        let _ = std::thread::Builder::new().name("power-off-quiesce".into()).spawn(move || {
            let _ = tx.send(f());
        });
        rx.recv_timeout(within).ok().map(|r| (r, t0.elapsed()))
    };
    let d = data.display().to_string();
    let o = std::sync::Arc::clone(&ops);
    match bounded(Box::new(move || {
        o.sync_all();
        String::new()
    })) {
        Some((_, took)) => step(format!("power-off: sync(2) done in {} ms", took.as_millis())),
        None => step(format!("power-off: sync(2) not back within {} s — the power goes anyway", within.as_secs_f32())),
    }
    let (o, p) = (std::sync::Arc::clone(&ops), data.to_path_buf());
    match bounded(Box::new(move || o.release_writers(&p).to_string())) {
        Some((n, _)) => step(format!("power-off: {n} writer(s) of this process on {d} released (to /dev/null)")),
        None => step(format!("power-off: releasing the writers on {d} not back within {} s", within.as_secs_f32())),
    }
    let (o, p) = (std::sync::Arc::clone(&ops), data.to_path_buf());
    let remounted = match bounded(Box::new(move || match o.remount_readonly(&p) {
        Ok(()) => String::new(),
        Err(e) => e.to_string(),
    })) {
        Some((err, took)) if err.is_empty() => {
            step(format!("power-off: {d} remounted read-only in {} ms — its log is clean, no recovery at the next mount", took.as_millis()));
            true
        }
        Some((err, _)) => {
            step(format!("power-off: {d} NOT remounted read-only ({err}) — the freeze instead"));
            false
        }
        None => {
            step(format!("power-off: the read-only remount of {d} not back within {} s — the freeze instead", within.as_secs_f32()));
            false
        }
    };
    if !remounted {
        let (o, p) = (ops, data.to_path_buf());
        match bounded(Box::new(move || match o.freeze(&p) {
            Ok(()) => String::new(),
            Err(e) => e.to_string(),
        })) {
            Some((err, took)) if err.is_empty() => step(format!("power-off: {d} frozen in {} ms (a freeze writes no unmount record: the next mount may replay its log)", took.as_millis())),
            Some((err, _)) => step(format!("power-off: {d} NOT frozen ({err}) — the next mount replays its log")),
            None => step(format!("power-off: the freeze of {d} not back within {} s — the power goes anyway", within.as_secs_f32())),
        }
    }
    steps
}

/// **Power off after [`quiesce_for_power_off`]**: `reboot(RB_POWER_OFF)` with NO
/// `sync(2)` — the disk was synced and frozen, and a sync on a filesystem that is (still)
/// freezing waits for the freeze. Parks like [`power_off`] if the kernel refuses.
pub fn power_off_quiesced() -> ! {
    // SAFETY: reboot(2) with the power-off command; on success it does not return.
    unsafe { libc::reboot(libc::LINUX_REBOOT_CMD_POWER_OFF) };
    loop {
        std::thread::sleep(std::time::Duration::from_secs(3600));
    }
}

/// **`sync(2)`, then restart** (`reboot(RB_AUTOBOOT)`): the end of a fault on a box that
/// was SERVING ([`crate::fault::End::Restart`], Gunnar's `narrate::restart_now`). Parks
/// if the kernel refuses, like [`power_off`].
pub fn restart_now() -> ! {
    // SAFETY: sync(2) has no preconditions.
    unsafe { libc::sync() };
    // SAFETY: reboot(2) with the restart command; on success it does not return.
    unsafe { libc::reboot(libc::LINUX_REBOOT_CMD_RESTART) };
    loop {
        std::thread::sleep(std::time::Duration::from_secs(3600));
    }
}

/// How long each quiesce step may take before the power goes anyway (Gunnar f8594dd6:
/// a `sync(2)` under load took 1 min 53 s and the box was hard-stopped mid-stop).
pub const QUIESCE_WITHIN: std::time::Duration = std::time::Duration::from_secs(10);

/// **The stop's last two duties, without the power**: [`quiesce_for_power_off`] on
/// `data` (each step bounded by `within` and said), then ONE line — `said`, then the
/// steps — handed to `keep` (the ESP record, [`crate::stop_record::keep_at`]). Returns
/// that line.
pub fn quiesce_and_record(ops: std::sync::Arc<dyn QuiesceOps>, data: &std::path::Path, within: std::time::Duration, said: &str, say: &dyn Fn(&str), keep: &dyn Fn(&str) -> bool) -> String {
    let steps = quiesce_for_power_off(ops, data, within, say);
    let line = format!("{said} — {}", steps.join("; "));
    if !keep(&line) {
        say("power-off: the stop record could not be kept on the ESP");
    }
    line
}

/// **The end of a clean stop for a PID 1** (Gunnar 812f4ffc / dcf1d82f, both products):
/// `data` quiesced ([`QUIESCE_WITHIN`] per step: sync, writers released, the read-only
/// remount that writes XFS's unmount record, a freeze only if that is refused), the
/// stop kept on the ESP as `product`'s record ([`crate::stop_record::record_for`]), then
/// the power with no sync after it ([`power_off_quiesced`]). Say the stop BEFORE calling
/// this: the disk log dies with the remount.
pub fn power_off_cleanly(product: &str, data: &std::path::Path, said: &str, say: &dyn Fn(&str)) -> ! {
    let record = crate::stop_record::record_for(product);
    let esp = std::path::Path::new(crate::stop_record::ESP_MOUNT);
    quiesce_and_record(std::sync::Arc::new(Kernel), data, QUIESCE_WITHIN, said, say, &|line| crate::stop_record::keep_at(&record, esp, line));
    power_off_quiesced()
}

#[cfg(test)]
mod tests {
    use super::*;

    fn nlmsg(ty: u16, body: &[u8]) -> Vec<u8> {
        let len = NLMSG_HDRLEN + body.len();
        let mut m = Vec::new();
        m.extend_from_slice(&(len as u32).to_ne_bytes());
        m.extend_from_slice(&ty.to_ne_bytes());
        m.extend_from_slice(&0u16.to_ne_bytes());
        m.extend_from_slice(&0u32.to_ne_bytes());
        m.extend_from_slice(&0u32.to_ne_bytes());
        m.extend_from_slice(body);
        m.resize(align4(m.len()), 0);
        m
    }

    fn nla(ty: u16, payload: &[u8]) -> Vec<u8> {
        let mut a = Vec::new();
        a.extend_from_slice(&((NLA_HDRLEN + payload.len()) as u16).to_ne_bytes());
        a.extend_from_slice(&ty.to_ne_bytes());
        a.extend_from_slice(payload);
        a.resize(align4(a.len()), 0);
        a
    }

    fn genl(cmd: u8, attrs: &[Vec<u8>]) -> Vec<u8> {
        let mut b = vec![cmd, 1, 0, 0];
        for a in attrs {
            b.extend_from_slice(a);
        }
        b
    }

    fn event(class: &str, bus: &str, kind: u32, data: u32) -> Vec<u8> {
        let mut e = vec![0u8; ACPI_GENL_EVENT_LEN];
        e[..class.len()].copy_from_slice(class.as_bytes());
        e[20..20 + bus.len()].copy_from_slice(bus.as_bytes());
        e[36..40].copy_from_slice(&kind.to_ne_bytes());
        e[40..44].copy_from_slice(&data.to_ne_bytes());
        e
    }

    #[test]
    fn the_family_request_is_one_aligned_getfamily_naming_acpi_event() {
        let r = getfamily_request(ACPI_FAMILY, 7);
        assert_eq!(r.len() % 4, 0);
        assert_eq!(u32_at(&r, 0).unwrap() as usize, r.len());
        assert_eq!(u16_at(&r, 4), Some(GENL_ID_CTRL));
        assert_eq!(u32_at(&r, 8), Some(7));
        assert_eq!(r[16], CTRL_CMD_GETFAMILY);
        let a = attrs(&r[20..]);
        assert_eq!(a.len(), 1);
        assert_eq!((a[0].0, c_str(a[0].1).as_str()), (CTRL_ATTR_FAMILY_NAME, "acpi_event"));
    }

    #[test]
    fn the_controllers_answer_gives_the_family_and_the_group() {
        let grp = |name: &str, id: u32| {
            let mut n = name.as_bytes().to_vec();
            n.push(0);
            let mut g = nla(CTRL_ATTR_MCAST_GRP_ID, &id.to_ne_bytes());
            g.extend(nla(CTRL_ATTR_MCAST_GRP_NAME, &n));
            g
        };
        let mut groups = nla(1, &grp("other", 3));
        groups.extend(nla(2, &grp(ACPI_GROUP, 9)));
        let body = genl(1, &[nla(CTRL_ATTR_FAMILY_NAME, b"acpi_event\0"), nla(CTRL_ATTR_FAMILY_ID, &27u16.to_ne_bytes()), nla(CTRL_ATTR_MCAST_GROUPS, &groups)]);
        assert_eq!(parse_family_reply(&nlmsg(GENL_ID_CTRL, &body), ACPI_GROUP), Ok((27, 9)));
        assert!(parse_family_reply(&nlmsg(GENL_ID_CTRL, &body), "nope").unwrap_err().contains("no multicast group `nope`"));
        // ENOENT: the kernel registered no such family.
        let err = nlmsg(NLMSG_ERROR, &(-2i32).to_ne_bytes());
        assert!(parse_family_reply(&err, ACPI_GROUP).unwrap_err().contains("errno 2"));
    }

    #[test]
    fn a_power_button_press_is_found_and_other_events_passed_over() {
        let mut dgram = nlmsg(27, &genl(ACPI_GENL_CMD_EVENT, &[nla(ACPI_GENL_ATTR_EVENT, &event("ac_adapter", "ACPI0003:00", 0x80, 1))]));
        dgram.extend(nlmsg(27, &genl(ACPI_GENL_CMD_EVENT, &[nla(ACPI_GENL_ATTR_EVENT, &event("button/power", "LNXPWRBN:00", 0x80, 1))])));
        // Another family's message in the same datagram is not ours.
        dgram.extend(nlmsg(28, &genl(ACPI_GENL_CMD_EVENT, &[nla(ACPI_GENL_ATTR_EVENT, &event("button/power", "X", 0x80, 2))])));
        let ev = parse_acpi_events(&dgram, 27);
        assert_eq!(ev.len(), 2);
        assert!(!ev[0].is_power_button());
        assert_eq!(ev[1], AcpiEvent { device_class: "button/power".into(), bus_id: "LNXPWRBN:00".into(), kind: 0x80, data: 1 });
        assert!(ev[1].is_power_button());
        // A truncated event is not invented.
        let short = nlmsg(27, &genl(ACPI_GENL_CMD_EVENT, &[nla(ACPI_GENL_ATTR_EVENT, &[0u8; 10])]));
        assert!(parse_acpi_events(&short, 27).is_empty());
    }

    struct FakeDisk(std::sync::Mutex<Vec<&'static str>>);
    impl QuiesceOps for FakeDisk {
        fn sync_all(&self) {
            self.0.lock().unwrap().push("sync");
        }
        fn release_writers(&self, _: &std::path::Path) -> usize {
            self.0.lock().unwrap().push("release");
            2
        }
        fn remount_readonly(&self, _: &std::path::Path) -> std::io::Result<()> {
            self.0.lock().unwrap().push("remount");
            Ok(())
        }
        fn freeze(&self, _: &std::path::Path) -> std::io::Result<()> {
            self.0.lock().unwrap().push("freeze");
            Ok(())
        }
    }

    /// ★ **Row 18: a clean stop quiesces /data (bounded at 10 s), then keeps the stop on
    /// the ESP, and only then the power goes** (Gunnar f8594dd6 / 812f4ffc / dcf1d82f).
    /// Holger called a bare `power_off` (sync + reboot): its next mount replayed the log,
    /// and no record said when the guest powered off.
    #[test]
    fn a_clean_stop_quiesces_the_data_disk_then_keeps_its_steps_on_the_esp() {
        assert_eq!(QUIESCE_WITHIN, std::time::Duration::from_secs(10));
        let disk = std::sync::Arc::new(FakeDisk(std::sync::Mutex::new(Vec::new())));
        let kept = std::sync::Mutex::new(Vec::<String>::new());
        let said = std::sync::Mutex::new(Vec::<String>::new());
        let line = quiesce_and_record(disk.clone(), std::path::Path::new("/data"), QUIESCE_WITHIN, "holger: stopped cleanly", &|l: &str| said.lock().unwrap().push(l.to_string()), &|l: &str| {
            kept.lock().unwrap().push(l.to_string());
            true
        });
        assert_eq!(*disk.0.lock().unwrap(), vec!["sync", "release", "remount"], "no freeze when the remount took");
        let kept = kept.lock().unwrap();
        assert_eq!(kept.len(), 1, "one record, after the quiesce");
        assert_eq!(kept[0], line);
        assert!(line.starts_with("holger: stopped cleanly — power-off: sync(2) done in"), "{line}");
        assert!(line.contains("2 writer(s)") && line.contains("remounted read-only"), "{line}");
        assert_eq!(said.lock().unwrap().len(), 3, "each step said as it happens");
    }

    /// On a development machine the family is usually registered (any kernel
    /// with ACPI and NET): opening proves the lookup and the join against the
    /// REAL kernel. A kernel without it answers a named error, never a panic.
    #[test]
    fn opening_against_this_kernel_joins_or_says_why() {
        match PowerButton::open() {
            Ok(p) => {
                eprintln!("joined {ACPI_FAMILY} (family id {}) on this kernel", p.family);
                assert!(p.family > GENL_ID_CTRL, "a dynamic family id: {}", p.family)
            }
            Err(e) => {
                eprintln!("this kernel has no {ACPI_FAMILY} to join: {e}");
                assert!(!e.to_string().is_empty())
            }
        }
    }
}