Files
Kigi-CLI/crates/codegen/kigi-system-power/src/macos.rs
T
ZacharyZhang-NY a02b555e66 docs(comments): rewrite comments across all crates to the guidelines
Sweep every first-party crate source (1956 .rs files) to the project comment
guidelines: delete redundant restatements, decorative banners, change
narration, and end-of-line comments; keep and tighten the crucial ones
(invariants, bug rationale, SAFETY blocks, ported-source attribution).

No functional code changed. Every edit is proven comment-only against the
prior tree by a comment-stripping lexer (string/char/raw-string aware) plus a
separate doctest-fence check. Where removing a comment made rustfmt or clippy
want to re-lay-out adjacent code, the minimal triggering comment is restored so
code tokens stay byte-identical.

Gates green: cargo fmt --all --check (0 diffs), cargo check and cargo clippy
--workspace --all-targets (0 warnings).

Adds scripts/check_codegen_comment_guidelines.py — the enforcement gate for
these guidelines (flags banners, end-of-line comments, change narration, and
commented-out code).
2026-07-23 16:55:39 -04:00

370 lines
15 KiB
Rust

//! 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" {
// CFRunLoopMode (CFStringRef)
static kCFRunLoopCommonModes: *const c_void;
// CFRunLoopMode (CFStringRef)
static kCFRunLoopDefaultMode: *const c_void;
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 deliberately 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
// `kigi_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));
}
}