feat(tui): prevent macOS idle sleep while turns run (#11711)
## Summary - add a shared `codex-core` sleep inhibitor that uses native macOS IOKit assertions (`IOPMAssertionCreateWithName` / `IOPMAssertionRelease`) instead of spawning `caffeinate` - wire sleep inhibition to turn lifecycle in `tui` (`TurnStarted` enables; `TurnComplete` and abort/error finalization disable) - gate this behavior behind a `/experimental` feature toggle (`[features].prevent_idle_sleep`) instead of a dedicated `[tui]` config flag - expose the toggle in `/experimental` on macOS; keep it under development on other platforms - keep behavior no-op on non-macOS targets <img width="1326" height="577" alt="image" src="https://github.com/user-attachments/assets/73fac06b-97ae-46a2-800a-30f9516cf8a3" /> ## Testing - `cargo check -p codex-core -p codex-tui` - `cargo test -p codex-core sleep_inhibitor::tests -- --nocapture` - `cargo test -p codex-core tui_config_missing_notifications_field_defaults_to_enabled -- --nocapture` - `cargo test -p codex-core prevent_idle_sleep_is_ -- --nocapture` ## Semantics and API references - This PR targets `caffeinate -i` semantics: prevent *idle system sleep* while allowing display idle sleep. - `caffeinate -i` mapping in Apple open source (`assertionMap`): - `kIdleAssertionFlag -> kIOPMAssertionTypePreventUserIdleSystemSleep` - Source: https://github.com/apple-oss-distributions/PowerManagement/blob/PowerManagement-1846.60.12/caffeinate/caffeinate.c#L52-L54 - Apple IOKit docs for assertion types and API: - https://developer.apple.com/documentation/iokit/iopmlib_h/iopmassertiontypes - https://developer.apple.com/documentation/iokit/1557092-iopmassertioncreatewithname - https://developer.apple.com/library/archive/qa/qa1340/_index.html ## Codex Electron vs this PR (full stack path) - Codex Electron app requests sleep blocking with `powerSaveBlocker.start("prevent-app-suspension")`: - https://github.com/openai/codex/blob/main/codex/codex-vscode/electron/src/electron-message-handler.ts - Electron maps that string to Chromium wake lock type `kPreventAppSuspension`: - https://github.com/electron/electron/blob/main/shell/browser/api/electron_api_power_save_blocker.cc - Chromium macOS backend maps wake lock types to IOKit assertion constants and calls IOKit: - `kPreventAppSuspension -> kIOPMAssertionTypeNoIdleSleep` - `kPreventDisplaySleep / kPreventDisplaySleepAllowDimming -> kIOPMAssertionTypeNoDisplaySleep` - https://github.com/chromium/chromium/blob/main/services/device/wake_lock/power_save_blocker/power_save_blocker_mac.cc ## Why this PR uses a different macOS constant name - This PR uses `"PreventUserIdleSystemSleep"` directly, via `IOPMAssertionCreateWithName`, in `codex-rs/core/src/sleep_inhibitor.rs`. - Apple’s IOKit header documents `kIOPMAssertionTypeNoIdleSleep` as deprecated and recommends `kIOPMAssertPreventUserIdleSystemSleep` / `kIOPMAssertionTypePreventUserIdleSystemSleep`: - https://github.com/apple-oss-distributions/IOKitUser/blob/IOKitUser-100222.60.2/pwr_mgt.subproj/IOPMLib.h#L1000-L1030 - So Chromium and this PR are using different constant names, but semantically equivalent idle-system-sleep prevention behavior. ## Future platform support The architecture is intentionally set up for multi-platform extensions: - UI code (`tui`) only calls `SleepInhibitor::set_turn_running(...)` on turn lifecycle boundaries. - Platform-specific behavior is isolated in `codex-rs/core/src/sleep_inhibitor.rs` behind `cfg(...)` blocks. - Feature exposure is centralized in `core/src/features.rs` and surfaced via `/experimental`. - Adding new OS backends should not require additional TUI wiring; only the backend internals and feature stage metadata need to change. Potential follow-up implementations: - Windows: - Add a backend using Win32 power APIs (`SetThreadExecutionState(ES_CONTINUOUS | ES_SYSTEM_REQUIRED)` as baseline). - Optionally move to `PowerCreateRequest` / `PowerSetRequest` / `PowerClearRequest` for richer assertion semantics. - Linux: - Add a backend using logind inhibitors over D-Bus (`org.freedesktop.login1.Manager.Inhibit` with `what="sleep"`). - Keep a no-op fallback where logind/D-Bus is unavailable. This PR keeps the cross-platform API surface minimal so future PRs can add Windows/Linux support incrementally with low churn. --------- Co-authored-by: jif-oai <jif@openai.com>
This commit is contained in:
parent
851fcc377b
commit
32da5eb358
12 changed files with 374 additions and 0 deletions
10
codex-rs/Cargo.lock
generated
10
codex-rs/Cargo.lock
generated
|
|
@ -2297,6 +2297,7 @@ dependencies = [
|
|||
"codex-utils-oss",
|
||||
"codex-utils-pty",
|
||||
"codex-utils-sandbox-summary",
|
||||
"codex-utils-sleep-inhibitor",
|
||||
"codex-windows-sandbox",
|
||||
"color-eyre",
|
||||
"crossterm",
|
||||
|
|
@ -2496,6 +2497,15 @@ dependencies = [
|
|||
"regex",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "codex-utils-sleep-inhibitor"
|
||||
version = "0.0.0"
|
||||
dependencies = [
|
||||
"core-foundation 0.9.4",
|
||||
"libc",
|
||||
"tracing",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "codex-utils-string"
|
||||
version = "0.0.0"
|
||||
|
|
|
|||
|
|
@ -54,6 +54,7 @@ members = [
|
|||
"utils/elapsed",
|
||||
"utils/sandbox-summary",
|
||||
"utils/sanitizer",
|
||||
"utils/sleep-inhibitor",
|
||||
"utils/approval-presets",
|
||||
"utils/oss",
|
||||
"utils/fuzzy-match",
|
||||
|
|
@ -131,6 +132,7 @@ codex-utils-readiness = { path = "utils/readiness" }
|
|||
codex-utils-rustls-provider = { path = "utils/rustls-provider" }
|
||||
codex-utils-sandbox-summary = { path = "utils/sandbox-summary" }
|
||||
codex-utils-sanitizer = { path = "utils/sanitizer" }
|
||||
codex-utils-sleep-inhibitor = { path = "utils/sleep-inhibitor" }
|
||||
codex-utils-string = { path = "utils/string" }
|
||||
codex-windows-sandbox = { path = "windows-sandbox-rs" }
|
||||
core_test_support = { path = "core/tests/common" }
|
||||
|
|
|
|||
|
|
@ -239,6 +239,9 @@
|
|||
"powershell_utf8": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"prevent_idle_sleep": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"remote_models": {
|
||||
"type": "boolean"
|
||||
},
|
||||
|
|
@ -1366,6 +1369,9 @@
|
|||
"powershell_utf8": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"prevent_idle_sleep": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"remote_models": {
|
||||
"type": "boolean"
|
||||
},
|
||||
|
|
|
|||
|
|
@ -133,6 +133,8 @@ pub enum Feature {
|
|||
CollaborationModes,
|
||||
/// Enable personality selection in the TUI.
|
||||
Personality,
|
||||
/// Prevent idle system sleep while a turn is actively running.
|
||||
PreventIdleSleep,
|
||||
/// Use the Responses API WebSocket transport for OpenAI by default.
|
||||
ResponsesWebsockets,
|
||||
/// Enable Responses API websocket v2 mode.
|
||||
|
|
@ -604,6 +606,20 @@ pub const FEATURES: &[FeatureSpec] = &[
|
|||
stage: Stage::Stable,
|
||||
default_enabled: true,
|
||||
},
|
||||
FeatureSpec {
|
||||
id: Feature::PreventIdleSleep,
|
||||
key: "prevent_idle_sleep",
|
||||
stage: if cfg!(target_os = "macos") {
|
||||
Stage::Experimental {
|
||||
name: "Prevent sleep while running",
|
||||
menu_description: "Keep your computer awake while Codex is running a thread.",
|
||||
announcement: "NEW: Prevent sleep while running is now available in /experimental.",
|
||||
}
|
||||
} else {
|
||||
Stage::UnderDevelopment
|
||||
},
|
||||
default_enabled: false,
|
||||
},
|
||||
FeatureSpec {
|
||||
id: Feature::ResponsesWebsockets,
|
||||
key: "responses_websockets",
|
||||
|
|
|
|||
|
|
@ -46,6 +46,7 @@ codex-utils-elapsed = { workspace = true }
|
|||
codex-utils-fuzzy-match = { workspace = true }
|
||||
codex-utils-oss = { workspace = true }
|
||||
codex-utils-sandbox-summary = { workspace = true }
|
||||
codex-utils-sleep-inhibitor = { workspace = true }
|
||||
color-eyre = { workspace = true }
|
||||
crossterm = { workspace = true, features = ["bracketed-paste", "event-stream"] }
|
||||
derive_more = { workspace = true, features = ["is_variant"] }
|
||||
|
|
|
|||
|
|
@ -131,6 +131,7 @@ use codex_protocol::parse_command::ParsedCommand;
|
|||
use codex_protocol::request_user_input::RequestUserInputEvent;
|
||||
use codex_protocol::user_input::TextElement;
|
||||
use codex_protocol::user_input::UserInput;
|
||||
use codex_utils_sleep_inhibitor::SleepInhibitor;
|
||||
use crossterm::event::KeyCode;
|
||||
use crossterm::event::KeyEvent;
|
||||
use crossterm::event::KeyEventKind;
|
||||
|
|
@ -520,6 +521,7 @@ pub(crate) struct ChatWidget {
|
|||
skills_initial_state: Option<HashMap<PathBuf, bool>>,
|
||||
last_unified_wait: Option<UnifiedExecWaitState>,
|
||||
unified_exec_wait_streak: Option<UnifiedExecWaitStreak>,
|
||||
turn_sleep_inhibitor: SleepInhibitor,
|
||||
task_complete_pending: bool,
|
||||
unified_exec_processes: Vec<UnifiedExecProcessSummary>,
|
||||
/// Tracks whether codex-core currently considers an agent turn to be in progress.
|
||||
|
|
@ -1275,6 +1277,7 @@ impl ChatWidget {
|
|||
|
||||
fn on_task_started(&mut self) {
|
||||
self.agent_turn_running = true;
|
||||
self.turn_sleep_inhibitor.set_turn_running(true);
|
||||
self.saw_plan_update_this_turn = false;
|
||||
self.saw_plan_item_this_turn = false;
|
||||
self.plan_delta_buffer.clear();
|
||||
|
|
@ -1332,6 +1335,7 @@ impl ChatWidget {
|
|||
// Mark task stopped and request redraw now that all content is in history.
|
||||
self.pending_status_indicator_restore = false;
|
||||
self.agent_turn_running = false;
|
||||
self.turn_sleep_inhibitor.set_turn_running(false);
|
||||
self.update_task_running_state();
|
||||
self.running_commands.clear();
|
||||
self.suppressed_exec_calls.clear();
|
||||
|
|
@ -1568,6 +1572,7 @@ impl ChatWidget {
|
|||
self.finalize_active_cell_as_failed();
|
||||
// Reset running state and clear streaming buffers.
|
||||
self.agent_turn_running = false;
|
||||
self.turn_sleep_inhibitor.set_turn_running(false);
|
||||
self.update_task_running_state();
|
||||
self.running_commands.clear();
|
||||
self.suppressed_exec_calls.clear();
|
||||
|
|
@ -2544,6 +2549,7 @@ impl ChatWidget {
|
|||
let model = model.filter(|m| !m.trim().is_empty());
|
||||
let mut config = config;
|
||||
config.model = model.clone();
|
||||
let prevent_idle_sleep = config.features.enabled(Feature::PreventIdleSleep);
|
||||
let mut rng = rand::rng();
|
||||
let placeholder = PLACEHOLDERS[rng.random_range(0..PLACEHOLDERS.len())].to_string();
|
||||
let codex_op_tx = spawn_agent(config.clone(), app_event_tx.clone(), thread_manager);
|
||||
|
|
@ -2611,6 +2617,7 @@ impl ChatWidget {
|
|||
suppressed_exec_calls: HashSet::new(),
|
||||
last_unified_wait: None,
|
||||
unified_exec_wait_streak: None,
|
||||
turn_sleep_inhibitor: SleepInhibitor::new(prevent_idle_sleep),
|
||||
task_complete_pending: false,
|
||||
unified_exec_processes: Vec::new(),
|
||||
agent_turn_running: false,
|
||||
|
|
@ -2710,6 +2717,7 @@ impl ChatWidget {
|
|||
let model = model.filter(|m| !m.trim().is_empty());
|
||||
let mut config = config;
|
||||
config.model = model.clone();
|
||||
let prevent_idle_sleep = config.features.enabled(Feature::PreventIdleSleep);
|
||||
let mut rng = rand::rng();
|
||||
let placeholder = PLACEHOLDERS[rng.random_range(0..PLACEHOLDERS.len())].to_string();
|
||||
|
||||
|
|
@ -2776,6 +2784,7 @@ impl ChatWidget {
|
|||
suppressed_exec_calls: HashSet::new(),
|
||||
last_unified_wait: None,
|
||||
unified_exec_wait_streak: None,
|
||||
turn_sleep_inhibitor: SleepInhibitor::new(prevent_idle_sleep),
|
||||
task_complete_pending: false,
|
||||
unified_exec_processes: Vec::new(),
|
||||
agent_turn_running: false,
|
||||
|
|
@ -2862,6 +2871,7 @@ impl ChatWidget {
|
|||
otel_manager,
|
||||
} = common;
|
||||
let model = model.filter(|m| !m.trim().is_empty());
|
||||
let prevent_idle_sleep = config.features.enabled(Feature::PreventIdleSleep);
|
||||
let mut rng = rand::rng();
|
||||
let placeholder = PLACEHOLDERS[rng.random_range(0..PLACEHOLDERS.len())].to_string();
|
||||
|
||||
|
|
@ -2930,6 +2940,7 @@ impl ChatWidget {
|
|||
suppressed_exec_calls: HashSet::new(),
|
||||
last_unified_wait: None,
|
||||
unified_exec_wait_streak: None,
|
||||
turn_sleep_inhibitor: SleepInhibitor::new(prevent_idle_sleep),
|
||||
task_complete_pending: false,
|
||||
unified_exec_processes: Vec::new(),
|
||||
agent_turn_running: false,
|
||||
|
|
@ -5950,6 +5961,11 @@ impl ChatWidget {
|
|||
if feature == Feature::Personality {
|
||||
self.sync_personality_command_enabled();
|
||||
}
|
||||
if feature == Feature::PreventIdleSleep {
|
||||
self.turn_sleep_inhibitor = SleepInhibitor::new(enabled);
|
||||
self.turn_sleep_inhibitor
|
||||
.set_turn_running(self.agent_turn_running);
|
||||
}
|
||||
#[cfg(target_os = "windows")]
|
||||
if matches!(
|
||||
feature,
|
||||
|
|
|
|||
|
|
@ -1030,6 +1030,7 @@ async fn make_chatwidget_manual(
|
|||
if let Some(model) = model_override {
|
||||
cfg.model = Some(model.to_string());
|
||||
}
|
||||
let prevent_idle_sleep = cfg.features.enabled(Feature::PreventIdleSleep);
|
||||
let otel_manager = test_otel_manager(&cfg, resolved_model.as_str());
|
||||
let mut bottom = BottomPane::new(BottomPaneParams {
|
||||
app_event_tx: app_event_tx.clone(),
|
||||
|
|
@ -1086,6 +1087,7 @@ async fn make_chatwidget_manual(
|
|||
skills_initial_state: None,
|
||||
last_unified_wait: None,
|
||||
unified_exec_wait_streak: None,
|
||||
turn_sleep_inhibitor: SleepInhibitor::new(prevent_idle_sleep),
|
||||
task_complete_pending: false,
|
||||
unified_exec_processes: Vec::new(),
|
||||
agent_turn_running: false,
|
||||
|
|
|
|||
6
codex-rs/utils/sleep-inhibitor/BUILD.bazel
Normal file
6
codex-rs/utils/sleep-inhibitor/BUILD.bazel
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
load("//:defs.bzl", "codex_rust_crate")
|
||||
|
||||
codex_rust_crate(
|
||||
name = "sleep-inhibitor",
|
||||
crate_name = "codex_utils_sleep_inhibitor",
|
||||
)
|
||||
13
codex-rs/utils/sleep-inhibitor/Cargo.toml
Normal file
13
codex-rs/utils/sleep-inhibitor/Cargo.toml
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
[package]
|
||||
name = "codex-utils-sleep-inhibitor"
|
||||
version.workspace = true
|
||||
edition.workspace = true
|
||||
license.workspace = true
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
[target.'cfg(target_os = "macos")'.dependencies]
|
||||
core-foundation = "0.9"
|
||||
libc = { workspace = true }
|
||||
tracing = { workspace = true }
|
||||
16
codex-rs/utils/sleep-inhibitor/src/dummy.rs
Normal file
16
codex-rs/utils/sleep-inhibitor/src/dummy.rs
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
use crate::PlatformSleepInhibitor;
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
pub(crate) struct DummySleepInhibitor;
|
||||
|
||||
impl DummySleepInhibitor {
|
||||
pub(crate) fn new() -> Self {
|
||||
Self
|
||||
}
|
||||
}
|
||||
|
||||
impl PlatformSleepInhibitor for DummySleepInhibitor {
|
||||
fn acquire(&mut self) {}
|
||||
|
||||
fn release(&mut self) {}
|
||||
}
|
||||
94
codex-rs/utils/sleep-inhibitor/src/lib.rs
Normal file
94
codex-rs/utils/sleep-inhibitor/src/lib.rs
Normal file
|
|
@ -0,0 +1,94 @@
|
|||
//! Cross-platform helper for preventing idle sleep while a turn is running.
|
||||
//!
|
||||
//! On macOS this uses native IOKit power assertions instead of spawning
|
||||
//! `caffeinate`, so assertion lifecycle is tied directly to Rust object lifetime.
|
||||
|
||||
#[cfg(not(target_os = "macos"))]
|
||||
mod dummy;
|
||||
#[cfg(target_os = "macos")]
|
||||
mod macos_inhibitor;
|
||||
|
||||
use std::fmt::Debug;
|
||||
|
||||
/// Keeps the machine awake while a turn is in progress when enabled.
|
||||
#[derive(Debug)]
|
||||
pub struct SleepInhibitor {
|
||||
enabled: bool,
|
||||
platform: Box<dyn PlatformSleepInhibitor>,
|
||||
}
|
||||
|
||||
pub(crate) trait PlatformSleepInhibitor: Debug {
|
||||
fn acquire(&mut self);
|
||||
fn release(&mut self);
|
||||
}
|
||||
|
||||
impl SleepInhibitor {
|
||||
pub fn new(enabled: bool) -> Self {
|
||||
#[cfg(target_os = "macos")]
|
||||
let platform: Box<dyn PlatformSleepInhibitor> =
|
||||
Box::new(macos_inhibitor::MacOsSleepInhibitor::new());
|
||||
#[cfg(not(target_os = "macos"))]
|
||||
let platform: Box<dyn PlatformSleepInhibitor> = Box::new(dummy::DummySleepInhibitor::new());
|
||||
|
||||
Self { enabled, platform }
|
||||
}
|
||||
|
||||
/// Update the active turn state; turns sleep prevention on/off as needed.
|
||||
pub fn set_turn_running(&mut self, turn_running: bool) {
|
||||
if !self.enabled {
|
||||
self.release();
|
||||
return;
|
||||
}
|
||||
|
||||
if turn_running {
|
||||
self.acquire();
|
||||
} else {
|
||||
self.release();
|
||||
}
|
||||
}
|
||||
|
||||
fn acquire(&mut self) {
|
||||
self.platform.acquire();
|
||||
}
|
||||
|
||||
fn release(&mut self) {
|
||||
self.platform.release();
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::SleepInhibitor;
|
||||
|
||||
#[test]
|
||||
fn sleep_inhibitor_toggles_without_panicking() {
|
||||
let mut inhibitor = SleepInhibitor::new(true);
|
||||
inhibitor.set_turn_running(true);
|
||||
inhibitor.set_turn_running(false);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn sleep_inhibitor_disabled_does_not_panic() {
|
||||
let mut inhibitor = SleepInhibitor::new(false);
|
||||
inhibitor.set_turn_running(true);
|
||||
inhibitor.set_turn_running(false);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn sleep_inhibitor_multiple_true_calls_are_idempotent() {
|
||||
let mut inhibitor = SleepInhibitor::new(true);
|
||||
inhibitor.set_turn_running(true);
|
||||
inhibitor.set_turn_running(true);
|
||||
inhibitor.set_turn_running(true);
|
||||
inhibitor.set_turn_running(false);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn sleep_inhibitor_can_toggle_multiple_times() {
|
||||
let mut inhibitor = SleepInhibitor::new(true);
|
||||
inhibitor.set_turn_running(true);
|
||||
inhibitor.set_turn_running(false);
|
||||
inhibitor.set_turn_running(true);
|
||||
inhibitor.set_turn_running(false);
|
||||
}
|
||||
}
|
||||
192
codex-rs/utils/sleep-inhibitor/src/macos_inhibitor.rs
Normal file
192
codex-rs/utils/sleep-inhibitor/src/macos_inhibitor.rs
Normal file
|
|
@ -0,0 +1,192 @@
|
|||
use crate::PlatformSleepInhibitor;
|
||||
use core_foundation::base::TCFType;
|
||||
use core_foundation::string::CFString;
|
||||
use core_foundation::string::CFStringRef;
|
||||
use std::fmt::Debug;
|
||||
use std::sync::OnceLock;
|
||||
use tracing::warn;
|
||||
|
||||
const ASSERTION_REASON: &str = "Codex is running an active turn";
|
||||
const MACOS_IDLE_SLEEP_ASSERTION_TYPE: &str = "PreventUserIdleSystemSleep";
|
||||
const IOKIT_FRAMEWORK_BINARY: &[u8] = b"/System/Library/Frameworks/IOKit.framework/IOKit\0";
|
||||
const IOPM_ASSERTION_CREATE_WITH_NAME_SYMBOL: &[u8] = b"IOPMAssertionCreateWithName\0";
|
||||
const IOPM_ASSERTION_RELEASE_SYMBOL: &[u8] = b"IOPMAssertionRelease\0";
|
||||
const IOKIT_ASSERTION_API_UNAVAILABLE: &str = "IOKit power assertion APIs are unavailable";
|
||||
|
||||
type IOPMAssertionReleaseFn = unsafe extern "C" fn(assertion_id: IOPMAssertionID) -> IOReturn;
|
||||
type IOPMAssertionID = u32;
|
||||
type IOPMAssertionLevel = u32;
|
||||
type IOReturn = i32;
|
||||
|
||||
const K_IOPM_ASSERTION_LEVEL_ON: IOPMAssertionLevel = 255;
|
||||
const K_IORETURN_SUCCESS: IOReturn = 0;
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
pub(crate) struct MacOsSleepInhibitor {
|
||||
assertion: Option<MacSleepAssertion>,
|
||||
}
|
||||
|
||||
impl MacOsSleepInhibitor {
|
||||
pub(crate) fn new() -> Self {
|
||||
Self {
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl PlatformSleepInhibitor for MacOsSleepInhibitor {
|
||||
fn acquire(&mut self) {
|
||||
if self.assertion.is_some() {
|
||||
return;
|
||||
}
|
||||
|
||||
match MacSleepAssertion::create(ASSERTION_REASON) {
|
||||
Ok(assertion) => {
|
||||
self.assertion = Some(assertion);
|
||||
}
|
||||
Err(error) => match error {
|
||||
MacSleepAssertionError::ApiUnavailable(reason) => {
|
||||
warn!(reason, "Failed to create macOS sleep-prevention assertion");
|
||||
}
|
||||
MacSleepAssertionError::Iokit(code) => {
|
||||
warn!(
|
||||
iokit_error = code,
|
||||
"Failed to create macOS sleep-prevention assertion"
|
||||
);
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
fn release(&mut self) {
|
||||
self.assertion = None;
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
struct MacSleepAssertion {
|
||||
id: IOPMAssertionID,
|
||||
}
|
||||
|
||||
impl MacSleepAssertion {
|
||||
fn create(name: &str) -> Result<Self, MacSleepAssertionError> {
|
||||
let Some(api) = MacSleepApi::get() else {
|
||||
return Err(MacSleepAssertionError::ApiUnavailable(
|
||||
IOKIT_ASSERTION_API_UNAVAILABLE,
|
||||
));
|
||||
};
|
||||
|
||||
let assertion_type = CFString::new(MACOS_IDLE_SLEEP_ASSERTION_TYPE);
|
||||
let assertion_name = CFString::new(name);
|
||||
let mut id: IOPMAssertionID = 0;
|
||||
let result = unsafe {
|
||||
(api.create_with_name)(
|
||||
assertion_type.as_concrete_TypeRef(),
|
||||
K_IOPM_ASSERTION_LEVEL_ON,
|
||||
assertion_name.as_concrete_TypeRef(),
|
||||
&mut id,
|
||||
)
|
||||
};
|
||||
if result == K_IORETURN_SUCCESS {
|
||||
Ok(Self { id })
|
||||
} else {
|
||||
Err(MacSleepAssertionError::Iokit(result))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for MacSleepAssertion {
|
||||
fn drop(&mut self) {
|
||||
let Some(api) = MacSleepApi::get() else {
|
||||
warn!(
|
||||
reason = IOKIT_ASSERTION_API_UNAVAILABLE,
|
||||
"Failed to release macOS sleep-prevention assertion"
|
||||
);
|
||||
return;
|
||||
};
|
||||
|
||||
let result = unsafe { (api.release)(self.id) };
|
||||
if result != K_IORETURN_SUCCESS {
|
||||
warn!(
|
||||
iokit_error = result,
|
||||
"Failed to release macOS sleep-prevention assertion"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
enum MacSleepAssertionError {
|
||||
ApiUnavailable(&'static str),
|
||||
Iokit(IOReturn),
|
||||
}
|
||||
|
||||
type IOPMAssertionCreateWithNameFn = unsafe extern "C" fn(
|
||||
assertion_type: CFStringRef,
|
||||
assertion_level: IOPMAssertionLevel,
|
||||
assertion_name: CFStringRef,
|
||||
assertion_id: *mut IOPMAssertionID,
|
||||
) -> IOReturn;
|
||||
|
||||
struct MacSleepApi {
|
||||
// Keep the dlopen handle alive for the lifetime of the loaded symbols.
|
||||
// This prevents accidental dlclose while function pointers are in use.
|
||||
_iokit_handle: usize,
|
||||
create_with_name: IOPMAssertionCreateWithNameFn,
|
||||
release: IOPMAssertionReleaseFn,
|
||||
}
|
||||
|
||||
impl MacSleepApi {
|
||||
fn get() -> Option<&'static Self> {
|
||||
static API: OnceLock<Option<MacSleepApi>> = OnceLock::new();
|
||||
API.get_or_init(Self::load).as_ref()
|
||||
}
|
||||
|
||||
fn load() -> Option<Self> {
|
||||
let handle = unsafe {
|
||||
libc::dlopen(
|
||||
IOKIT_FRAMEWORK_BINARY.as_ptr().cast(),
|
||||
libc::RTLD_LOCAL | libc::RTLD_LAZY,
|
||||
)
|
||||
};
|
||||
if handle.is_null() {
|
||||
warn!(framework = "IOKit", "Failed to open IOKit framework");
|
||||
return None;
|
||||
}
|
||||
|
||||
let create_with_name = unsafe {
|
||||
libc::dlsym(
|
||||
handle,
|
||||
IOPM_ASSERTION_CREATE_WITH_NAME_SYMBOL.as_ptr().cast(),
|
||||
)
|
||||
};
|
||||
if create_with_name.is_null() {
|
||||
warn!(
|
||||
symbol = "IOPMAssertionCreateWithName",
|
||||
"Failed to load IOKit symbol"
|
||||
);
|
||||
let _ = unsafe { libc::dlclose(handle) };
|
||||
return None;
|
||||
}
|
||||
|
||||
let release = unsafe { libc::dlsym(handle, IOPM_ASSERTION_RELEASE_SYMBOL.as_ptr().cast()) };
|
||||
if release.is_null() {
|
||||
warn!(
|
||||
symbol = "IOPMAssertionRelease",
|
||||
"Failed to load IOKit symbol"
|
||||
);
|
||||
let _ = unsafe { libc::dlclose(handle) };
|
||||
return None;
|
||||
}
|
||||
|
||||
let create_with_name: IOPMAssertionCreateWithNameFn =
|
||||
unsafe { std::mem::transmute(create_with_name) };
|
||||
let release: IOPMAssertionReleaseFn = unsafe { std::mem::transmute(release) };
|
||||
|
||||
Some(Self {
|
||||
_iokit_handle: handle as usize,
|
||||
create_with_name,
|
||||
release,
|
||||
})
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue