mirror of
https://github.com/openai/codex.git
synced 2026-05-01 18:06:47 +00:00
## 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.
120 lines
4.2 KiB
Rust
120 lines
4.2 KiB
Rust
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::Foundation::INVALID_HANDLE_VALUE;
|
|
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::PowerRequestSystemRequired;
|
|
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>,
|
|
}
|
|
|
|
pub(crate) use WindowsSleepInhibitor as SleepInhibitor;
|
|
|
|
impl WindowsSleepInhibitor {
|
|
pub(crate) fn new() -> Self {
|
|
Self::default()
|
|
}
|
|
|
|
pub(crate) fn acquire(&mut self) {
|
|
if self.request.is_some() {
|
|
return;
|
|
}
|
|
|
|
match PowerRequest::new_system_required(ASSERTION_REASON) {
|
|
Ok(request) => {
|
|
self.request = Some(request);
|
|
}
|
|
Err(error) => {
|
|
warn!(
|
|
reason = %error,
|
|
"Failed to acquire Windows sleep-prevention request"
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
pub(crate) 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_system_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(),
|
|
},
|
|
};
|
|
// SAFETY: `context` points to a valid `REASON_CONTEXT` for the duration
|
|
// of the call and Windows copies the relevant data before returning.
|
|
let handle = unsafe { PowerCreateRequest(&context) };
|
|
if handle.is_null() || handle == INVALID_HANDLE_VALUE {
|
|
let error = std::io::Error::last_os_error();
|
|
return Err(format!("PowerCreateRequest failed: {error}"));
|
|
}
|
|
|
|
// Match macOS `PreventUserIdleSystemSleep`: prevent idle system sleep
|
|
// without forcing the display to stay on.
|
|
let request_type = PowerRequestSystemRequired;
|
|
// SAFETY: `handle` is a live power request handle and `request_type` is a
|
|
// valid power request enum value.
|
|
if unsafe { PowerSetRequest(handle, request_type) } == 0 {
|
|
let error = std::io::Error::last_os_error();
|
|
// SAFETY: `handle` was returned by `PowerCreateRequest` and has not
|
|
// been closed yet on this error path.
|
|
let _ = unsafe { CloseHandle(handle) };
|
|
return Err(format!("PowerSetRequest failed: {error}"));
|
|
}
|
|
|
|
Ok(Self {
|
|
handle,
|
|
request_type,
|
|
})
|
|
}
|
|
}
|
|
|
|
impl Drop for PowerRequest {
|
|
fn drop(&mut self) {
|
|
// SAFETY: `self.handle` is the handle owned by this `PowerRequest`, and
|
|
// `self.request_type` is the request type that was set on acquire.
|
|
if unsafe { PowerClearRequest(self.handle, self.request_type) } == 0 {
|
|
let error = std::io::Error::last_os_error();
|
|
warn!(
|
|
reason = %error,
|
|
"Failed to clear Windows sleep-prevention request"
|
|
);
|
|
}
|
|
// SAFETY: `self.handle` is owned by this struct and closed exactly once
|
|
// in `Drop`.
|
|
if unsafe { CloseHandle(self.handle) } == 0 {
|
|
let error = std::io::Error::last_os_error();
|
|
warn!(
|
|
reason = %error,
|
|
"Failed to close Windows sleep-prevention request handle"
|
|
);
|
|
}
|
|
}
|
|
}
|