//! 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 //! `kigi-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 earlier 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; /// 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 { 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(callback: F) -> Option 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; 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| {}); } }