feat(sleep-inhibitor): add Linux and Windows idle-sleep prevention

This commit is contained in:
Yaroslav Volovich
2026-02-13 18:47:41 +00:00
parent 6c0a924203
commit 51149e1119
6 changed files with 291 additions and 3 deletions

2
codex-rs/Cargo.lock generated
View File

@@ -2502,8 +2502,10 @@ name = "codex-utils-sleep-inhibitor"
version = "0.0.0"
dependencies = [
"core-foundation 0.9.4",
"dbus",
"libc",
"tracing",
"windows-sys 0.60.2",
]
[[package]]

View File

@@ -609,7 +609,11 @@ pub const FEATURES: &[FeatureSpec] = &[
FeatureSpec {
id: Feature::PreventIdleSleep,
key: "prevent_idle_sleep",
stage: if cfg!(target_os = "macos") {
stage: if cfg!(any(
target_os = "macos",
target_os = "linux",
target_os = "windows"
)) {
Stage::Experimental {
name: "Prevent sleep while running",
menu_description: "Keep your computer awake while Codex is running a thread.",

View File

@@ -11,3 +11,16 @@ workspace = true
core-foundation = "0.9"
libc = { workspace = true }
tracing = { workspace = true }
[target.'cfg(target_os = "linux")'.dependencies]
dbus = "0.9"
tracing = { workspace = true }
[target.'cfg(target_os = "windows")'.dependencies]
tracing = { workspace = true }
windows-sys = { version = "0.60.2", features = [
"Win32_Foundation",
"Win32_System_Power",
"Win32_System_SystemServices",
"Win32_System_Threading",
] }

View File

@@ -3,10 +3,14 @@
//! 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"))]
#[cfg(not(any(target_os = "linux", target_os = "macos", target_os = "windows")))]
mod dummy;
#[cfg(target_os = "linux")]
mod linux_inhibitor;
#[cfg(target_os = "macos")]
mod macos_inhibitor;
#[cfg(target_os = "windows")]
mod windows_inhibitor;
use std::fmt::Debug;
@@ -27,7 +31,13 @@ impl SleepInhibitor {
#[cfg(target_os = "macos")]
let platform: Box<dyn PlatformSleepInhibitor> =
Box::new(macos_inhibitor::MacOsSleepInhibitor::new());
#[cfg(not(target_os = "macos"))]
#[cfg(target_os = "linux")]
let platform: Box<dyn PlatformSleepInhibitor> =
Box::new(linux_inhibitor::LinuxSleepInhibitor::new());
#[cfg(target_os = "windows")]
let platform: Box<dyn PlatformSleepInhibitor> =
Box::new(windows_inhibitor::WindowsSleepInhibitor::new());
#[cfg(not(any(target_os = "linux", target_os = "macos", target_os = "windows")))]
let platform: Box<dyn PlatformSleepInhibitor> = Box::new(dummy::DummySleepInhibitor::new());
Self { enabled, platform }

View File

@@ -0,0 +1,166 @@
use crate::PlatformSleepInhibitor;
use dbus::arg::OwnedFd;
use dbus::blocking::Connection;
use std::time::Duration;
use tracing::warn;
const ASSERTION_REASON: &str = "Codex is running an active turn";
const APP_ID: &str = "codex";
const DBUS_TIMEOUT: Duration = Duration::from_secs(2);
const GNOME_INHIBIT_SUSPEND_SESSION_FLAG: u32 = 4;
#[derive(Debug, Default)]
pub(crate) struct LinuxSleepInhibitor {
state: InhibitState,
}
#[derive(Debug, Default)]
enum InhibitState {
#[default]
Inactive,
Logind(OwnedFd),
Cookie {
api: CookieApi,
cookie: u32,
},
}
#[derive(Debug, Clone, Copy)]
enum CookieApi {
Gnome,
FreedesktopPower,
FreedesktopScreensaver,
}
impl CookieApi {
fn service_name(self) -> &'static str {
match self {
Self::Gnome => "org.gnome.SessionManager",
Self::FreedesktopPower => "org.freedesktop.PowerManagement",
Self::FreedesktopScreensaver => "org.freedesktop.ScreenSaver",
}
}
fn object_path(self) -> &'static str {
match self {
Self::Gnome => "/org/gnome/SessionManager",
Self::FreedesktopPower => "/org/freedesktop/PowerManagement/Inhibit",
Self::FreedesktopScreensaver => "/org/freedesktop/ScreenSaver",
}
}
fn interface_name(self) -> &'static str {
match self {
Self::Gnome => "org.gnome.SessionManager",
Self::FreedesktopPower => "org.freedesktop.PowerManagement.Inhibit",
Self::FreedesktopScreensaver => "org.freedesktop.ScreenSaver",
}
}
fn uninhibit_method(self) -> &'static str {
match self {
Self::Gnome => "Uninhibit",
Self::FreedesktopPower | Self::FreedesktopScreensaver => "UnInhibit",
}
}
}
impl LinuxSleepInhibitor {
pub(crate) fn new() -> Self {
Self::default()
}
}
impl PlatformSleepInhibitor for LinuxSleepInhibitor {
fn acquire(&mut self) {
if !matches!(self.state, InhibitState::Inactive) {
return;
}
match acquire_logind_inhibitor() {
Ok(state) => {
self.state = state;
return;
}
Err(error) => {
warn!(
error = %error,
"Failed to acquire sleep inhibitor via org.freedesktop.login1"
);
}
}
for api in [
CookieApi::Gnome,
CookieApi::FreedesktopPower,
CookieApi::FreedesktopScreensaver,
] {
match acquire_cookie_inhibitor(api) {
Ok(state) => {
self.state = state;
return;
}
Err(error) => {
warn!(?api, error = %error, "Failed to acquire sleep inhibitor via D-Bus");
}
}
}
warn!("No Linux sleep inhibition API is available");
}
fn release(&mut self) {
match std::mem::take(&mut self.state) {
InhibitState::Inactive => {}
InhibitState::Logind(fd) => drop(fd),
InhibitState::Cookie { api, cookie } => {
if let Err(error) = release_cookie_inhibitor(api, cookie) {
warn!(?api, error = %error, "Failed to release D-Bus sleep inhibitor");
}
}
}
}
}
fn acquire_logind_inhibitor() -> Result<InhibitState, dbus::Error> {
let connection = Connection::new_system()?;
let proxy = connection.with_proxy(
"org.freedesktop.login1",
"/org/freedesktop/login1",
DBUS_TIMEOUT,
);
let (fd,): (OwnedFd,) = proxy.method_call(
"org.freedesktop.login1.Manager",
"Inhibit",
("sleep", APP_ID, ASSERTION_REASON, "block"),
)?;
Ok(InhibitState::Logind(fd))
}
fn acquire_cookie_inhibitor(api: CookieApi) -> Result<InhibitState, dbus::Error> {
let connection = Connection::new_session()?;
let proxy = connection.with_proxy(api.service_name(), api.object_path(), DBUS_TIMEOUT);
let (cookie,): (u32,) = match api {
CookieApi::Gnome => proxy.method_call(
api.interface_name(),
"Inhibit",
(
APP_ID,
0_u32,
ASSERTION_REASON,
GNOME_INHIBIT_SUSPEND_SESSION_FLAG,
),
)?,
CookieApi::FreedesktopPower | CookieApi::FreedesktopScreensaver => {
proxy.method_call(api.interface_name(), "Inhibit", (APP_ID, ASSERTION_REASON))?
}
};
Ok(InhibitState::Cookie { api, cookie })
}
fn release_cookie_inhibitor(api: CookieApi, cookie: u32) -> Result<(), dbus::Error> {
let connection = Connection::new_session()?;
let proxy = connection.with_proxy(api.service_name(), api.object_path(), DBUS_TIMEOUT);
let _: () = proxy.method_call(api.interface_name(), api.uninhibit_method(), (cookie,))?;
Ok(())
}

View File

@@ -0,0 +1,93 @@
use crate::PlatformSleepInhibitor;
use std::ffi::OsStr;
use std::iter::once;
use std::os::windows::ffi::OsStrExt;
use tracing::warn;
use windows_sys::Win32::Foundation::CloseHandle;
use windows_sys::Win32::System::Power::POWER_REQUEST_TYPE;
use windows_sys::Win32::System::Power::PowerClearRequest;
use windows_sys::Win32::System::Power::PowerCreateRequest;
use windows_sys::Win32::System::Power::PowerRequestExecutionRequired;
use windows_sys::Win32::System::Power::PowerSetRequest;
use windows_sys::Win32::System::SystemServices::POWER_REQUEST_CONTEXT_VERSION;
use windows_sys::Win32::System::Threading::POWER_REQUEST_CONTEXT_SIMPLE_STRING;
use windows_sys::Win32::System::Threading::REASON_CONTEXT;
use windows_sys::Win32::System::Threading::REASON_CONTEXT_0;
const ASSERTION_REASON: &str = "Codex is running an active turn";
#[derive(Debug, Default)]
pub(crate) struct WindowsSleepInhibitor {
request: Option<PowerRequest>,
}
impl WindowsSleepInhibitor {
pub(crate) fn new() -> Self {
Self::default()
}
}
impl PlatformSleepInhibitor for WindowsSleepInhibitor {
fn acquire(&mut self) {
if self.request.is_some() {
return;
}
match PowerRequest::new_execution_required(ASSERTION_REASON) {
Ok(request) => {
self.request = Some(request);
}
Err(error) => {
warn!(
reason = error,
"Failed to acquire Windows sleep-prevention request"
);
}
}
}
fn release(&mut self) {
self.request = None;
}
}
#[derive(Debug)]
struct PowerRequest {
handle: windows_sys::Win32::Foundation::HANDLE,
request_type: POWER_REQUEST_TYPE,
}
impl PowerRequest {
fn new_execution_required(reason: &str) -> Result<Self, &'static str> {
let mut wide_reason: Vec<u16> = OsStr::new(reason).encode_wide().chain(once(0)).collect();
let context = REASON_CONTEXT {
Version: POWER_REQUEST_CONTEXT_VERSION,
Flags: POWER_REQUEST_CONTEXT_SIMPLE_STRING,
Reason: REASON_CONTEXT_0 {
SimpleReasonString: wide_reason.as_mut_ptr(),
},
};
let handle = unsafe { PowerCreateRequest(&context) };
if handle.is_null() {
return Err("PowerCreateRequest failed");
}
let request_type = PowerRequestExecutionRequired;
if unsafe { PowerSetRequest(handle, request_type) } == 0 {
let _ = unsafe { CloseHandle(handle) };
return Err("PowerSetRequest failed");
}
Ok(Self {
handle,
request_type,
})
}
}
impl Drop for PowerRequest {
fn drop(&mut self) {
let _ = unsafe { PowerClearRequest(self.handle, self.request_type) };
let _ = unsafe { CloseHandle(self.handle) };
}
}