Publish harness and TUI open-source
initial sync from the monorepo
This commit is contained in:
commit
c68e39f604
2734 changed files with 1437016 additions and 0 deletions
23
crates/codegen/xai-system-power/Cargo.toml
Normal file
23
crates/codegen/xai-system-power/Cargo.toml
Normal 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
|
||||
180
crates/codegen/xai-system-power/src/lib.rs
Normal file
180
crates/codegen/xai-system-power/src/lib.rs
Normal 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| {});
|
||||
}
|
||||
}
|
||||
93
crates/codegen/xai-system-power/src/linux.rs
Normal file
93
crates/codegen/xai-system-power/src/linux.rs
Normal 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
|
||||
}
|
||||
367
crates/codegen/xai-system-power/src/macos.rs
Normal file
367
crates/codegen/xai-system-power/src/macos.rs
Normal 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));
|
||||
}
|
||||
}
|
||||
103
crates/codegen/xai-system-power/src/windows.rs
Normal file
103
crates/codegen/xai-system-power/src/windows.rs
Normal 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
|
||||
}
|
||||
Loading…
Reference in a new issue