Publish harness and TUI open-source

initial sync from the monorepo
This commit is contained in:
grokkybara[bot] 2026-07-16 06:46:02 +01:00
commit c68e39f604
2734 changed files with 1437016 additions and 0 deletions

View file

@ -0,0 +1,23 @@
[package]
name = "xai-system-power"
version = "0.1.0"
edition.workspace = true
license = "Apache-2.0"
description = "Cross-platform system sleep/wake (suspend) notifications — used to defer work across a suspend boundary"
[dependencies]
[target.'cfg(target_os = "linux")'.dependencies]
zbus = { workspace = true }
[target.'cfg(target_os = "windows")'.dependencies]
windows-sys = { version = "0.59", features = [
"Win32_Foundation",
"Win32_System_Power",
# Provides REGISTER_NOTIFICATION_FLAGS / DEVICE_NOTIFY_CALLBACK and gates
# PowerRegisterSuspendResumeNotification (its flags param lives here).
"Win32_UI_WindowsAndMessaging",
] }
[lints]
workspace = true

View file

@ -0,0 +1,180 @@
//! Cross-platform system **sleep/wake** (suspend/resume) notifications.
//!
//! The motivating use case: an OIDC token refresh that is *in flight when the
//! laptop sleeps* can lose its rotated successor token (the server processes
//! the request, rotates/revokes the old refresh token, and the response is
//! lost across the suspend). On wake the client is holding a dead refresh
//! token and the user is forced to re-login. See
//! `xai-grok-shell`'s `AuthManager` sleep gate, which consumes these events to
//! avoid *starting* a refresh just before sleep. An in-flight refresh is
//! deliberately left to finish, never aborted (dropping it could discard a
//! rotated-token response and cause the very revocation this guards against);
//! instead, its [`PowerEvent::WillSleep`] handler may block briefly (bounded)
//! to hold off the suspend until that in-flight refresh completes — see the
//! callback contract below.
//!
//! This crate exposes a single tiny abstraction — [`SystemPowerListener`] —
//! with per-OS implementations behind `#[cfg]` and a no-op fallback:
//!
//! | OS | Mechanism |
//! |---------|----------------------------------------------------------------------|
//! | macOS | IOKit `IORegisterForSystemPower` on a dedicated `CFRunLoop` thread |
//! | Windows | `PowerRegisterSuspendResumeNotification` (`DEVICE_NOTIFY_CALLBACK`) |
//! | Linux | logind D-Bus `PrepareForSleep` signal + a `delay` inhibitor lock |
//! | other | no-op (returns `None` from [`SystemPowerListener::start`]) |
//!
//! The callback fires from a platform event thread/callback, so it must be
//! `Send + Sync`. It should return promptly, but a [`PowerEvent::WillSleep`]
//! handler *may* block for a short, bounded time to hold off sleep: the per-OS
//! implementations acknowledge the transition only **after** the callback
//! returns (macOS calls `IOAllowPowerChange`; Linux releases its `delay`
//! inhibitor), so blocking there delays the suspend itself. Keep any such block
//! within the OS budget — macOS allows ~30 s after `kIOMessageSystemWillSleep`;
//! Linux logind's `InhibitDelayMaxSec` defaults to 5 s — or the OS proceeds to
//! sleep anyway. `DidWake` handlers must stay cheap and non-blocking.
/// A system power transition.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum PowerEvent {
/// The system is about to sleep (lid close / suspend), or — on macOS — is
/// *negotiating* an idle sleep (`kIOMessageCanSystemSleep`), which may
/// follow within seconds. Best-effort: on macOS and Linux there is a short
/// window to react before sleep proceeds, and the handler may block within
/// it to hold off the suspend (see the crate-level callback contract); on
/// Windows modern-standby it may not wait at all.
///
/// Because the idle-sleep negotiation can be vetoed (by any power client),
/// a `WillSleep` is **not** a guarantee that sleep follows: it may be
/// succeeded by a [`Self::DidWake`] without an intervening suspend.
/// Handlers must therefore be idempotent and safe to "cancel" via
/// `DidWake`.
WillSleep,
/// The system resumed from sleep, or a previously announced sleep was
/// cancelled (macOS `kIOMessageSystemWillNotSleep` after a vetoed
/// idle-sleep query). Both mean "not sleeping (anymore)".
DidWake,
}
/// Boxed user callback invoked on each [`PowerEvent`].
pub type PowerCallback = Box<dyn Fn(PowerEvent) + Send + Sync + 'static>;
/// A coarse, synchronously-queryable system power state (see
/// [`current_power_state`]).
///
/// The motivating distinction is **dark wake**: on macOS the system wakes
/// briefly for background/maintenance work (Power Nap, network/disk
/// maintenance) with the display off and no user present, then re-sleeps —
/// frequently *without* delivering a [`PowerEvent`] at all (the legacy
/// `IORegisterForSystemPower` notifications used by [`SystemPowerListener`] are
/// blind to dark wakes). Code that starts irreversible network work — notably a
/// one-time-use OIDC refresh-token exchange — should avoid doing so during a
/// dark wake, because the machine may re-sleep mid-request and lose the
/// response that carries the rotated token.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum PowerState {
/// Full / user wake: display (graphics) capability present — a user is (or
/// can be) present. Safe to start irreversible work.
FullWake,
/// Dark wake: CPU (and usually network/disk) up for background or
/// maintenance work, but display off and no user. The system may re-sleep
/// at any moment with no warning.
DarkWake,
/// State could not be determined: an unsupported OS, or the platform query
/// failed / returned a transitional sample. Callers should treat this as
/// "no signal" and fall back to their existing behavior — never block on
/// it.
Unknown,
}
/// Query the current system power state synchronously.
///
/// Cheap, non-blocking, and never panics. Returns [`PowerState::Unknown`] on
/// platforms without a real implementation (currently everything except macOS)
/// or when the platform query fails. Unlike [`SystemPowerListener`], this needs
/// no running listener — on macOS it is a single connection-less IOKit call.
pub fn current_power_state() -> PowerState {
imp::current_power_state()
}
#[cfg(target_os = "macos")]
#[path = "macos.rs"]
mod imp;
#[cfg(target_os = "windows")]
#[path = "windows.rs"]
mod imp;
#[cfg(target_os = "linux")]
#[path = "linux.rs"]
mod imp;
#[cfg(not(any(target_os = "macos", target_os = "windows", target_os = "linux")))]
mod imp {
use super::PowerCallback;
pub(crate) struct Listener;
impl Listener {
pub(crate) fn start(_callback: PowerCallback) -> Option<Self> {
None
}
}
pub(crate) fn current_power_state() -> super::PowerState {
super::PowerState::Unknown
}
}
/// A running system-power listener. On macOS/Windows, dropping it stops the
/// listener and releases its OS resources. On Linux the worker parks on a
/// blocking logind signal and cannot be cleanly interrupted, so it runs (with
/// its D-Bus connection + sleep-delay inhibitor) until process exit — see the
/// `linux` module. Intended as a process-lifetime singleton.
pub struct SystemPowerListener {
// Kept for its `Drop`; the field is read on platforms with a real impl.
#[allow(dead_code)]
inner: imp::Listener,
}
impl SystemPowerListener {
/// Start listening for system sleep/wake events.
///
/// Returns `None` when the platform mechanism is unavailable — an
/// unsupported OS, a missing systemd-logind on Linux, or a registration
/// failure. Callers should treat `None` as "no power notifications" and
/// degrade gracefully (the dependent feature simply does not engage).
///
/// `callback` is invoked from a platform event thread, so it must be
/// `Send + Sync`, cheap, and non-blocking.
pub fn start<F>(callback: F) -> Option<Self>
where
F: Fn(PowerEvent) + Send + Sync + 'static,
{
imp::Listener::start(Box::new(callback)).map(|inner| Self { inner })
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn power_event_is_copy_eq() {
let e = PowerEvent::WillSleep;
let copied = e; // Copy
assert_eq!(e, copied);
assert_ne!(PowerEvent::WillSleep, PowerEvent::DidWake);
}
/// `start` + `drop` must be clean on every platform: no panic and no hang
/// (the latter exercises the macOS run-loop teardown). On Linux/Windows CI
/// `start` may return `None` (no system bus / unsupported) — also fine.
#[test]
fn start_and_drop_is_clean() {
// Bind and let it drop at end of scope rather than calling `drop()`:
// on platforms where the listener owns no `Drop` type (e.g. Linux,
// whose worker is detached and runs until process exit) an explicit
// `drop()` trips `clippy::drop_non_drop`.
let _listener = SystemPowerListener::start(|_event| {});
}
}

View file

@ -0,0 +1,93 @@
//! Linux system sleep/wake via systemd-logind's `PrepareForSleep` D-Bus
//! signal, with a `delay` inhibitor lock so we get a short window to react
//! before the system actually sleeps.
//!
//! Uses the `zbus` blocking API on a dedicated thread so we don't require the
//! caller to run any particular async runtime. If the system bus or logind is
//! unavailable (non-systemd distro, container, permission error), `start`
//! returns `None` and the caller degrades gracefully.
use std::thread;
use super::{PowerCallback, PowerEvent};
const DEST: &str = "org.freedesktop.login1";
const PATH: &str = "/org/freedesktop/login1";
const IFACE: &str = "org.freedesktop.login1.Manager";
/// Linux listener handle.
///
/// There is intentionally no clean stop: the worker thread parks on a blocking
/// logind signal iterator, which cannot be interrupted without a signal
/// arriving, so dropping this neither joins nor cancels it. The thread (and its
/// D-Bus connection + sleep-delay inhibitor fd) live until process exit. That
/// is acceptable for the only intended use — a single process-lifetime listener
/// whose callback holds a `Weak` ref and no-ops once the owner is gone. (macOS
/// can `CFRunLoopStop` from `Drop` and so joins; Linux cannot — hence the
/// asymmetry, and why there is no `Drop` impl here.)
pub(crate) struct Listener;
impl Listener {
pub(crate) fn start(callback: PowerCallback) -> Option<Self> {
// Probe synchronously so registration failures return `None` to the
// caller rather than dying silently on the worker thread.
let conn = zbus::blocking::Connection::system().ok()?;
let proxy = zbus::blocking::Proxy::new(&conn, DEST, PATH, IFACE).ok()?;
let signals = proxy.receive_signal("PrepareForSleep").ok()?;
thread::Builder::new()
.name("xai-power-listener".into())
.spawn(move || run_thread(proxy, signals, callback))
.ok()?;
Some(Self)
}
}
/// Take a `delay` sleep inhibitor: logind holds off sleep until the returned fd
/// drops. `None` if unavailable — we then react without a pre-sleep window.
fn take_inhibitor(proxy: &zbus::blocking::Proxy<'_>) -> Option<zbus::zvariant::OwnedFd> {
proxy
.call(
"Inhibit",
&("sleep", "grok", "Pause token refresh across sleep", "delay"),
)
.ok()
}
fn run_thread(
proxy: zbus::blocking::Proxy<'static>,
signals: zbus::blocking::proxy::SignalIterator<'static>,
callback: PowerCallback,
) {
// Hold the delay lock so the first PrepareForSleep(true) gives us a window.
let mut inhibitor = take_inhibitor(&proxy);
for msg in signals {
let Ok(about_to_sleep) = msg.body().deserialize::<bool>() else {
continue;
};
if about_to_sleep {
// The callback may block (bounded) waiting for an in-flight token
// refresh to finish; the `delay` inhibitor is still held across it,
// so that wait holds off the suspend (up to logind's
// `InhibitDelayMaxSec`, default 5 s). Release it only once the
// callback returns so the system can then proceed to sleep.
callback(PowerEvent::WillSleep);
inhibitor = None;
} else {
callback(PowerEvent::DidWake);
// Re-acquire the delay lock for the next sleep cycle.
inhibitor = take_inhibitor(&proxy);
}
}
drop(inhibitor);
}
pub(crate) fn current_power_state() -> crate::PowerState {
// Linux has no "dark wake" equivalent to query (the system is either
// suspended or fully awake); report Unknown so callers fall back to the
// logind `PrepareForSleep` path.
crate::PowerState::Unknown
}

View file

@ -0,0 +1,367 @@
//! macOS system sleep/wake via IOKit `IORegisterForSystemPower`.
//!
//! IOKit delivers power notifications through a `CFRunLoop` source, so we run a
//! dedicated thread whose run loop receives the callbacks. The thread owns all
//! IOKit resources for their full lifetime and tears them down after the run
//! loop is stopped (from `Drop`).
//!
//! FFI is declared directly (CoreFoundation + IOKit frameworks) to avoid a
//! `core-foundation` crate dependency for this tiny surface. The opaque CF
//! types (`CFRunLoopRef`, `CFRunLoopSourceRef`, `CFRunLoopMode`) are pointers.
use std::os::raw::c_void;
use std::sync::Arc;
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::mpsc;
use std::thread;
use super::{PowerCallback, PowerEvent, PowerState};
// `io_object_t` / `io_connect_t` are `mach_port_t` == `unsigned int`.
type MachPort = u32;
const MACH_PORT_NULL: MachPort = 0;
// IOKit power-management message types (IOMessage.h).
const K_IO_MESSAGE_CAN_SYSTEM_SLEEP: u32 = 0xe000_0270;
const K_IO_MESSAGE_SYSTEM_WILL_SLEEP: u32 = 0xe000_0280;
const K_IO_MESSAGE_SYSTEM_WILL_NOT_SLEEP: u32 = 0xe000_0290;
const K_IO_MESSAGE_SYSTEM_HAS_POWERED_ON: u32 = 0xe000_0300;
// IOPM system-power capability bits (`IOPMCapabilityBits`). These constants and
// the `IOPMConnectionGetSystemCapabilities` query below are **SPI**: declared in
// the *private* `IOPMLibPrivate.h` (IOKitUser), not the public `IOPMLib.h` that
// ships in the SDK. A dark wake has CPU (and usually network/disk) but *not*
// video: the system is up for background maintenance with the display off. A
// full/user wake additionally carries the video capability. (See
// `crate::PowerState` for the canonical dark-wake explanation.)
const K_IOPM_CAPABILITY_CPU: u32 = 0x1;
const K_IOPM_CAPABILITY_VIDEO: u32 = 0x2;
type IoServiceInterestCallback = extern "C" fn(
refcon: *mut c_void,
service: MachPort,
message_type: u32,
message_argument: *mut c_void,
);
#[link(name = "CoreFoundation", kind = "framework")]
unsafe extern "C" {
static kCFRunLoopCommonModes: *const c_void; // CFRunLoopMode (CFStringRef)
static kCFRunLoopDefaultMode: *const c_void; // CFRunLoopMode (CFStringRef)
fn CFRunLoopGetCurrent() -> *mut c_void;
fn CFRunLoopRunInMode(
mode: *const c_void,
seconds: f64,
return_after_source_handled: u8,
) -> i32;
fn CFRunLoopStop(rl: *mut c_void);
fn CFRunLoopAddSource(rl: *mut c_void, source: *mut c_void, mode: *const c_void);
}
#[link(name = "IOKit", kind = "framework")]
unsafe extern "C" {
fn IORegisterForSystemPower(
refcon: *mut c_void,
the_port_ref: *mut *mut c_void,
callback: IoServiceInterestCallback,
notifier: *mut MachPort,
) -> MachPort;
fn IODeregisterForSystemPower(notifier: *mut MachPort) -> i32;
fn IONotificationPortGetRunLoopSource(port: *mut c_void) -> *mut c_void;
fn IONotificationPortDestroy(port: *mut c_void);
fn IOAllowPowerChange(kern_port: MachPort, notification_id: isize) -> i32;
fn IOServiceClose(connect: MachPort) -> i32;
// `IOPMCapabilityBits IOPMConnectionGetSystemCapabilities(void)` — an
// undeclared **SPI** symbol: exported by IOKit but prototyped only in the
// private `IOPMLibPrivate.h`, not the public SDK. Despite the "Connection"
// in the name the real prototype takes **no** arguments (it reads global
// state — no `IOPMConnectionCreate`, no run loop, no acknowledgment), so
// this zero-arg declaration matches the ABI: a cheap synchronous read of
// the current power state.
fn IOPMConnectionGetSystemCapabilities() -> u32;
}
/// Classify raw IOPM capability bits into a coarse [`PowerState`].
///
/// - no CPU bit → [`PowerState::Unknown`]: we only ever call this while the
/// process is executing, so a missing CPU bit is a transitional / bogus
/// sample. Fail open so callers keep their existing behavior rather than
/// blocking on a bad read.
/// - CPU + video → [`PowerState::FullWake`].
/// - CPU, no video → [`PowerState::DarkWake`].
///
/// Note an idle *display sleep* while the system is otherwise fully awake keeps
/// the system-level video capability set (the system can drive graphics on
/// demand), so it classifies as `FullWake`, not `DarkWake` — only a real dark
/// wake from sleep drops the video capability.
fn classify_capabilities(caps: u32) -> PowerState {
if caps & K_IOPM_CAPABILITY_CPU == 0 {
return PowerState::Unknown;
}
if caps & K_IOPM_CAPABILITY_VIDEO != 0 {
PowerState::FullWake
} else {
PowerState::DarkWake
}
}
pub(crate) fn current_power_state() -> PowerState {
// Safe: the C function takes no arguments and returns a plain bitfield.
let caps = unsafe { IOPMConnectionGetSystemCapabilities() };
// IOKit also exports `IOPMIsADarkWake(IOPMCapabilityBits)` /
// `IOPMIsAUserWake(IOPMCapabilityBits)` (also `IOPMLibPrivate.h` SPI), which
// classify these bits directly. We classify them ourselves so the mapping
// stays a pure, unit-tested function (`classify_capabilities`) and so we
// control the fail-open-to-`Unknown` behavior on a missing CPU bit, which
// those predicates don't express.
classify_capabilities(caps)
}
/// Lives for the duration of the run loop; pointed to by the IOKit `refcon`.
/// Only touched from the run-loop thread (registration sets `root_port`
/// before the loop runs; the callback reads both fields on that same thread).
struct Context {
callback: PowerCallback,
root_port: MachPort,
}
/// `CFRunLoopRef` is safe to call `CFRunLoopStop` on from another thread.
struct SendRunLoop(*mut c_void);
unsafe impl Send for SendRunLoop {}
pub(crate) struct Listener {
runloop: SendRunLoop,
stop: Arc<AtomicBool>,
handle: Option<thread::JoinHandle<()>>,
}
impl Listener {
pub(crate) fn start(callback: PowerCallback) -> Option<Self> {
let (tx, rx) = mpsc::channel::<Option<SendRunLoop>>();
let stop = Arc::new(AtomicBool::new(false));
let stop_thread = stop.clone();
let handle = thread::Builder::new()
.name("xai-power-listener".into())
.spawn(move || run_thread(callback, tx, stop_thread))
.ok()?;
// Block until the thread has registered (or failed). This keeps the
// returned handle meaningful and lets us return `None` on failure.
match rx.recv() {
Ok(Some(runloop)) => Some(Self {
runloop,
stop,
handle: Some(handle),
}),
_ => {
let _ = handle.join();
None
}
}
}
}
impl Drop for Listener {
fn drop(&mut self) {
// Signal stop, then wake the run loop so the thread exits promptly and
// tears down IOKit resources. The stop flag also covers the race where
// `CFRunLoopStop` arrives before the loop starts (the timed
// `CFRunLoopRunInMode` re-checks the flag).
self.stop.store(true, Ordering::SeqCst);
unsafe { CFRunLoopStop(self.runloop.0) };
if let Some(handle) = self.handle.take() {
let _ = handle.join();
}
}
}
fn run_thread(
callback: PowerCallback,
tx: mpsc::Sender<Option<SendRunLoop>>,
stop: Arc<AtomicBool>,
) {
let ctx = Box::into_raw(Box::new(Context {
callback,
root_port: MACH_PORT_NULL,
}));
let mut notifier: MachPort = MACH_PORT_NULL;
let mut port: *mut c_void = std::ptr::null_mut();
let root_port = unsafe {
IORegisterForSystemPower(ctx as *mut c_void, &mut port, power_callback, &mut notifier)
};
if root_port == MACH_PORT_NULL || port.is_null() {
// Registration failed — reclaim the context and report failure.
unsafe { drop(Box::from_raw(ctx)) };
let _ = tx.send(None);
return;
}
// Safe: the callback cannot fire until the run loop runs, below.
unsafe { (*ctx).root_port = root_port };
let runloop = unsafe { CFRunLoopGetCurrent() };
unsafe {
let source = IONotificationPortGetRunLoopSource(port);
CFRunLoopAddSource(runloop, source, kCFRunLoopCommonModes);
}
if tx.send(Some(SendRunLoop(runloop))).is_err() {
// Receiver gone (start() bailed) — clean up and exit without running.
unsafe {
IODeregisterForSystemPower(&mut notifier);
IONotificationPortDestroy(port);
IOServiceClose(root_port);
drop(Box::from_raw(ctx));
}
return;
}
// Service power notifications until stopped. `Drop` calls `CFRunLoopStop`,
// which wakes this immediately; the finite (rather than infinite) timeout
// only exists to cover the rare race where `CFRunLoopStop` arrives before
// the loop starts. A long interval keeps idle wakeups negligible without
// delaying normal teardown.
while !stop.load(Ordering::SeqCst) {
unsafe { CFRunLoopRunInMode(kCFRunLoopDefaultMode, 5.0, 0) };
}
// Run loop stopped: tear down IOKit resources and the context.
unsafe {
IODeregisterForSystemPower(&mut notifier);
IONotificationPortDestroy(port);
IOServiceClose(root_port);
drop(Box::from_raw(ctx));
}
}
/// Pure mapping of an IOKit power message to the [`PowerEvent`] delivered to
/// the user callback (if any) and whether the message requires an
/// `IOAllowPowerChange` acknowledgment. Split from [`power_callback`] so the
/// mapping is unit-testable without IOKit ports.
///
/// - `CAN_SYSTEM_SLEEP` (idle-sleep query) maps to [`PowerEvent::WillSleep`]:
/// an idle sleep may follow within seconds, so consumers must treat it
/// exactly like an announced sleep — the auth sleep gate must already be up
/// (and in-flight token refreshes drained, via the bounded blocking callback)
/// *before* we permit the transition. We never veto; the callback runs, then
/// the ack allows the sleep. If the sleep is vetoed by another client,
/// `SYSTEM_WILL_NOT_SLEEP` arrives and maps to [`PowerEvent::DidWake`]
/// (transition cancelled — same "not sleeping anymore" meaning), lowering the
/// gate; if it proceeds, the later `SYSTEM_WILL_SLEEP` re-raises it
/// (idempotent, and its drain-wait finds the in-flight counter already at
/// zero).
/// - `SYSTEM_WILL_NOT_SLEEP` requires no ack (informational).
fn map_power_message(message_type: u32) -> (Option<PowerEvent>, bool) {
match message_type {
K_IO_MESSAGE_CAN_SYSTEM_SLEEP => (Some(PowerEvent::WillSleep), true),
K_IO_MESSAGE_SYSTEM_WILL_SLEEP => (Some(PowerEvent::WillSleep), true),
K_IO_MESSAGE_SYSTEM_WILL_NOT_SLEEP => (Some(PowerEvent::DidWake), false),
K_IO_MESSAGE_SYSTEM_HAS_POWERED_ON => (Some(PowerEvent::DidWake), false),
_ => (None, false),
}
}
extern "C" fn power_callback(
refcon: *mut c_void,
_service: MachPort,
message_type: u32,
message_argument: *mut c_void,
) {
// Safe: `refcon` is the live `Context` for this run-loop thread.
let ctx = unsafe { &*(refcon as *const Context) };
let (event, needs_ack) = map_power_message(message_type);
if let Some(event) = event {
// For sleep-bound messages the ack is sent only *after* the callback
// returns: a `WillSleep` handler may block (bounded) waiting for an
// in-flight token refresh to finish, which intentionally delays the
// `IOAllowPowerChange` and holds off the suspend. IOKit allows ~30 s
// per phase before forcing sleep, so a bounded wait is safe. See the
// `xai_system_power` crate-level callback contract.
(ctx.callback)(event);
}
if needs_ack {
unsafe { IOAllowPowerChange(ctx.root_port, message_argument as isize) };
}
}
#[cfg(test)]
mod tests {
use super::*;
// Network (0x8) + disk (0x10): the `kIOPMCapabilityNetwork` /
// `kIOPMCapabilityDisk` bits a real dark/full wake typically also carries.
// Named here so the classifier inputs mirror real
// `IOPMConnectionGetSystemCapabilities` samples, not just the CPU/video bits
// `classify_capabilities` keys on.
const K_IOPM_CAPABILITY_NETWORK: u32 = 0x8;
const K_IOPM_CAPABILITY_DISK: u32 = 0x10;
#[test]
fn classify_full_wake_has_video() {
// CPU + video (+ network/disk) => full/user wake.
let caps = K_IOPM_CAPABILITY_CPU
| K_IOPM_CAPABILITY_VIDEO
| K_IOPM_CAPABILITY_NETWORK
| K_IOPM_CAPABILITY_DISK;
assert_eq!(classify_capabilities(caps), PowerState::FullWake);
}
#[test]
fn classify_dark_wake_cpu_without_video() {
// CPU + network/disk but no video => dark wake.
assert_eq!(
classify_capabilities(
K_IOPM_CAPABILITY_CPU | K_IOPM_CAPABILITY_NETWORK | K_IOPM_CAPABILITY_DISK
),
PowerState::DarkWake
);
// CPU alone (no video) is still a dark wake.
assert_eq!(
classify_capabilities(K_IOPM_CAPABILITY_CPU),
PowerState::DarkWake
);
}
#[test]
fn classify_unknown_without_cpu() {
// No CPU bit while we are running is a bogus/transitional sample: fail
// open to Unknown so callers keep their existing behavior.
assert_eq!(classify_capabilities(0), PowerState::Unknown);
assert_eq!(
classify_capabilities(K_IOPM_CAPABILITY_VIDEO),
PowerState::Unknown
);
}
/// Message → (event, needs_ack) contract. The load-bearing rows:
/// - the idle-sleep *query* must deliver `WillSleep` (raise the auth sleep
/// gate / drain in-flight refreshes **before** we allow the transition —
/// an idle sleep can follow within seconds, and a one-time-use OIDC
/// refresh-token exchange started in that window would straddle it), and
/// must still be acked (we never veto);
/// - a vetoed sleep must deliver `DidWake` so a gate raised at the query
/// is lowered instead of blocking refresh for `SLEEP_GATE_MAX`.
#[test]
fn map_power_message_matrix() {
assert_eq!(
map_power_message(K_IO_MESSAGE_CAN_SYSTEM_SLEEP),
(Some(PowerEvent::WillSleep), true)
);
assert_eq!(
map_power_message(K_IO_MESSAGE_SYSTEM_WILL_SLEEP),
(Some(PowerEvent::WillSleep), true)
);
assert_eq!(
map_power_message(K_IO_MESSAGE_SYSTEM_WILL_NOT_SLEEP),
(Some(PowerEvent::DidWake), false)
);
assert_eq!(
map_power_message(K_IO_MESSAGE_SYSTEM_HAS_POWERED_ON),
(Some(PowerEvent::DidWake), false)
);
// Unrelated messages (e.g. kIOMessageSystemWillPowerOn 0xe0000320)
// deliver nothing and need no ack.
assert_eq!(map_power_message(0xe000_0320), (None, false));
}
}

View file

@ -0,0 +1,103 @@
//! Windows system sleep/wake via `PowerRegisterSuspendResumeNotification`
//! with a `DEVICE_NOTIFY_CALLBACK` recipient — no hidden window or message
//! loop required (Windows 8+).
//!
//! NOTE: this module only compiles when targeting Windows.
use std::os::raw::c_void;
use windows_sys::Win32::Foundation::HANDLE;
use windows_sys::Win32::System::Power::{
DEVICE_NOTIFY_SUBSCRIBE_PARAMETERS, HPOWERNOTIFY, PowerRegisterSuspendResumeNotification,
PowerUnregisterSuspendResumeNotification,
};
use windows_sys::Win32::UI::WindowsAndMessaging::DEVICE_NOTIFY_CALLBACK;
use super::{PowerCallback, PowerEvent};
// Power-broadcast event types (WM_POWERBROADCAST `wParam`).
const PBT_APMSUSPEND: u32 = 0x0004;
const PBT_APMRESUMESUSPEND: u32 = 0x0007;
const PBT_APMRESUMEAUTOMATIC: u32 = 0x0012;
const ERROR_SUCCESS: u32 = 0;
/// Heap-pinned so its address stays stable for the registration lifetime; the
/// raw pointer is handed to the OS as the callback context.
struct Context {
callback: PowerCallback,
}
pub(crate) struct Listener {
// Registration handle from `PowerRegisterSuspendResumeNotification`
// (a `*mut c_void`; cast to `HPOWERNOTIFY` for unregister).
handle: *mut c_void,
// Kept alive (and freed in `Drop`) because the OS holds a raw pointer to it.
ctx: *mut Context,
}
// The OS invokes the callback on an arbitrary thread; the handle is only used
// to unregister. `PowerCallback` is `Send + Sync`.
unsafe impl Send for Listener {}
unsafe impl Sync for Listener {}
impl Listener {
pub(crate) fn start(callback: PowerCallback) -> Option<Self> {
let ctx = Box::into_raw(Box::new(Context { callback }));
let mut params = DEVICE_NOTIFY_SUBSCRIBE_PARAMETERS {
Callback: Some(power_callback),
Context: ctx as *mut c_void,
};
let mut handle: *mut c_void = std::ptr::null_mut();
let status = unsafe {
PowerRegisterSuspendResumeNotification(
DEVICE_NOTIFY_CALLBACK,
&mut params as *mut DEVICE_NOTIFY_SUBSCRIBE_PARAMETERS as HANDLE,
&mut handle,
)
};
if status != ERROR_SUCCESS || handle.is_null() {
unsafe { drop(Box::from_raw(ctx)) };
return None;
}
Some(Self { handle, ctx })
}
}
impl Drop for Listener {
fn drop(&mut self) {
unsafe {
PowerUnregisterSuspendResumeNotification(self.handle as HPOWERNOTIFY);
drop(Box::from_raw(self.ctx));
}
}
}
unsafe extern "system" fn power_callback(
context: *const c_void,
event_type: u32,
_setting: *const c_void,
) -> u32 {
// Safe: `context` is the live `Context` we registered with.
let ctx = unsafe { &*(context as *const Context) };
match event_type {
PBT_APMSUSPEND => (ctx.callback)(PowerEvent::WillSleep),
// A single resume can deliver both PBT_APMRESUMEAUTOMATIC and
// PBT_APMRESUMESUSPEND, so `DidWake` may fire twice per wake. That is
// fine and intentional: lowering the sleep gate is idempotent, so a
// duplicate wake is harmless — do not try to "dedupe" this later.
PBT_APMRESUMEAUTOMATIC | PBT_APMRESUMESUSPEND => (ctx.callback)(PowerEvent::DidWake),
_ => {}
}
ERROR_SUCCESS
}
pub(crate) fn current_power_state() -> crate::PowerState {
// No synchronous dark-wake query wired up on Windows; report Unknown so
// callers fall back to the suspend/resume notification path.
crate::PowerState::Unknown
}