mirror of
https://github.com/openai/codex.git
synced 2026-10-20 17:38:45 +00:00
Offer explicit recovery for incompatible background servers (#47318)
## Why Automatic daemon launches could silently fall back to embedded mode when shared feature settings differed from the session's requirements. Changing those settings affects other clients, and restarting the server may interrupt active or queued work. ## What changed - Offer interactive choices to run without the daemon, restart with the required settings, or cancel. Default to cancel and require explicit confirmation for restart. - Allow restart only for managed daemons with a feature mismatch, then recheck compatibility once. Noninteractive required-daemon failures return an error with `--no-daemon` guidance; optional attachment retains its fallback behavior. - Preserve saved feature overrides on startup and require confirmation before applying requested shared-feature disables, including on fresh starts. - Merge confirmed settings under the lifecycle lock, persist changes after the old process stops, and avoid restarting when the saved overrides already match. ## Testing Add recovery-menu snapshots and confirmation tests, compatibility tests for required and optional attachment, daemon ownership and settings-preservation tests, and CLI terminal tests for cancellation, embedded fallback, confirmed restart, and disabling shared features. GitOrigin-RevId: d50a6cf7e476b9647237431e8fc0fc6041ed17db
This commit is contained in:
@@ -1,13 +1,15 @@
|
||||
//! Invocation feature overrides apply only when starting a missing daemon.
|
||||
//! The lifecycle lock protects their persistence against concurrent starts and updates.
|
||||
//! Invocation overrides apply on fresh starts or explicitly confirmed managed restarts.
|
||||
//! The lifecycle lock serializes persistence against other clients and updates.
|
||||
|
||||
use crate::Daemon;
|
||||
use crate::LifecycleOutput;
|
||||
use crate::ensure_supported_platform;
|
||||
use anyhow::Result;
|
||||
use anyhow::ensure;
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
/// Start a missing daemon with these features, or leave a running daemon unchanged.
|
||||
/// Start a missing daemon with these features, preserving other saved overrides,
|
||||
/// or leave a running daemon unchanged.
|
||||
/// Callers must check the running server's configuration before using it.
|
||||
pub async fn start_with_features(features: &BTreeMap<String, bool>) -> Result<LifecycleOutput> {
|
||||
ensure_supported_platform()?;
|
||||
@@ -16,5 +18,44 @@ pub async fn start_with_features(features: &BTreeMap<String, bool>) -> Result<Li
|
||||
let daemon = Daemon::from_environment()?;
|
||||
let _operation_lock = daemon.acquire_operation_lock().await?;
|
||||
let selected = daemon.current_installation()?;
|
||||
Box::pin(selected.start(features)).await
|
||||
let mut overrides = selected.load_settings().await?.feature_overrides;
|
||||
overrides.extend(features.clone());
|
||||
Box::pin(selected.start(&overrides)).await
|
||||
}
|
||||
|
||||
/// Apply confirmed feature settings to an existing managed daemon and restart it.
|
||||
/// Callers must obtain consent for changes to shared services and interrupted work,
|
||||
/// then verify effective server compatibility after this operation completes.
|
||||
pub async fn restart_with_features(features: &BTreeMap<String, bool>) -> Result<LifecycleOutput> {
|
||||
ensure_supported_platform()?;
|
||||
#[cfg(windows)]
|
||||
crate::backend::windows::ensure_not_elevated()?;
|
||||
let daemon = Daemon::from_environment()?;
|
||||
let _operation_lock = daemon.acquire_operation_lock().await?;
|
||||
let selected = daemon.current_installation()?;
|
||||
Box::pin(selected.restart_with_features_locked(features)).await
|
||||
}
|
||||
|
||||
impl Daemon {
|
||||
pub(super) async fn restart(&self) -> Result<LifecycleOutput> {
|
||||
self.restart_with_settings(self.load_settings().await?)
|
||||
.await
|
||||
}
|
||||
|
||||
pub(super) async fn restart_with_features_locked(
|
||||
&self,
|
||||
features: &BTreeMap<String, bool>,
|
||||
) -> Result<LifecycleOutput> {
|
||||
let mut settings = self.load_settings().await?;
|
||||
ensure!(
|
||||
self.running_backend_instance(&settings).await?.is_some(),
|
||||
"no running managed daemon; rerun the command to check the current server"
|
||||
);
|
||||
let previous = settings.feature_overrides.clone();
|
||||
settings.feature_overrides.extend(features.clone());
|
||||
if settings.feature_overrides == previous {
|
||||
return self.start(&previous).await;
|
||||
}
|
||||
self.restart_with_settings(settings).await
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ use backend::windows::try_lock_file;
|
||||
mod client;
|
||||
mod install_lock;
|
||||
mod launch;
|
||||
pub use launch::restart_with_features;
|
||||
pub use launch::start_with_features;
|
||||
mod managed_install;
|
||||
mod prepare_install;
|
||||
@@ -437,8 +438,7 @@ impl Daemon {
|
||||
.await)
|
||||
}
|
||||
|
||||
async fn restart(&self) -> Result<LifecycleOutput> {
|
||||
let settings = self.load_settings().await?;
|
||||
async fn restart_with_settings(&self, settings: DaemonSettings) -> Result<LifecycleOutput> {
|
||||
if client::probe(&self.socket_path).await.is_ok()
|
||||
&& self.running_backend(&settings).await?.is_none()
|
||||
{
|
||||
@@ -465,6 +465,11 @@ impl Daemon {
|
||||
.await?;
|
||||
}
|
||||
|
||||
// Persist changed launch settings only after the old process has stopped.
|
||||
// A failed or interrupted drain must not make an unapplied change look current.
|
||||
if self.load_settings().await? != settings {
|
||||
settings.save(&self.settings_file).await?;
|
||||
}
|
||||
let pid = managed.start_managed_backend(&settings).await?;
|
||||
let info = self.wait_until_ready().await?;
|
||||
if let Err(err) = managed.ensure_managed_updater(&settings).await {
|
||||
|
||||
@@ -617,6 +617,74 @@ async fn daemon_start_and_restart_preserve_launch_features() {
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
#[tokio::test]
|
||||
async fn confirmed_feature_restart_preserves_ownership_and_skips_matching_settings() {
|
||||
use crate::LifecycleStatus;
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
for managed in [true, false] {
|
||||
let home = TempDir::new().unwrap();
|
||||
let (daemon, _) = manual_update_daemon(&home);
|
||||
std::fs::write(&daemon.settings_file,
|
||||
r#"{"featureOverrides":{"auth_elicitation":true,"api_key_model_discovery":true},"updater":{"autoUpdateEnabled":false},"shutdownGraceSeconds":0}"#
|
||||
).unwrap();
|
||||
let original = daemon.load_settings().await.unwrap();
|
||||
if managed {
|
||||
daemon.start_managed_backend(&original).await.unwrap();
|
||||
}
|
||||
let server = test_control_server(&daemon, home.path()).await;
|
||||
let _lock = daemon.acquire_operation_lock().await.unwrap();
|
||||
let requested = BTreeMap::from([
|
||||
("api_key_model_discovery".to_string(), false),
|
||||
("mcp_oauth_refresh_coordination".to_string(), true),
|
||||
]);
|
||||
if managed {
|
||||
// Hide the selection without removing the script the spawned shell still needs.
|
||||
let selected_package = daemon.managed_codex_bin.parent().unwrap();
|
||||
let saved_package = selected_package.with_extension("saved");
|
||||
std::fs::rename(selected_package, &saved_package).unwrap();
|
||||
let error = daemon
|
||||
.restart_with_features_locked(&requested)
|
||||
.await
|
||||
.unwrap_err();
|
||||
std::fs::rename(saved_package, selected_package).unwrap();
|
||||
assert!(
|
||||
error.to_string().contains("daemon executable not found"),
|
||||
"{error:#}"
|
||||
);
|
||||
assert_eq!(daemon.load_settings().await.unwrap(), original);
|
||||
}
|
||||
let result = daemon.restart_with_features_locked(&requested).await;
|
||||
if managed {
|
||||
assert_eq!(result.unwrap().status, LifecycleStatus::Restarted);
|
||||
let pid = std::fs::read(&daemon.pid_file).unwrap();
|
||||
let mut expected = original;
|
||||
expected.feature_overrides.extend(requested.clone());
|
||||
assert_eq!(daemon.load_settings().await.unwrap(), expected);
|
||||
assert_eq!(
|
||||
daemon
|
||||
.restart_with_features_locked(&requested)
|
||||
.await
|
||||
.unwrap()
|
||||
.status,
|
||||
LifecycleStatus::AlreadyRunning
|
||||
);
|
||||
assert_eq!(std::fs::read(&daemon.pid_file).unwrap(), pid);
|
||||
daemon.stop().await.unwrap();
|
||||
} else {
|
||||
assert!(
|
||||
result
|
||||
.unwrap_err()
|
||||
.to_string()
|
||||
.contains("no running managed daemon")
|
||||
);
|
||||
assert_eq!(daemon.load_settings().await.unwrap(), original);
|
||||
}
|
||||
server.abort();
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
#[tokio::test]
|
||||
async fn manual_update_restarts_local_daemon_with_automatic_updates_disabled() {
|
||||
|
||||
Reference in New Issue
Block a user