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())
}
}
}
}