Files
codex/codex-rs/utils/sleep-inhibitor/src/linux_inhibitor.rs
Yaroslav Volovich 67d9261e2c feat(sleep-inhibitor): add Linux and Windows idle-sleep prevention (#11766)
## Background
- follow-up to previous macOS-only PR:
https://github.com/openai/codex/pull/11711
- follow-up macOS refactor PR (current structural approach used here):
https://github.com/openai/codex/pull/12340

## Summary
- extend `codex-utils-sleep-inhibitor` with Linux and Windows backends
while preserving existing macOS behavior
- Linux backend:
  - use `systemd-inhibit` (`--what=idle --mode=block`) when available
- fall back to `gnome-session-inhibit` (`--inhibit idle`) when available
  - keep no-op behavior if neither backend exists on host
- Windows backend:
- use Win32 power request handles (`PowerCreateRequest` +
`PowerSetRequest` / `PowerClearRequest`) with
`PowerRequestSystemRequired`
- make `prevent_idle_sleep` Experimental on macOS/Linux/Windows; keep
under development on other targets

## Testing
- `just fmt`
- `cargo test -p codex-utils-sleep-inhibitor`
- `cargo test -p codex-core features::tests::`
- `cargo test -p codex-tui chatwidget::tests::`
- `just fix -p codex-utils-sleep-inhibitor`
- `just fix -p codex-core`

## Semantics and API references
- Goal remains: prevent idle system sleep while a turn is running.
- Linux:
  - `systemd-inhibit` / login1 inhibitor model:
-
https://www.freedesktop.org/software/systemd/man/latest/systemd-inhibit.html
-
https://www.freedesktop.org/software/systemd/man/org.freedesktop.login1.html
    - https://systemd.io/INHIBITOR_LOCKS/
  - xdg-desktop-portal Inhibit (relevant for sandboxed apps):
-
https://flatpak.github.io/xdg-desktop-portal/docs/doc-org.freedesktop.portal.Inhibit.html
- Windows:
  - `PowerCreateRequest`:
-
https://learn.microsoft.com/en-us/windows/win32/api/winbase/nf-winbase-powercreaterequest
  - `PowerSetRequest`:
-
https://learn.microsoft.com/en-us/windows/win32/api/winbase/nf-winbase-powersetrequest
  - `PowerClearRequest`:
-
https://learn.microsoft.com/en-us/windows/win32/api/winbase/nf-winbase-powerclearrequest
  - `SetThreadExecutionState` (alternative baseline API):
-
https://learn.microsoft.com/en-us/windows/win32/api/winbase/nf-winbase-setthreadexecutionstate

## Chromium vs this PR
- Chromium Linux backend:
-
https://github.com/chromium/chromium/blob/main/services/device/wake_lock/power_save_blocker/power_save_blocker_linux.cc
- Chromium Windows backend:
-
https://github.com/chromium/chromium/blob/main/services/device/wake_lock/power_save_blocker/power_save_blocker_win.cc
- Electron powerSaveBlocker entry point:
-
https://github.com/electron/electron/blob/main/shell/browser/api/electron_api_power_save_blocker.cc

## Why we differ from Chromium
- Linux implementation mechanism:
- Chromium uses in-process D-Bus APIs plus UI-integrated screen-saver
suspension.
- This PR uses command-based inhibitor backends (`systemd-inhibit`,
`gnome-session-inhibit`) instead of linking a Linux D-Bus client in this
crate.
- Reason: keep `codex-utils-sleep-inhibitor` dependency-light and avoid
Linux CI/toolchain fragility from new native D-Bus linkage, while
preserving the same runtime intent (hold an inhibitor while a turn
runs).
- Linux UI integration scope:
- Chromium also uses `display::Screen::SuspendScreenSaver()` in its UI
stack.
- Codex `codex-rs` does not have that display abstraction in this crate,
so this PR scopes Linux behavior to process-level sleep inhibition only.
- Windows wake-lock type breadth:
- Chromium supports both display/system wake-lock types and extra
display-specific handling for some pre-Win11 scenarios.
- Codex’s feature is scoped to turn execution continuity (not forcing
display on), so this PR uses `PowerRequestSystemRequired` only.
2026-02-24 11:51:44 -08:00

241 lines
7.9 KiB
Rust

use std::os::unix::process::CommandExt;
use std::process::Child;
use std::process::Command;
use std::process::Stdio;
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,
}
pub(crate) use LinuxSleepInhibitor as SleepInhibitor;
#[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()
}
pub(crate) 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; attempting restart"
);
}
}
}
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"
);
}
if let Err(kill_error) = child.kill()
&& !child_exited(&kill_error)
{
warn!(
?backend,
reason = %kill_error,
"Failed to stop Linux sleep inhibitor backend after status probe failure"
);
}
if let Err(wait_error) = child.wait()
&& !child_exited(&wait_error)
{
warn!(
?backend,
reason = %wait_error,
"Failed to reap Linux sleep inhibitor backend after status probe failure"
);
}
}
},
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;
}
}
pub(crate) 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> {
// Ensure the helper receives SIGTERM when the original parent dies.
// `parent_pid` is captured before spawn and checked in `pre_exec` to avoid
// the fork/exec race where the parent exits before PDEATHSIG is armed.
// SAFETY: `getpid` has no preconditions and is safe to call here.
let parent_pid = unsafe { libc::getpid() };
let mut command = match backend {
LinuxBackend::SystemdInhibit => {
let mut command = Command::new("systemd-inhibit");
command.args([
"--what=idle",
"--mode=block",
"--who",
APP_ID,
"--why",
ASSERTION_REASON,
"--",
"sleep",
BLOCKER_SLEEP_SECONDS,
]);
command
}
LinuxBackend::GnomeSessionInhibit => {
let mut command = Command::new("gnome-session-inhibit");
command.args([
"--inhibit",
"idle",
"--reason",
ASSERTION_REASON,
"sleep",
BLOCKER_SLEEP_SECONDS,
]);
command
}
};
command
.stdin(Stdio::null())
.stdout(Stdio::null())
.stderr(Stdio::null());
// SAFETY: `pre_exec` must be registered before spawn. The closure only
// performs libc setup for the child process and returns an `io::Error`
// when parent-death signal setup fails.
unsafe {
command.pre_exec(move || {
if libc::prctl(libc::PR_SET_PDEATHSIG, libc::SIGTERM) == -1 {
return Err(std::io::Error::last_os_error());
}
if libc::getppid() != parent_pid {
libc::raise(libc::SIGTERM);
}
Ok(())
});
}
command.spawn()
}
fn child_exited(error: &std::io::Error) -> bool {
matches!(error.kind(), std::io::ErrorKind::InvalidInput)
}
#[cfg(test)]
mod tests {
use super::BLOCKER_SLEEP_SECONDS;
#[test]
fn sleep_seconds_is_i32_max() {
assert_eq!(BLOCKER_SLEEP_SECONDS, format!("{}", i32::MAX));
}
}