Files
Kigi-CLI/crates/codegen/kigi-system-power/src/macos.rs
T
ZacharyZhang-NY d6c20fc13f M0: compilable skeleton — Kigi 0.1.0 fork surgery
Hard fork of xai-org/grok-build (Apache-2.0) re-targeted as Kigi, an
unofficial Kimi Code CLI community build.

Rename & identity
- 72 xai-*/xai-grok-* crates -> kigi-* (explicit: xai-grok-pager-bin ->
  kigi-bin [binary `kigi`], xai-grok-pager -> kigi-tui; rest mechanical);
  ptyctl, ptyctl-cli, third_party/ unchanged; proto package
  xai.grok.tools.v1 -> kigi.tools.v1
- Config home ~/.kigi (KIGI_SHARE_DIR override), env prefix GROK_* ->
  KIGI_*, `kigi --version` carries the unofficial-community-build notice
- clap identity, help text, startup banner, prompt templates rebranded
  (templates re-encrypted)

Deletions (PRD removal list #5/#6/#7/#9/#10)
- voice input (xai-grok-voice) and all TUI wiring
- telemetry: Mixpanel client, external OTel stream, Sentry, OTLP layers,
  trace/GCS/S3 upload queues (kigi-file-utils halved), workspace upload
  module & dc_log, heap-profile uploader, auth-diagnostics uploader,
  session-analytics halves of feedback; local zero-egress observability
  preserved in new kigi-log crate (unified log, --debug firehose,
  subsystem file logs, opt-in instrumentation)
- announcements (crate, remote-settings fields, TUI surfaces)
- plugin marketplace (crate, sources/browse/CTA/extensions-modal tab);
  direct plugin install/uninstall/update via kigi-agent git_install kept
- relay/gateway/assets endpoints and features (agent relay, headless
  relay transport, gateway bridge, LeaderEnvUrls); leader IPC socket now
  ~/.kigi/leader.sock + KIGI_LEADER_SOCKET, no ws-url derivation
- functional types rehomed instead of deleted: PermissionMode ->
  kigi-config-types, McpInitStrategy -> kigi-mcp, PrCreationSource ->
  session signals, TerminalDiagnostics -> kigi-pager-render, agent_id ->
  shell util

Endpoints
- kigi-env rewritten: single production KigiEndpoints {coding_api_base_url
  https://api.kimi.com/coding/v1 (KIGI_CODE_BASE_URL), oauth_host
  https://auth.kimi.com (KIGI_OAUTH_HOST), update_base_url (GitHub
  Releases API), upgrade_page_url}; GrokBuildEnvironment enum deleted

Toolchain & workspace hygiene
- Rust 1.97.0 pinned; edition 2024; full cargo update; git2 hoisted to
  workspace at 0.21 (Option->Result API migration), quick-xml 0.41
- Root Cargo.toml hand-maintained (PRD §8.1): version 0.1.0 inherited by
  all members, members sorted, unused deps pruned
- cargo-deny advisories gate (deny.toml with documented transitive
  exceptions); CI workflow (check/clippy/fmt/deny/test, macOS+Linux)
- cross-crate test seams re-gated behind `test-support` cargo feature;
  insta snapshot baselines renamed to the kigi_tui prefix
- clippy --workspace --all-targets: zero warnings; fmt clean

Fixes surfaced by the port
- updater probe/installer divergence (bin/kigi vs bin/grok symlink set)
- idle model-metadata refresh dead under KIGI_CODE_BASE_URL override
  (new is_effective_coding_endpoint_url, loopback+override aware)
- macOS symlinked-TMPDIR fixture canonicalization (foreign_sessions,
  fast-worktree); RSS measurement tests serialized via serial_test

Docs & legal (Apache §4)
- NOTICE added (upstream attribution + change statement); THIRD-PARTY
  notices sustained; kigi-tools ported-code notices extended; README,
  CONTRIBUTING, SECURITY, AGENTS.md rewritten

Out of scope for M0 (tracked): Kimi auth/inference (M1), search/fetch,
command parity, config import (M2), Computer Hub excision & final
brand-token sweep (M2), distribution & self-update rewrite (M3).
2026-07-17 05:31:01 -04:00

368 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" {
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
// `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));
}
}