Compare commits

...

7 Commits

Author SHA1 Message Date
Yaroslav Volovich
e182331dc7 fix(sleep-inhibitor): keep linux state on probe error 2026-02-18 11:08:16 +00:00
Yaroslav Volovich
ece9757260 Fix Windows HANDLE check for windows-sys 0.52 2026-02-18 11:08:16 +00:00
Yaroslav Volovich
c136143be9 Refine Linux and Windows sleep inhibitor robustness 2026-02-18 11:08:16 +00:00
Yaroslav Volovich
52db1ffcb3 Ensure Linux sleep inhibitor releases on drop 2026-02-18 11:08:16 +00:00
Yaroslav Volovich
e312e613ba fix(ci): use tracing-compatible io::Error field 2026-02-18 11:08:16 +00:00
Yaroslav Volovich
f81ccfa2ee fix(ci): avoid Linux dbus linkage in sleep inhibitor 2026-02-18 11:08:16 +00:00
Yaroslav Volovich
69dfa11ba9 feat(sleep-inhibitor): add Linux and Windows idle-sleep prevention 2026-02-18 11:08:15 +00:00
6 changed files with 307 additions and 3 deletions

1
codex-rs/Cargo.lock generated
View File

@@ -2505,6 +2505,7 @@ dependencies = [
"core-foundation 0.9.4",
"libc",
"tracing",
"windows-sys 0.52.0",
]
[[package]]

View File

@@ -626,7 +626,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,15 @@ workspace = true
core-foundation = "0.9"
libc = { workspace = true }
tracing = { workspace = true }
[target.'cfg(target_os = "linux")'.dependencies]
tracing = { workspace = true }
[target.'cfg(target_os = "windows")'.dependencies]
tracing = { workspace = true }
windows-sys = { version = "0.52", 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,182 @@
use crate::PlatformSleepInhibitor;
use std::process::Child;
use std::process::Command;
use tracing::warn;
const ASSERTION_REASON: &str = "Codex is running an active turn";
const APP_ID: &str = "codex";
// Keep the blocker process alive "long enough" without needing restarts.
// This is `i32::MAX` seconds, which is accepted by common `sleep` implementations.
const BLOCKER_SLEEP_SECONDS: &str = "2147483647";
#[derive(Debug, Default)]
pub(crate) struct LinuxSleepInhibitor {
state: InhibitState,
preferred_backend: Option<LinuxBackend>,
missing_backend_logged: bool,
}
#[derive(Debug, Default)]
enum InhibitState {
#[default]
Inactive,
Active {
backend: LinuxBackend,
child: Child,
},
}
#[derive(Debug, Clone, Copy)]
enum LinuxBackend {
SystemdInhibit,
GnomeSessionInhibit,
}
impl LinuxSleepInhibitor {
pub(crate) fn new() -> Self {
Self::default()
}
}
impl PlatformSleepInhibitor for LinuxSleepInhibitor {
fn acquire(&mut self) {
if let InhibitState::Active { backend, child } = &mut self.state {
match child.try_wait() {
Ok(None) => return,
Ok(Some(status)) => {
warn!(
?backend,
?status,
"Linux sleep inhibitor backend exited unexpectedly; attempting fallback"
);
}
Err(error) => {
warn!(
?backend,
reason = %error,
"Failed to query Linux sleep inhibitor backend status"
);
return;
}
}
}
self.state = InhibitState::Inactive;
let should_log_backend_failures = !self.missing_backend_logged;
let backends = match self.preferred_backend {
Some(LinuxBackend::SystemdInhibit) => [
LinuxBackend::SystemdInhibit,
LinuxBackend::GnomeSessionInhibit,
],
Some(LinuxBackend::GnomeSessionInhibit) => [
LinuxBackend::GnomeSessionInhibit,
LinuxBackend::SystemdInhibit,
],
None => [
LinuxBackend::SystemdInhibit,
LinuxBackend::GnomeSessionInhibit,
],
};
for backend in backends {
match spawn_backend(backend) {
Ok(mut child) => match child.try_wait() {
Ok(None) => {
self.state = InhibitState::Active { backend, child };
self.preferred_backend = Some(backend);
self.missing_backend_logged = false;
return;
}
Ok(Some(status)) => {
if should_log_backend_failures {
warn!(
?backend,
?status,
"Linux sleep inhibitor backend exited immediately"
);
}
}
Err(error) => {
if should_log_backend_failures {
warn!(
?backend,
reason = %error,
"Failed to query Linux sleep inhibitor backend status after spawn"
);
}
}
},
Err(error) => {
if should_log_backend_failures && error.kind() != std::io::ErrorKind::NotFound {
warn!(
?backend,
reason = %error,
"Failed to start Linux sleep inhibitor backend"
);
}
}
}
}
if should_log_backend_failures {
warn!("No Linux sleep inhibitor backend is available");
self.missing_backend_logged = true;
}
}
fn release(&mut self) {
match std::mem::take(&mut self.state) {
InhibitState::Inactive => {}
InhibitState::Active { backend, mut child } => {
if let Err(error) = child.kill()
&& !child_exited(&error)
{
warn!(?backend, reason = %error, "Failed to stop Linux sleep inhibitor backend");
}
if let Err(error) = child.wait()
&& !child_exited(&error)
{
warn!(?backend, reason = %error, "Failed to reap Linux sleep inhibitor backend");
}
}
}
}
}
impl Drop for LinuxSleepInhibitor {
fn drop(&mut self) {
self.release();
}
}
fn spawn_backend(backend: LinuxBackend) -> Result<Child, std::io::Error> {
match backend {
LinuxBackend::SystemdInhibit => Command::new("systemd-inhibit")
.args([
"--what=idle",
"--mode=block",
"--who",
APP_ID,
"--why",
ASSERTION_REASON,
"--",
"sleep",
BLOCKER_SLEEP_SECONDS,
])
.spawn(),
LinuxBackend::GnomeSessionInhibit => Command::new("gnome-session-inhibit")
.args([
"--inhibit",
"idle",
"--reason",
ASSERTION_REASON,
"sleep",
BLOCKER_SLEEP_SECONDS,
])
.spawn(),
}
}
fn child_exited(error: &std::io::Error) -> bool {
matches!(error.kind(), std::io::ErrorKind::InvalidInput)
}

View File

@@ -0,0 +1,95 @@
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, String> {
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 == 0 {
let error = std::io::Error::last_os_error();
return Err(format!("PowerCreateRequest failed: {error}"));
}
let request_type = PowerRequestExecutionRequired;
if unsafe { PowerSetRequest(handle, request_type) } == 0 {
let error = std::io::Error::last_os_error();
let _ = unsafe { CloseHandle(handle) };
return Err(format!("PowerSetRequest failed: {error}"));
}
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) };
}
}