mirror of
https://github.com/openai/codex.git
synced 2026-11-03 19:53:25 +00:00
## Description
This PR makes `codex remote-control` behave like a foreground CLI
command by default. Running it now starts remote control, waits for
readiness, prints a clear status message with the machine name, and
stays alive until Ctrl-C.
Users who want daemon behavior can use `codex remote-control start`, and
`codex remote-control stop` now prints concise human-readable output.
`--json` remains available for scripts.
Implementation-wise, this now verifies the real app-server state instead
of just assuming startup worked. The CLI starts or connects to
app-server, probes its control socket, calls the `remoteControl/enable`
API, and waits for the remote-control status response/notification
before printing success.
For daemon mode, `codex remote-control start` also reports which managed
app-server binary was used, including its path and best-effort `codex
--version`, so failures are easier to diagnose.
## Examples
Example output:
```
> codex remote-control
Starting app-server with remote control enabled...
This machine is available for remote control as com-97826.
Press Ctrl-C to stop.
```
Error case using daemon (currently expected based on our publicly
released CLI version):
```
> ./target/debug/codex remote-control start
Starting app-server daemon with remote control enabled...
Error: app server did not become ready on /Users/owen/.codex/app-server-control/app-server-control.sock
Daemon used app-server:
path: /Users/owen/.codex/packages/standalone/current/codex
version: 0.130.0
Managed app-server stderr (/Users/owen/.codex/app-server-daemon/app-server.stderr.log):
error: unexpected argument '--remote-control' found
Usage: codex app-server [OPTIONS] [COMMAND]
For more information, try '--help'.
Caused by:
0: failed to connect to /Users/owen/.codex/app-server-control/app-server-control.sock
1: No such file or directory (os error 2)
```
## What changed
- `codex remote-control` now runs remote control in the foreground and
prints a Ctrl-C stop hint.
- `codex remote-control start` starts the daemon and waits for remote
control readiness before reporting success.
- `codex remote-control stop` reports stopped/not-running status in
plain language.
- Startup failures now include recent managed app-server stderr to make
daemon issues easier to diagnose.
- Added coverage for CLI output, readiness waiting, foreground shutdown,
and stderr log tailing.
197 lines
5.5 KiB
Rust
197 lines
5.5 KiB
Rust
use std::time::Duration;
|
|
|
|
use pretty_assertions::assert_eq;
|
|
use tempfile::TempDir;
|
|
|
|
use super::PidBackend;
|
|
use super::PidCommandKind;
|
|
use super::PidFileState;
|
|
use super::PidLogTail;
|
|
use super::PidRecord;
|
|
use super::read_stderr_log_tail;
|
|
use super::stderr_log_file_for_pid_file;
|
|
use super::try_lock_file;
|
|
|
|
#[tokio::test]
|
|
async fn locked_empty_pid_file_is_treated_as_active_reservation() {
|
|
let temp_dir = TempDir::new().expect("temp dir");
|
|
let pid_file = temp_dir.path().join("app-server.pid");
|
|
tokio::fs::write(&pid_file, "")
|
|
.await
|
|
.expect("write pid file");
|
|
let backend = PidBackend::new(
|
|
temp_dir.path().join("codex"),
|
|
pid_file.clone(),
|
|
/*remote_control_enabled*/ false,
|
|
);
|
|
let reservation = tokio::fs::OpenOptions::new()
|
|
.create(true)
|
|
.truncate(false)
|
|
.write(true)
|
|
.open(&backend.lock_file)
|
|
.await
|
|
.expect("open pid lock file");
|
|
assert!(try_lock_file(&reservation).expect("lock reservation"));
|
|
|
|
assert_eq!(
|
|
backend.read_pid_file_state().await.expect("read pid"),
|
|
PidFileState::Starting
|
|
);
|
|
assert!(pid_file.exists());
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn unlocked_empty_pid_file_is_treated_as_stale_reservation() {
|
|
let temp_dir = TempDir::new().expect("temp dir");
|
|
let pid_file = temp_dir.path().join("app-server.pid");
|
|
tokio::fs::write(&pid_file, "")
|
|
.await
|
|
.expect("write pid file");
|
|
let backend = PidBackend::new(
|
|
temp_dir.path().join("codex"),
|
|
pid_file.clone(),
|
|
/*remote_control_enabled*/ false,
|
|
);
|
|
|
|
assert_eq!(
|
|
backend.read_pid_file_state().await.expect("read pid"),
|
|
PidFileState::Missing
|
|
);
|
|
assert!(!pid_file.exists());
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn stop_waits_for_live_reservation_to_resolve() {
|
|
let temp_dir = TempDir::new().expect("temp dir");
|
|
let pid_file = temp_dir.path().join("app-server.pid");
|
|
tokio::fs::write(&pid_file, "")
|
|
.await
|
|
.expect("write pid file");
|
|
let backend = PidBackend::new(
|
|
temp_dir.path().join("codex"),
|
|
pid_file.clone(),
|
|
/*remote_control_enabled*/ false,
|
|
);
|
|
let reservation = tokio::fs::OpenOptions::new()
|
|
.create(true)
|
|
.truncate(false)
|
|
.write(true)
|
|
.open(&backend.lock_file)
|
|
.await
|
|
.expect("open pid lock file");
|
|
assert!(try_lock_file(&reservation).expect("lock reservation"));
|
|
let cleanup = tokio::spawn(async move {
|
|
tokio::time::sleep(Duration::from_millis(50)).await;
|
|
drop(reservation);
|
|
tokio::fs::remove_file(pid_file)
|
|
.await
|
|
.expect("remove pid file");
|
|
});
|
|
|
|
backend.stop().await.expect("stop");
|
|
cleanup.await.expect("cleanup task");
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn start_retries_stale_empty_pid_file_under_its_own_lock() {
|
|
let temp_dir = TempDir::new().expect("temp dir");
|
|
let pid_file = temp_dir.path().join("app-server.pid");
|
|
tokio::fs::write(&pid_file, "")
|
|
.await
|
|
.expect("write pid file");
|
|
let backend = PidBackend::new(
|
|
temp_dir.path().join("missing-codex"),
|
|
pid_file,
|
|
/*remote_control_enabled*/ false,
|
|
);
|
|
|
|
let err = backend.start().await.expect_err("start");
|
|
assert!(
|
|
err.to_string()
|
|
.starts_with("failed to spawn detached app-server process using ")
|
|
);
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn stale_record_cleanup_preserves_replacement_record() {
|
|
let temp_dir = TempDir::new().expect("temp dir");
|
|
let pid_file = temp_dir.path().join("app-server.pid");
|
|
let backend = PidBackend::new(
|
|
temp_dir.path().join("codex"),
|
|
pid_file.clone(),
|
|
/*remote_control_enabled*/ false,
|
|
);
|
|
let stale = PidRecord {
|
|
pid: 1,
|
|
process_start_time: "old".to_string(),
|
|
};
|
|
let replacement = PidRecord {
|
|
pid: 2,
|
|
process_start_time: "new".to_string(),
|
|
};
|
|
tokio::fs::write(
|
|
&pid_file,
|
|
serde_json::to_vec(&replacement).expect("serialize replacement"),
|
|
)
|
|
.await
|
|
.expect("write replacement pid file");
|
|
|
|
assert_eq!(
|
|
backend
|
|
.refresh_after_stale_record(&stale)
|
|
.await
|
|
.expect("cleanup"),
|
|
PidFileState::Running(replacement)
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn update_loop_uses_hidden_app_server_subcommand() {
|
|
let backend = PidBackend {
|
|
codex_bin: "codex".into(),
|
|
pid_file: "updater.pid".into(),
|
|
lock_file: "updater.pid.lock".into(),
|
|
command_kind: PidCommandKind::UpdateLoop,
|
|
};
|
|
|
|
assert_eq!(
|
|
backend.command_args(),
|
|
vec!["app-server", "daemon", "pid-update-loop"]
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn app_server_remote_control_uses_runtime_flag() {
|
|
let backend = PidBackend::new(
|
|
"codex".into(),
|
|
"app-server.pid".into(),
|
|
/*remote_control_enabled*/ true,
|
|
);
|
|
|
|
assert_eq!(
|
|
backend.command_args(),
|
|
vec!["app-server", "--remote-control", "--listen", "unix://"]
|
|
);
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn read_stderr_log_tail_returns_recent_complete_lines() {
|
|
let temp_dir = TempDir::new().expect("temp dir");
|
|
let pid_file = temp_dir.path().join("app-server.pid");
|
|
let log_file = stderr_log_file_for_pid_file(&pid_file);
|
|
let contents = format!("{}\nrecent error\nusage", "x".repeat(4100));
|
|
tokio::fs::write(&log_file, contents)
|
|
.await
|
|
.expect("write stderr log");
|
|
|
|
assert_eq!(
|
|
read_stderr_log_tail(&pid_file)
|
|
.await
|
|
.expect("read stderr log"),
|
|
Some(PidLogTail {
|
|
path: log_file,
|
|
contents: "recent error\nusage".to_string(),
|
|
})
|
|
);
|
|
}
|