mirror of
https://github.com/openai/codex.git
synced 2026-11-06 08:31:30 +00:00
## Why Managed app-server shutdowns use a fixed 60-second grace period. Allow users to adjust how long shutdown waits before forcing the process to exit. ## What changed - Add `shutdownGraceSeconds` to daemon settings, accepting integers from 0 through 300 and defaulting to 60. Zero requests graceful shutdown and then forces termination immediately. - Apply the setting to managed app-server stops and restarts, including updater-triggered restarts, and extend the lifecycle lock timeout to accommodate the maximum grace period. - Keep `stop` usable with unreadable or invalid settings by falling back to the default grace period. ## Testing Add tests for setting defaults and bounds, invalid-value fallback, preservation when saving remote-control settings, graceful process exit, and forced termination with zero or finite grace periods. GitOrigin-RevId: 8627b391323cf27b40df96935a82e66aa715f572
196 lines
6.0 KiB
Rust
196 lines
6.0 KiB
Rust
use std::path::Path;
|
|
use std::time::Duration;
|
|
|
|
use anyhow::Context;
|
|
use anyhow::Result;
|
|
use anyhow::ensure;
|
|
use serde::Deserialize;
|
|
use serde::de::DeserializeOwned;
|
|
use serde_json::Map;
|
|
use serde_json::Value;
|
|
use tokio::fs;
|
|
|
|
pub(crate) const DEFAULT_UPDATE_INTERVAL_MINUTES: u32 = 60;
|
|
pub(crate) const DEFAULT_SHUTDOWN_GRACE_SECONDS: u32 = 60;
|
|
pub(crate) const MAX_SHUTDOWN_GRACE_SECONDS: u32 = 5 * 60;
|
|
|
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
|
pub(crate) struct DaemonSettings {
|
|
pub(crate) remote_control_enabled: bool,
|
|
pub(crate) auto_update_enabled: bool,
|
|
pub(crate) update_interval_minutes: u32,
|
|
pub(crate) shutdown_grace_seconds: u32,
|
|
}
|
|
|
|
impl Default for DaemonSettings {
|
|
fn default() -> Self {
|
|
Self {
|
|
remote_control_enabled: false,
|
|
auto_update_enabled: true,
|
|
update_interval_minutes: DEFAULT_UPDATE_INTERVAL_MINUTES,
|
|
shutdown_grace_seconds: DEFAULT_SHUTDOWN_GRACE_SECONDS,
|
|
}
|
|
}
|
|
}
|
|
|
|
#[derive(Default, Deserialize)]
|
|
#[serde(rename_all = "camelCase")]
|
|
struct StopSettings {
|
|
shutdown_grace_seconds: Option<u32>,
|
|
}
|
|
|
|
#[derive(Deserialize)]
|
|
#[serde(rename_all = "camelCase")]
|
|
struct StoredSettings {
|
|
#[serde(default)]
|
|
remote_control_enabled: bool,
|
|
#[serde(default = "default_shutdown_grace_seconds")]
|
|
shutdown_grace_seconds: u32,
|
|
#[serde(default)]
|
|
updater: UpdaterSettings,
|
|
}
|
|
|
|
impl Default for StoredSettings {
|
|
fn default() -> Self {
|
|
Self {
|
|
remote_control_enabled: false,
|
|
shutdown_grace_seconds: DEFAULT_SHUTDOWN_GRACE_SECONDS,
|
|
updater: UpdaterSettings::default(),
|
|
}
|
|
}
|
|
}
|
|
|
|
#[derive(Debug, Deserialize)]
|
|
#[serde(rename_all = "camelCase")]
|
|
pub(crate) struct UpdaterSettings {
|
|
#[serde(default = "default_auto_update_enabled")]
|
|
pub(crate) auto_update_enabled: bool,
|
|
#[serde(default = "default_update_interval_minutes")]
|
|
pub(crate) update_interval_minutes: u32,
|
|
}
|
|
|
|
impl Default for UpdaterSettings {
|
|
fn default() -> Self {
|
|
Self {
|
|
auto_update_enabled: default_auto_update_enabled(),
|
|
update_interval_minutes: default_update_interval_minutes(),
|
|
}
|
|
}
|
|
}
|
|
|
|
fn default_auto_update_enabled() -> bool {
|
|
true
|
|
}
|
|
|
|
fn default_update_interval_minutes() -> u32 {
|
|
DEFAULT_UPDATE_INTERVAL_MINUTES
|
|
}
|
|
|
|
fn default_shutdown_grace_seconds() -> u32 {
|
|
DEFAULT_SHUTDOWN_GRACE_SECONDS
|
|
}
|
|
|
|
fn validate_shutdown_grace(seconds: u32) -> Result<()> {
|
|
ensure!(
|
|
seconds <= MAX_SHUTDOWN_GRACE_SECONDS,
|
|
"shutdown grace must be between 0 and {MAX_SHUTDOWN_GRACE_SECONDS} seconds"
|
|
);
|
|
Ok(())
|
|
}
|
|
|
|
impl UpdaterSettings {
|
|
pub(crate) async fn load(settings_file: &Path) -> Result<Self> {
|
|
let settings: StoredSettings = read_settings(settings_file).await?;
|
|
validate_shutdown_grace(settings.shutdown_grace_seconds)?;
|
|
let settings = settings.updater;
|
|
settings.validate()?;
|
|
Ok(settings)
|
|
}
|
|
|
|
pub(crate) fn validate(&self) -> Result<()> {
|
|
ensure!(
|
|
self.update_interval_minutes > 0,
|
|
"update interval must be positive"
|
|
);
|
|
Ok(())
|
|
}
|
|
|
|
pub(crate) fn update_interval(&self, minute: Duration) -> Duration {
|
|
minute * self.update_interval_minutes
|
|
}
|
|
}
|
|
|
|
impl DaemonSettings {
|
|
pub(crate) async fn load(path: &Path) -> Result<Self> {
|
|
let settings: StoredSettings = read_settings(path).await?;
|
|
settings.updater.validate()?;
|
|
validate_shutdown_grace(settings.shutdown_grace_seconds)?;
|
|
Ok(Self {
|
|
remote_control_enabled: settings.remote_control_enabled,
|
|
auto_update_enabled: settings.updater.auto_update_enabled,
|
|
update_interval_minutes: settings.updater.update_interval_minutes,
|
|
shutdown_grace_seconds: settings.shutdown_grace_seconds,
|
|
})
|
|
}
|
|
|
|
pub(crate) async fn load_for_stop(path: &Path) -> Self {
|
|
// Stop must work even when settings are unreadable or partially edited.
|
|
let shutdown_grace_seconds = read_settings::<StopSettings>(path)
|
|
.await
|
|
.ok()
|
|
.and_then(|settings| settings.shutdown_grace_seconds)
|
|
.filter(|&seconds| seconds <= MAX_SHUTDOWN_GRACE_SECONDS)
|
|
.unwrap_or(DEFAULT_SHUTDOWN_GRACE_SECONDS);
|
|
Self {
|
|
shutdown_grace_seconds,
|
|
..Self::default()
|
|
}
|
|
}
|
|
|
|
pub(crate) async fn save(&self, path: &Path) -> Result<()> {
|
|
if let Some(parent) = path.parent() {
|
|
fs::create_dir_all(parent).await.with_context(|| {
|
|
format!(
|
|
"failed to create daemon settings directory {}",
|
|
parent.display()
|
|
)
|
|
})?;
|
|
}
|
|
let mut settings: Map<String, Value> = read_settings(path).await?;
|
|
settings.insert(
|
|
"remoteControlEnabled".to_string(),
|
|
Value::Bool(self.remote_control_enabled),
|
|
);
|
|
let contents =
|
|
serde_json::to_vec_pretty(&settings).context("failed to serialize settings")?;
|
|
let temporary_path = path.with_extension("tmp");
|
|
fs::write(&temporary_path, contents)
|
|
.await
|
|
.with_context(|| {
|
|
format!(
|
|
"failed to write daemon settings {}",
|
|
temporary_path.display()
|
|
)
|
|
})?;
|
|
fs::rename(&temporary_path, path)
|
|
.await
|
|
.with_context(|| format!("failed to replace daemon settings {}", path.display()))
|
|
}
|
|
}
|
|
|
|
async fn read_settings<T: DeserializeOwned + Default>(path: &Path) -> Result<T> {
|
|
let contents = match fs::read_to_string(path).await {
|
|
Ok(contents) => contents,
|
|
Err(err) if err.kind() == std::io::ErrorKind::NotFound => return Ok(T::default()),
|
|
Err(err) => {
|
|
return Err(err).with_context(|| format!("failed to read settings {}", path.display()));
|
|
}
|
|
};
|
|
serde_json::from_str(&contents)
|
|
.with_context(|| format!("failed to parse settings {}", path.display()))
|
|
}
|
|
|
|
#[cfg(test)]
|
|
#[path = "settings_tests.rs"]
|
|
mod tests;
|