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).
368 lines
15 KiB
Rust
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));
|
|
}
|
|
}
|