mirror of
https://github.com/openai/codex.git
synced 2026-09-15 12:08:01 +00:00
feat(sleep-inhibitor): add Linux and Windows idle-sleep prevention
This commit is contained in:
2
codex-rs/Cargo.lock
generated
2
codex-rs/Cargo.lock
generated
@@ -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]]
|
||||
|
||||
@@ -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.",
|
||||
|
||||
@@ -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",
|
||||
] }
|
||||
|
||||
@@ -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 }
|
||||
|
||||
166
codex-rs/utils/sleep-inhibitor/src/linux_inhibitor.rs
Normal file
166
codex-rs/utils/sleep-inhibitor/src/linux_inhibitor.rs
Normal 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(())
|
||||
}
|
||||
93
codex-rs/utils/sleep-inhibitor/src/windows_inhibitor.rs
Normal file
93
codex-rs/utils/sleep-inhibitor/src/windows_inhibitor.rs
Normal 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) };
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user