Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 13 additions & 4 deletions codex-rs/app-server-daemon/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ support Windows lifecycle management.

```sh
codex app-server daemon start
codex app-server daemon start --codex-bin /absolute/path/to/codex --analytics-default-enabled
codex app-server daemon restart
codex app-server daemon enable-remote-control
codex app-server daemon disable-remote-control
Expand All @@ -28,8 +29,9 @@ codex app-server daemon bootstrap --remote-control

On success, every command writes exactly one JSON object to stdout. Consumers
should parse that JSON rather than relying on human-readable text. Lifecycle
responses report the resolved backend, socket path, local CLI version, and
running app-server version when applicable.
responses report the resolved backend, socket path, local CLI version, running
app-server version when applicable, and persisted launch settings such as
`managedCodexPath` and `analyticsDefaultEnabled`.

## Bootstrap flow

Expand All @@ -44,15 +46,22 @@ $HOME/.codex/packages/standalone/current/codex app-server daemon bootstrap --rem
settings under `CODEX_HOME/app-server-daemon/`, starts app-server as a
pidfile-backed detached process, and launches a detached updater loop.

`start` and `restart` can persist an explicit `--codex-bin` for clients that
own the executable lifecycle, such as a desktop app bundle. The path must be
absolute and point to an existing file. `--analytics-default-enabled` persists
the matching app-server launch flag for first-party clients. Those overrides do
not enable the standalone updater loop.

## Installation and update cases

The daemon assumes Codex is installed through `install.sh` and always launches
the standalone managed binary under `CODEX_HOME`.
Without launch overrides, the daemon assumes Codex is installed through
`install.sh` and launches the standalone managed binary under `CODEX_HOME`.

| Situation | What starts | Does this daemon fetch new binaries? | Does a running app-server eventually move to a newer binary on its own? |
| --- | --- | --- | --- |
| `install.sh` has run, but only `start` is used | `start` uses `CODEX_HOME/packages/standalone/current/codex` | No | No. The managed path is used when starting or restarting, but no updater is installed. |
| `install.sh` has run, then `bootstrap` is used | The pidfile backend uses `CODEX_HOME/packages/standalone/current/codex` | Yes. Bootstrap launches a detached updater loop that runs `install.sh` hourly. | Yes, while that updater process is alive and app-server is already running. After a successful fetch, the updater restarts app-server with the refreshed binary and only then replaces its own process image. |
| `start --codex-bin /absolute/path/to/codex` is used | `start` and future lifecycle operations use the explicit executable path | No | No. The owner of that executable decides when to restart the daemon with a replacement binary. |
| Some other tool updates the managed binary path | The next fresh start or restart uses the updated file at that path | Only if `bootstrap` is active, because the updater still runs `install.sh` on its normal cadence. | Without `bootstrap`, no. With `bootstrap`, the next successful updater pass compares the managed binary contents after `install.sh` runs; if app-server is running and they differ from the updater's current image, it refreshes app-server first and then itself. |

### Standalone installs
Expand Down
2 changes: 2 additions & 0 deletions codex-rs/app-server-daemon/src/backend/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,13 +19,15 @@ pub(crate) struct BackendPaths {
pub(crate) pid_file: PathBuf,
pub(crate) update_pid_file: PathBuf,
pub(crate) remote_control_enabled: bool,
pub(crate) analytics_default_enabled: bool,
}

pub(crate) fn pid_backend(paths: BackendPaths) -> PidBackend {
PidBackend::new(
paths.codex_bin,
paths.pid_file,
paths.remote_control_enabled,
paths.analytics_default_enabled,
)
}

Expand Down
33 changes: 26 additions & 7 deletions codex-rs/app-server-daemon/src/backend/pid.rs
Original file line number Diff line number Diff line change
Expand Up @@ -70,19 +70,28 @@ enum PidFileState {
#[derive(Debug, Clone, Copy)]
#[cfg_attr(not(unix), allow(dead_code))]
enum PidCommandKind {
AppServer { remote_control_enabled: bool },
AppServer {
remote_control_enabled: bool,
analytics_default_enabled: bool,
},
UpdateLoop,
}

impl PidBackend {
pub(crate) fn new(codex_bin: PathBuf, pid_file: PathBuf, remote_control_enabled: bool) -> Self {
pub(crate) fn new(
codex_bin: PathBuf,
pid_file: PathBuf,
remote_control_enabled: bool,
analytics_default_enabled: bool,
) -> Self {
let lock_file = pid_file.with_extension("pid.lock");
Self {
codex_bin,
pid_file,
lock_file,
command_kind: PidCommandKind::AppServer {
remote_control_enabled,
analytics_default_enabled,
},
}
}
Expand Down Expand Up @@ -403,11 +412,19 @@ impl PidBackend {
fn command_args(&self) -> Vec<&'static str> {
match self.command_kind {
PidCommandKind::AppServer {
remote_control_enabled: true,
} => vec!["app-server", "--remote-control", "--listen", "unix://"],
PidCommandKind::AppServer {
remote_control_enabled: false,
} => vec!["app-server", "--listen", "unix://"],
remote_control_enabled,
analytics_default_enabled,
} => {
let mut args = vec!["app-server"];
if remote_control_enabled {
args.push("--remote-control");
}
if analytics_default_enabled {
args.push("--analytics-default-enabled");
}
args.extend(["--listen", "unix://"]);
args
}
PidCommandKind::UpdateLoop => vec!["app-server", "daemon", "pid-update-loop"],
}
}
Expand All @@ -417,9 +434,11 @@ impl PidBackend {
match self.command_kind {
PidCommandKind::AppServer {
remote_control_enabled: false,
..
} => Some((REMOTE_CONTROL_DISABLED_ENV_VAR, "1")),
PidCommandKind::AppServer {
remote_control_enabled: true,
..
}
| PidCommandKind::UpdateLoop => None,
}
Expand Down
27 changes: 27 additions & 0 deletions codex-rs/app-server-daemon/src/backend/pid_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ async fn locked_empty_pid_file_is_treated_as_active_reservation() {
temp_dir.path().join("codex"),
pid_file.clone(),
/*remote_control_enabled*/ false,
/*analytics_default_enabled*/ false,
);
let reservation = tokio::fs::OpenOptions::new()
.create(true)
Expand Down Expand Up @@ -53,6 +54,7 @@ async fn unlocked_empty_pid_file_is_treated_as_stale_reservation() {
temp_dir.path().join("codex"),
pid_file.clone(),
/*remote_control_enabled*/ false,
/*analytics_default_enabled*/ false,
);

assert_eq!(
Expand All @@ -73,6 +75,7 @@ async fn stop_waits_for_live_reservation_to_resolve() {
temp_dir.path().join("codex"),
pid_file.clone(),
/*remote_control_enabled*/ false,
/*analytics_default_enabled*/ false,
);
let reservation = tokio::fs::OpenOptions::new()
.create(true)
Expand Down Expand Up @@ -105,6 +108,7 @@ async fn start_retries_stale_empty_pid_file_under_its_own_lock() {
temp_dir.path().join("missing-codex"),
pid_file,
/*remote_control_enabled*/ false,
/*analytics_default_enabled*/ false,
);

let err = backend.start().await.expect_err("start");
Expand All @@ -122,6 +126,7 @@ async fn stale_record_cleanup_preserves_replacement_record() {
temp_dir.path().join("codex"),
pid_file.clone(),
/*remote_control_enabled*/ false,
/*analytics_default_enabled*/ false,
);
let stale = PidRecord {
pid: 1,
Expand Down Expand Up @@ -168,6 +173,7 @@ fn app_server_remote_control_uses_runtime_flag() {
"codex".into(),
"app-server.pid".into(),
/*remote_control_enabled*/ true,
/*analytics_default_enabled*/ false,
);

assert_eq!(
Expand All @@ -182,6 +188,7 @@ fn app_server_disabled_remote_control_uses_compatible_args_and_runtime_env() {
"codex".into(),
"app-server.pid".into(),
/*remote_control_enabled*/ false,
/*analytics_default_enabled*/ false,
);

assert_eq!(
Expand All @@ -194,6 +201,26 @@ fn app_server_disabled_remote_control_uses_compatible_args_and_runtime_env() {
);
}

#[test]
fn app_server_analytics_default_uses_runtime_flag() {
let backend = PidBackend::new(
"codex".into(),
"app-server.pid".into(),
/*remote_control_enabled*/ false,
/*analytics_default_enabled*/ true,
);

assert_eq!(
backend.command_args(),
vec![
"app-server",
"--analytics-default-enabled",
"--listen",
"unix://"
]
);
}

#[tokio::test]
async fn read_stderr_log_tail_returns_recent_complete_lines() {
let temp_dir = TempDir::new().expect("temp dir");
Expand Down
Loading
Loading