Recover daemon startup and updater re-exec after cwd deletion (#49819)

## Why

Managed daemons and updaters can outlive their launching directory. Restarts and updater replacement need to keep working after that directory is removed.

## What changed

- Share working-directory preparation between background launches and Unix updater re-execution. Fall back to the daemon state directory when the Unix working directory is missing, while retaining usable working directories and the existing Windows state-directory behavior.
- Preserve configured `CODEX_HOME` paths and valid symlink aliases, and make certificate and AWS environment paths absolute before launching children. Handle both `npm_config_cafile` and `NPM_CONFIG_CAFILE`, preserving supported home-relative values.
- Log warnings when scheduled daemon updates fail.

## Testing

Add Unix regression coverage for restarting the daemon and updater from a deleted working directory, and for updater re-execution with preserved environment paths, `CODEX_HOME` symlink aliases, and a Linux non-Unicode home path.

GitOrigin-RevId: f45aefd8ff060adc6e025badf0bb8c81f842bf88
This commit is contained in:
Eric Traut
2026-10-01 02:06:50 +00:00
committed by copyberry
parent a73898c249
commit cda82a2c68
6 changed files with 311 additions and 61 deletions
+57
View File
@@ -114,6 +114,63 @@ impl Drop for TestDaemon {
}
}
#[test]
fn managed_daemon_restarts_from_deleted_working_directory() -> Result<()> {
let daemon = TestDaemon::new()?;
let project = daemon.home.path().join("project");
std::fs::create_dir(&project)?;
let started = daemon
.command()
.current_dir(&project)
.args(["app-server", "daemon", "start"])
.output()?;
ensure!(
started.status.success(),
"{}",
String::from_utf8_lossy(&started.stderr)
);
let old_pid = daemon.pid("app-server.pid")?;
let old_updater_pid = daemon.pid("app-server-updater.pid")?;
signal(old_updater_pid, libc::SIGTERM)?;
wait_for_exit(old_updater_pid)?;
// Delete the restart caller's cwd without changing the test process's cwd.
let restarted = Command::new("/bin/sh")
.args([
"-c",
"rmdir \"$1\" && exec \"$2\" app-server daemon restart",
"restart",
])
.arg(&project)
.arg(&daemon.codex)
.current_dir(&project)
.env("CODEX_HOME", daemon.home.path())
.output()?;
ensure!(
restarted.status.success(),
"{}",
String::from_utf8_lossy(&restarted.stderr)
);
let restarted: Value = serde_json::from_slice(&restarted.stdout)?;
assert_eq!(restarted["status"], "restarted");
assert_ne!(daemon.pid("app-server.pid")?, old_pid);
assert_ne!(daemon.pid("app-server-updater.pid")?, old_updater_pid);
assert_eq!(daemon.lifecycle("version")?["status"], "running");
let updater_socket = daemon
.home
.path()
.join("app-server-daemon/app-server-updater.sock");
let deadline = Instant::now() + Duration::from_secs(30);
while std::os::unix::net::UnixStream::connect(&updater_socket).is_err() {
ensure!(
Instant::now() < deadline,
"replacement updater did not become ready"
);
std::thread::sleep(Duration::from_millis(50));
}
Ok(())
}
fn signal(pid: u32, signal: libc::c_int) -> Result<()> {
let raw_pid = libc::pid_t::try_from(pid).context("pid out of range")?;
ensure!(