fix: simplify macOS sleep inhibitor FFI (#12340)

Summary
- simplify the macOS sleep inhibitor FFI by replacing `dlopen` / `dlsym`
/ `transmute` with normal IOKit extern calls and `SAFETY` comments
- switch to cfg-selected platform implementations
(`imp::SleepInhibitor`) instead of `Box<dyn ...>`
- check in minimal IOKit bindings generated with `bindgen` and include
them from the macOS backend
- enable direct IOKit linkage in Bazel macOS builds by registering
`IOKit` in the Bazel `osx.framework(...)` toolchain extension list
- update `Cargo.lock` and `MODULE.bazel.lock` after removing the
build-time `bindgen` dependency path

Testing
- `just fmt`
- `cargo clippy -p codex-utils-sleep-inhibitor --all-targets -- -D
warnings`
- `cargo test -p codex-utils-sleep-inhibitor`
- `bazel test //codex-rs/utils/sleep-inhibitor:all --test_output=errors`
- `just bazel-lock-update`
- `just bazel-lock-check`

Context
- follow-up to #11711 addressing Ryan's review comments
- `bindgen` is used to generate the checked-in bindings file, but not at
build time
This commit is contained in:
Yaroslav Volovich
2026-02-20 17:52:21 +00:00
committed by GitHub
parent fd67aba114
commit 5b71246001
8 changed files with 159 additions and 227 deletions

View File

@@ -1,16 +1,12 @@
use crate::PlatformSleepInhibitor;
#[derive(Debug, Default)]
pub(crate) struct DummySleepInhibitor;
pub(crate) struct SleepInhibitor;
impl DummySleepInhibitor {
impl SleepInhibitor {
pub(crate) fn new() -> Self {
Self
}
}
impl PlatformSleepInhibitor for DummySleepInhibitor {
fn acquire(&mut self) {}
pub(crate) fn acquire(&mut self) {}
fn release(&mut self) {}
pub(crate) fn release(&mut self) {}
}

View File

@@ -0,0 +1,27 @@
/* automatically generated by rust-bindgen 0.71.1 */
pub const kIOReturnSuccess: u32 = 0;
#[repr(C)]
#[derive(Debug, Copy, Clone)]
pub struct __CFString {
_unused: [u8; 0],
}
pub type CFStringRef = *const __CFString;
pub type kern_return_t = ::std::os::raw::c_int;
pub type IOReturn = kern_return_t;
pub type IOPMAssertionID = u32;
pub type IOPMAssertionLevel = u32;
pub const kIOPMAssertionLevelOff: _bindgen_ty_36 = 0;
pub const kIOPMAssertionLevelOn: _bindgen_ty_36 = 255;
pub type _bindgen_ty_36 = ::std::os::raw::c_uint;
unsafe extern "C" {
pub fn IOPMAssertionRelease(AssertionID: IOPMAssertionID) -> IOReturn;
}
unsafe extern "C" {
pub fn IOPMAssertionCreateWithName(
AssertionType: CFStringRef,
AssertionLevel: IOPMAssertionLevel,
AssertionName: CFStringRef,
AssertionID: *mut IOPMAssertionID,
) -> IOReturn;
}

View File

@@ -6,31 +6,26 @@
#[cfg(not(target_os = "macos"))]
mod dummy;
#[cfg(target_os = "macos")]
mod macos_inhibitor;
mod macos;
use std::fmt::Debug;
#[cfg(not(target_os = "macos"))]
use dummy as imp;
#[cfg(target_os = "macos")]
use macos as imp;
/// 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);
platform: imp::SleepInhibitor,
}
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 }
Self {
enabled,
platform: imp::SleepInhibitor::new(),
}
}
/// Update the active turn state; turns sleep prevention on/off as needed.

View File

@@ -0,0 +1,107 @@
use core_foundation::base::TCFType;
use core_foundation::string::CFString;
use tracing::warn;
#[allow(
dead_code,
non_camel_case_types,
non_snake_case,
non_upper_case_globals,
clippy::all
)]
mod iokit {
#[link(name = "IOKit", kind = "framework")]
unsafe extern "C" {}
include!("iokit_bindings.rs");
}
type IOPMAssertionID = iokit::IOPMAssertionID;
type IOPMAssertionLevel = iokit::IOPMAssertionLevel;
type IOReturn = iokit::IOReturn;
const ASSERTION_REASON: &str = "Codex is running an active turn";
// Apple exposes this assertion type as a `CFSTR(...)` macro, so bindgen cannot generate a
// reusable `CFStringRef` constant for it.
const ASSERTION_TYPE_PREVENT_USER_IDLE_SYSTEM_SLEEP: &str = "PreventUserIdleSystemSleep";
#[derive(Debug, Default)]
pub(crate) struct SleepInhibitor {
assertion: Option<MacSleepAssertion>,
}
impl SleepInhibitor {
pub(crate) fn new() -> Self {
Self::default()
}
pub(crate) fn acquire(&mut self) {
if self.assertion.is_some() {
return;
}
match MacSleepAssertion::create(ASSERTION_REASON) {
Ok(assertion) => {
self.assertion = Some(assertion);
}
Err(error) => {
warn!(
iokit_error = error,
"Failed to create macOS sleep-prevention assertion"
);
}
}
}
pub(crate) fn release(&mut self) {
self.assertion = None;
}
}
#[derive(Debug)]
struct MacSleepAssertion {
id: IOPMAssertionID,
}
impl MacSleepAssertion {
fn create(name: &str) -> Result<Self, IOReturn> {
let assertion_type = CFString::new(ASSERTION_TYPE_PREVENT_USER_IDLE_SYSTEM_SLEEP);
let assertion_name = CFString::new(name);
let mut id: IOPMAssertionID = 0;
// `core-foundation` and the generated bindings each declare an opaque `__CFString` type,
// so we cast to the bindgen pointer aliases before crossing the FFI boundary.
let assertion_type_ref: iokit::CFStringRef = assertion_type.as_concrete_TypeRef().cast();
let assertion_name_ref: iokit::CFStringRef = assertion_name.as_concrete_TypeRef().cast();
let result = unsafe {
// SAFETY: `assertion_type_ref` and `assertion_name_ref` are valid `CFStringRef`s and
// `&mut id` is a valid out-pointer for `IOPMAssertionCreateWithName` to initialize.
iokit::IOPMAssertionCreateWithName(
assertion_type_ref,
iokit::kIOPMAssertionLevelOn as IOPMAssertionLevel,
assertion_name_ref,
&mut id,
)
};
if result == iokit::kIOReturnSuccess as IOReturn {
Ok(Self { id })
} else {
Err(result)
}
}
}
impl Drop for MacSleepAssertion {
fn drop(&mut self) {
let result = unsafe {
// SAFETY: `self.id` was returned by `IOPMAssertionCreateWithName` and this `Drop`
// implementation releases it exactly once when the owning assertion is dropped.
iokit::IOPMAssertionRelease(self.id)
};
if result != iokit::kIOReturnSuccess as IOReturn {
warn!(
iokit_error = result,
"Failed to release macOS sleep-prevention assertion"
);
}
}
}

View File

@@ -1,192 +0,0 @@
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,
})
}
}