mirror of
https://github.com/openai/codex.git
synced 2026-11-03 19:53:25 +00:00
## Why We are seeing cases where users have an old background app-server still running. `codex doctor` already reports background server state, but without the running app-server version it is harder to diagnose behaviors that depend on the daemon build. ## What changed - Reused the app-server daemon's passive initialize probe through a narrow `probe_app_server_version` helper. - Updated the `codex doctor` Background Server section to report `app-server version: <version>` when the socket is reachable. - Preserved the not-running OK behavior and report `app-server version: unavailable (<short error>)` when a socket exists but the passive probe fails.
260 lines
8.4 KiB
Rust
260 lines
8.4 KiB
Rust
//! Reports app-server daemon state without starting or stopping the daemon.
|
|
//!
|
|
//! The background-server check is deliberately passive. It reads the daemon
|
|
//! state directory, PID files, settings file, and control socket path, then
|
|
//! attempts only a bounded initialize probe when a socket already exists. That
|
|
//! keeps doctor safe to run while the user is debugging startup or update-loop
|
|
//! issues.
|
|
|
|
use std::path::Path;
|
|
|
|
use codex_core::config::Config;
|
|
|
|
use super::CheckStatus;
|
|
use super::DoctorCheck;
|
|
|
|
const MAX_PROBE_ERROR_CHARS: usize = 120;
|
|
const STATE_DIR_NAME: &str = "app-server-daemon";
|
|
const SETTINGS_FILE_NAME: &str = "settings.json";
|
|
const PID_FILE_NAME: &str = "app-server.pid";
|
|
const UPDATE_PID_FILE_NAME: &str = "app-server-updater.pid";
|
|
|
|
/// Builds the app-server status row from existing daemon state.
|
|
///
|
|
/// Missing files are expected for the ephemeral/not-running case and should not
|
|
/// be treated as failures. A stale socket is a warning because it can explain
|
|
/// client connection problems without proving the daemon itself is broken.
|
|
pub(super) async fn background_server_check(config: &Config) -> DoctorCheck {
|
|
let mut details = Vec::new();
|
|
let state_dir = config.codex_home.join(STATE_DIR_NAME);
|
|
details.push(format!("daemon state dir: {}", state_dir.display()));
|
|
push_file_detail(
|
|
&mut details,
|
|
"settings",
|
|
&state_dir.join(SETTINGS_FILE_NAME),
|
|
);
|
|
push_file_detail(&mut details, "pid file", &state_dir.join(PID_FILE_NAME));
|
|
push_file_detail(
|
|
&mut details,
|
|
"update-loop pid file",
|
|
&state_dir.join(UPDATE_PID_FILE_NAME),
|
|
);
|
|
|
|
let socket_path = match codex_app_server::app_server_control_socket_path(&config.codex_home) {
|
|
Ok(socket_path) => socket_path,
|
|
Err(err) => {
|
|
return DoctorCheck::new(
|
|
"app_server.status",
|
|
"app-server",
|
|
CheckStatus::Warning,
|
|
"background server socket path could not be resolved",
|
|
)
|
|
.details(details)
|
|
.detail(err.to_string());
|
|
}
|
|
};
|
|
|
|
details.push(format!("control socket: {}", socket_path.display()));
|
|
let status = socket_status(socket_path.as_path()).await;
|
|
details.push(format!("status: {}", status.detail_label()));
|
|
if let Some(version_detail) = status.app_server_version_detail() {
|
|
details.push(version_detail);
|
|
}
|
|
details.push(format!("mode: {}", server_mode(&state_dir)));
|
|
|
|
let mut check = DoctorCheck::new(
|
|
"app_server.status",
|
|
"app-server",
|
|
status.check_status(),
|
|
status.summary(),
|
|
)
|
|
.details(details);
|
|
if status.check_status() == CheckStatus::Warning {
|
|
check = check.remediation("Run codex app-server daemon version for more details.");
|
|
}
|
|
check
|
|
}
|
|
|
|
fn push_file_detail(details: &mut Vec<String>, label: &str, path: &Path) {
|
|
match std::fs::metadata(path) {
|
|
Ok(metadata) if metadata.is_file() => {
|
|
details.push(format!("{label}: {} (file)", path.display()));
|
|
}
|
|
Ok(_) => {
|
|
details.push(format!("{label}: {} (not a file)", path.display()));
|
|
}
|
|
Err(err) if err.kind() == std::io::ErrorKind::NotFound => {
|
|
details.push(format!("{label}: {} (missing)", path.display()));
|
|
}
|
|
Err(err) => details.push(format!("{label}: {} ({err})", path.display())),
|
|
}
|
|
}
|
|
|
|
fn server_mode(state_dir: &Path) -> &'static str {
|
|
if state_dir.join(SETTINGS_FILE_NAME).is_file() {
|
|
"persistent"
|
|
} else {
|
|
"ephemeral"
|
|
}
|
|
}
|
|
|
|
enum SocketStatus {
|
|
NotRunning,
|
|
Running(String),
|
|
StaleOrUnreachable(String),
|
|
}
|
|
|
|
impl SocketStatus {
|
|
fn check_status(&self) -> CheckStatus {
|
|
match self {
|
|
Self::NotRunning | Self::Running(_) => CheckStatus::Ok,
|
|
Self::StaleOrUnreachable(_) => CheckStatus::Warning,
|
|
}
|
|
}
|
|
|
|
fn summary(&self) -> &'static str {
|
|
match self {
|
|
Self::NotRunning => "background server is not running",
|
|
Self::Running(_) => "background server is running",
|
|
Self::StaleOrUnreachable(_) => "background server socket is stale or unreachable",
|
|
}
|
|
}
|
|
|
|
fn detail_label(&self) -> &'static str {
|
|
match self {
|
|
Self::NotRunning => "not running",
|
|
Self::Running(_) => "running",
|
|
Self::StaleOrUnreachable(_) => "stale or unreachable",
|
|
}
|
|
}
|
|
|
|
fn app_server_version_detail(&self) -> Option<String> {
|
|
match self {
|
|
Self::NotRunning => None,
|
|
Self::Running(app_server_version) => {
|
|
Some(format!("app-server version: {app_server_version}"))
|
|
}
|
|
Self::StaleOrUnreachable(error) => {
|
|
Some(format!("app-server version: unavailable ({error})"))
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
async fn socket_status(socket_path: &Path) -> SocketStatus {
|
|
if !socket_path.exists() {
|
|
return SocketStatus::NotRunning;
|
|
}
|
|
|
|
match codex_app_server_daemon::probe_app_server_version(socket_path).await {
|
|
Ok(app_server_version) => SocketStatus::Running(app_server_version),
|
|
Err(err) => SocketStatus::StaleOrUnreachable(concise_probe_error(&err, socket_path)),
|
|
}
|
|
}
|
|
|
|
fn concise_probe_error(err: &anyhow::Error, socket_path: &Path) -> String {
|
|
let socket_path = socket_path.display().to_string();
|
|
let message = err
|
|
.to_string()
|
|
.replace(&socket_path, "control socket")
|
|
.split_whitespace()
|
|
.collect::<Vec<_>>()
|
|
.join(" ");
|
|
if message.is_empty() {
|
|
return "unknown error".to_string();
|
|
}
|
|
let mut chars = message.chars();
|
|
let truncated = chars
|
|
.by_ref()
|
|
.take(MAX_PROBE_ERROR_CHARS)
|
|
.collect::<String>();
|
|
if chars.next().is_some() {
|
|
format!("{truncated}...")
|
|
} else {
|
|
message
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use std::path::PathBuf;
|
|
|
|
use codex_core::config::ConfigBuilder;
|
|
use pretty_assertions::assert_eq;
|
|
|
|
use super::*;
|
|
|
|
async fn test_config(codex_home: PathBuf) -> Config {
|
|
ConfigBuilder::default()
|
|
.codex_home(codex_home)
|
|
.build()
|
|
.await
|
|
.expect("config")
|
|
}
|
|
|
|
fn create_socket_placeholder(config: &Config) {
|
|
let socket_path = codex_app_server::app_server_control_socket_path(&config.codex_home)
|
|
.expect("socket path");
|
|
std::fs::create_dir_all(socket_path.parent().expect("socket parent"))
|
|
.expect("create socket dir");
|
|
std::fs::write(socket_path, "").expect("create socket placeholder");
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn not_running_background_server_stays_ok_without_version() {
|
|
let temp = tempfile::tempdir().expect("tempdir");
|
|
let config = test_config(temp.path().to_path_buf()).await;
|
|
|
|
let check = background_server_check(&config).await;
|
|
|
|
assert_eq!(check.status, CheckStatus::Ok);
|
|
assert_eq!(check.summary, "background server is not running");
|
|
assert!(check.details.contains(&"status: not running".to_string()));
|
|
assert!(
|
|
!check
|
|
.details
|
|
.iter()
|
|
.any(|detail| detail.starts_with("app-server version:"))
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn running_background_server_reports_app_server_version() {
|
|
let status = SocketStatus::Running("1.2.3".to_string());
|
|
|
|
assert_eq!(status.check_status(), CheckStatus::Ok);
|
|
assert_eq!(status.summary(), "background server is running");
|
|
assert_eq!(status.detail_label(), "running");
|
|
assert_eq!(
|
|
status.app_server_version_detail(),
|
|
Some("app-server version: 1.2.3".to_string())
|
|
);
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn failed_version_probe_reports_unavailable() {
|
|
let temp = tempfile::tempdir().expect("tempdir");
|
|
let config = test_config(temp.path().to_path_buf()).await;
|
|
create_socket_placeholder(&config);
|
|
|
|
let check = background_server_check(&config).await;
|
|
|
|
assert_eq!(check.status, CheckStatus::Warning);
|
|
assert_eq!(
|
|
check.summary,
|
|
"background server socket is stale or unreachable"
|
|
);
|
|
assert!(
|
|
check
|
|
.details
|
|
.contains(&"status: stale or unreachable".to_string())
|
|
);
|
|
assert!(
|
|
check
|
|
.details
|
|
.iter()
|
|
.any(|detail| detail.starts_with("app-server version: unavailable ("))
|
|
);
|
|
}
|
|
}
|