Download codex-rs/cli/src/remote_control_cmd.rs from SaylorTwift/codex: direct link, hf CLI and curl.
- Browser
- Download file 27.3 kB
-
https://huggingface.co/SaylorTwift/codex/resolve/main/codex-rs/cli/src/remote_control_cmd.rs
- Command line
-
hf download hf://SaylorTwift/codex/codex-rs/cli/src/remote_control_cmd.rs
-
curl -L -o remote_control_cmd.rs https://huggingface.co/SaylorTwift/codex/resolve/main/codex-rs/cli/src/remote_control_cmd.rs
27.3 kB
| use std::io::Write; | |
| use std::time::Duration; | |
| use anyhow::Context; | |
| use clap::Args; | |
| use codex_app_server::AppServerRuntimeOptions; | |
| use codex_app_server::AppServerTransport; | |
| use codex_app_server::AppServerWebsocketAuthSettings; | |
| use codex_app_server_daemon::LifecycleCommand as AppServerLifecycleCommand; | |
| use codex_app_server_daemon::LifecycleOutput as AppServerLifecycleOutput; | |
| use codex_app_server_daemon::LifecycleStatus as AppServerLifecycleStatus; | |
| use codex_app_server_daemon::RemoteControlReadyOutput as AppServerRemoteControlReadyOutput; | |
| use codex_app_server_daemon::RemoteControlReadyStatus as AppServerRemoteControlReadyStatus; | |
| use codex_app_server_daemon::RemoteControlStartOutput as AppServerRemoteControlStartOutput; | |
| use codex_app_server_protocol::RemoteControlConnectionStatus; | |
| use codex_app_server_protocol::RemoteControlPairingStartResponse; | |
| use codex_arg0::Arg0DispatchPaths; | |
| use codex_config::LoaderOverrides; | |
| use codex_protocol::protocol::SessionSource; | |
| use codex_utils_absolute_path::AbsolutePathBuf; | |
| use codex_utils_cli::CliConfigOverrides; | |
| use serde::Serialize; | |
| use tokio::sync::watch; | |
| use tokio::task::JoinHandle; | |
| use tokio::time::timeout; | |
| const FOREGROUND_SOCKET_CONNECT_TIMEOUT: Duration = Duration::from_secs(10); | |
| const FOREGROUND_SOCKET_CONNECT_RETRY_DELAY: Duration = Duration::from_millis(50); | |
| const FOREGROUND_APP_SERVER_ABORT_TIMEOUT: Duration = Duration::from_secs(1); | |
| pub(crate) struct RemoteControlCommand { | |
| /// Emit machine-readable JSON. | |
| json: bool, | |
| subcommand: Option<RemoteControlSubcommand>, | |
| } | |
| impl RemoteControlCommand { | |
| pub(crate) fn subcommand_name(&self) -> &'static str { | |
| match self.subcommand { | |
| None => "remote-control", | |
| Some(RemoteControlSubcommand::Start) => "remote-control start", | |
| Some(RemoteControlSubcommand::Stop) => "remote-control stop", | |
| Some(RemoteControlSubcommand::Pair) => "remote-control pair", | |
| } | |
| } | |
| } | |
| enum RemoteControlSubcommand { | |
| /// Start the app-server daemon with remote control enabled. | |
| Start, | |
| /// Stop the app-server daemon. | |
| Stop, | |
| /// Create and print a short-lived manual pairing code. | |
| Pair, | |
| } | |
| pub(crate) async fn run( | |
| command: RemoteControlCommand, | |
| arg0_paths: Arg0DispatchPaths, | |
| root_config_overrides: CliConfigOverrides, | |
| ) -> anyhow::Result<()> { | |
| match command.subcommand { | |
| None => { | |
| print_remote_control_progress( | |
| command.json, | |
| "Starting app-server with remote control enabled...", | |
| )?; | |
| run_foreground_remote_control(command.json, arg0_paths, root_config_overrides).await?; | |
| } | |
| Some(RemoteControlSubcommand::Start) => { | |
| print_remote_control_progress( | |
| command.json, | |
| "Starting app-server daemon with remote control enabled...", | |
| )?; | |
| let output = codex_app_server_daemon::ensure_remote_control_ready().await?; | |
| print_remote_control_start_output(&output, command.json)?; | |
| } | |
| Some(RemoteControlSubcommand::Stop) => { | |
| print_remote_control_progress(command.json, "Stopping remote control...")?; | |
| let output = codex_app_server_daemon::run(AppServerLifecycleCommand::Stop).await?; | |
| print_remote_control_stop_output(&output, command.json)?; | |
| } | |
| Some(RemoteControlSubcommand::Pair) => { | |
| let output = codex_app_server_daemon::start_remote_control_pairing().await?; | |
| print_remote_control_pairing_output(&output, command.json)?; | |
| } | |
| } | |
| Ok(()) | |
| } | |
| fn print_remote_control_progress(json: bool, message: &str) -> anyhow::Result<()> { | |
| if json { | |
| return Ok(()); | |
| } | |
| println!("{message}"); | |
| std::io::stdout() | |
| .flush() | |
| .context("failed to flush remote-control progress message")?; | |
| Ok(()) | |
| } | |
| async fn run_foreground_remote_control( | |
| json: bool, | |
| arg0_paths: Arg0DispatchPaths, | |
| root_config_overrides: CliConfigOverrides, | |
| ) -> anyhow::Result<()> { | |
| let socket_dir = tempfile::Builder::new() | |
| .prefix("codex-rc-") | |
| .tempdir_in("/tmp") | |
| .or_else(|_| tempfile::tempdir()) | |
| .context("failed to create private app-server socket directory")?; | |
| let socket_path = socket_dir.path().join("rc.sock"); | |
| let socket_path = AbsolutePathBuf::from_absolute_path(&socket_path) | |
| .context("private app-server socket path was not absolute")?; | |
| let transport = AppServerTransport::UnixSocket { | |
| socket_path: socket_path.clone(), | |
| }; | |
| let runtime_options = AppServerRuntimeOptions { | |
| remote_control_startup_mode: codex_app_server::RemoteControlStartupMode::EnabledEphemeral, | |
| install_shutdown_signal_handler: false, | |
| ..Default::default() | |
| }; | |
| let (stop_rx, stop_signal_task) = foreground_stop_signal(); | |
| let app_server = codex_app_server::run_main_with_transport_options( | |
| arg0_paths, | |
| root_config_overrides, | |
| LoaderOverrides::default(), | |
| /*strict_config*/ false, | |
| /*default_analytics_enabled*/ false, | |
| transport, | |
| SessionSource::VSCode, | |
| AppServerWebsocketAuthSettings::default(), | |
| runtime_options, | |
| ); | |
| let mut app_server_task = tokio::spawn(async move { app_server.await.map(|_| ()) }); | |
| let summary = match wait_for_foreground_remote_control_start( | |
| &mut app_server_task, | |
| wait_for_foreground_remote_control_ready(socket_path), | |
| stop_rx.clone(), | |
| ) | |
| .await | |
| { | |
| ForegroundStartupResult::Ready(summary) => summary, | |
| ForegroundStartupResult::Stopped => { | |
| abort_foreground_app_server(app_server_task).await; | |
| stop_signal_task.abort(); | |
| return Ok(()); | |
| } | |
| ForegroundStartupResult::ReadyFailed(error) => { | |
| abort_foreground_app_server(app_server_task).await; | |
| stop_signal_task.abort(); | |
| return Err(error); | |
| } | |
| ForegroundStartupResult::AppServerExited(error) => { | |
| stop_signal_task.abort(); | |
| return Err(error); | |
| } | |
| }; | |
| if *stop_rx.borrow() { | |
| abort_foreground_app_server(app_server_task).await; | |
| stop_signal_task.abort(); | |
| return Ok(()); | |
| } | |
| if let Err(error) = print_foreground_ready_output(&summary, json) { | |
| abort_foreground_app_server(app_server_task).await; | |
| stop_signal_task.abort(); | |
| return Err(error); | |
| } | |
| let result = wait_for_foreground_app_server(app_server_task, stop_rx).await; | |
| stop_signal_task.abort(); | |
| result | |
| } | |
| fn foreground_stop_signal() -> (watch::Receiver<bool>, JoinHandle<()>) { | |
| let (stop_tx, stop_rx) = watch::channel(false); | |
| let task = tokio::spawn(async move { | |
| if let Err(err) = tokio::signal::ctrl_c().await { | |
| eprintln!("failed to listen for Ctrl-C: {err}"); | |
| } | |
| let _ = stop_tx.send(true); | |
| }); | |
| (stop_rx, task) | |
| } | |
| enum ForegroundStartupResult { | |
| Ready(AppServerRemoteControlReadyStatus), | |
| Stopped, | |
| ReadyFailed(anyhow::Error), | |
| AppServerExited(anyhow::Error), | |
| } | |
| async fn wait_for_foreground_remote_control_start( | |
| app_server_task: &mut JoinHandle<std::io::Result<()>>, | |
| ready: impl std::future::Future<Output = anyhow::Result<AppServerRemoteControlReadyStatus>>, | |
| mut stop_rx: watch::Receiver<bool>, | |
| ) -> ForegroundStartupResult { | |
| tokio::pin!(ready); | |
| tokio::select! { | |
| ready_result = &mut ready => match ready_result { | |
| Ok(summary) => ForegroundStartupResult::Ready(summary), | |
| Err(error) => ForegroundStartupResult::ReadyFailed(error), | |
| }, | |
| app_server_result = app_server_task => { | |
| ForegroundStartupResult::AppServerExited( | |
| foreground_app_server_exited_before_ready(app_server_result) | |
| ) | |
| } | |
| _ = wait_for_stop_signal(&mut stop_rx) => ForegroundStartupResult::Stopped, | |
| } | |
| } | |
| async fn wait_for_foreground_app_server( | |
| mut app_server_task: JoinHandle<std::io::Result<()>>, | |
| mut stop_rx: watch::Receiver<bool>, | |
| ) -> anyhow::Result<()> { | |
| tokio::select! { | |
| app_server_result = &mut app_server_task => { | |
| app_server_result | |
| .context("foreground app-server task failed to join")? | |
| .context("foreground app-server exited with an error")?; | |
| } | |
| _ = wait_for_stop_signal(&mut stop_rx) => { | |
| abort_foreground_app_server(app_server_task).await; | |
| } | |
| } | |
| Ok(()) | |
| } | |
| async fn wait_for_stop_signal(stop_rx: &mut watch::Receiver<bool>) { | |
| if *stop_rx.borrow() { | |
| return; | |
| } | |
| let _ = stop_rx.wait_for(|stopped| *stopped).await; | |
| } | |
| fn foreground_app_server_exited_before_ready( | |
| result: Result<std::io::Result<()>, tokio::task::JoinError>, | |
| ) -> anyhow::Error { | |
| match result { | |
| Ok(Ok(())) => { | |
| anyhow::anyhow!("foreground app-server exited before remote control became ready") | |
| } | |
| Ok(Err(error)) => anyhow::Error::new(error) | |
| .context("foreground app-server exited before remote control became ready"), | |
| Err(error) => anyhow::Error::new(error) | |
| .context("foreground app-server task failed before remote control became ready"), | |
| } | |
| } | |
| async fn abort_foreground_app_server(app_server_task: JoinHandle<std::io::Result<()>>) { | |
| app_server_task.abort(); | |
| let _ = timeout(FOREGROUND_APP_SERVER_ABORT_TIMEOUT, app_server_task).await; | |
| } | |
| async fn wait_for_foreground_remote_control_ready( | |
| socket_path: AbsolutePathBuf, | |
| ) -> anyhow::Result<AppServerRemoteControlReadyStatus> { | |
| codex_app_server_daemon::enable_remote_control_on_socket( | |
| socket_path.as_path(), | |
| FOREGROUND_SOCKET_CONNECT_TIMEOUT, | |
| FOREGROUND_SOCKET_CONNECT_RETRY_DELAY, | |
| ) | |
| .await | |
| } | |
| fn print_remote_control_start_output( | |
| output: &AppServerRemoteControlReadyOutput, | |
| json: bool, | |
| ) -> anyhow::Result<()> { | |
| ensure_remote_control_startable(&output.remote_control)?; | |
| if json { | |
| println!( | |
| "{}", | |
| serde_json::to_string(&RemoteControlStartJsonOutput::daemon(output))? | |
| ); | |
| return Ok(()); | |
| } | |
| for line in remote_control_start_human_lines( | |
| &output.remote_control, | |
| RemoteControlHumanOutputMode::Daemon, | |
| )? { | |
| println!("{line}"); | |
| } | |
| for line in daemon_app_server_human_lines(&output.daemon) { | |
| println!("{line}"); | |
| } | |
| Ok(()) | |
| } | |
| fn print_foreground_ready_output( | |
| summary: &AppServerRemoteControlReadyStatus, | |
| json: bool, | |
| ) -> anyhow::Result<()> { | |
| if json { | |
| ensure_remote_control_startable(summary)?; | |
| println!( | |
| "{}", | |
| serde_json::to_string(&RemoteControlStartJsonOutput::foreground(summary))? | |
| ); | |
| return Ok(()); | |
| } | |
| for line in remote_control_start_human_lines(summary, RemoteControlHumanOutputMode::Foreground)? | |
| { | |
| println!("{line}"); | |
| } | |
| Ok(()) | |
| } | |
| struct RemoteControlStartJsonOutput<'a> { | |
| mode: RemoteControlModeJson, | |
| status: RemoteControlConnectionStatus, | |
| server_name: &'a str, | |
| environment_id: Option<&'a str>, | |
| timed_out: bool, | |
| daemon: Option<&'a AppServerRemoteControlStartOutput>, | |
| } | |
| enum RemoteControlModeJson { | |
| Foreground, | |
| Daemon, | |
| } | |
| impl<'a> RemoteControlStartJsonOutput<'a> { | |
| fn foreground(summary: &'a AppServerRemoteControlReadyStatus) -> Self { | |
| Self { | |
| mode: RemoteControlModeJson::Foreground, | |
| status: summary.status, | |
| server_name: &summary.server_name, | |
| environment_id: summary.environment_id.as_deref(), | |
| timed_out: summary.timed_out, | |
| daemon: None, | |
| } | |
| } | |
| fn daemon(output: &'a AppServerRemoteControlReadyOutput) -> Self { | |
| let remote_control = &output.remote_control; | |
| Self { | |
| mode: RemoteControlModeJson::Daemon, | |
| status: remote_control.status, | |
| server_name: &remote_control.server_name, | |
| environment_id: remote_control.environment_id.as_deref(), | |
| timed_out: remote_control.timed_out, | |
| daemon: Some(&output.daemon), | |
| } | |
| } | |
| } | |
| fn remote_control_start_human_message( | |
| output: &AppServerRemoteControlReadyStatus, | |
| ) -> anyhow::Result<String> { | |
| ensure_remote_control_startable(output)?; | |
| match output.status { | |
| RemoteControlConnectionStatus::Connected => Ok(format!( | |
| "This machine is available for remote control as {}.", | |
| output.server_name | |
| )), | |
| RemoteControlConnectionStatus::Connecting => Ok(format!( | |
| "Remote control is enabled on {} and still connecting.", | |
| output.server_name | |
| )), | |
| RemoteControlConnectionStatus::Errored | RemoteControlConnectionStatus::Disabled => { | |
| unreachable!("errored and disabled statuses are rejected before formatting") | |
| } | |
| } | |
| } | |
| fn ensure_remote_control_startable( | |
| output: &AppServerRemoteControlReadyStatus, | |
| ) -> anyhow::Result<()> { | |
| match output.status { | |
| RemoteControlConnectionStatus::Connected | RemoteControlConnectionStatus::Connecting => { | |
| Ok(()) | |
| } | |
| RemoteControlConnectionStatus::Errored => { | |
| anyhow::bail!( | |
| "Remote control is enabled on {} but the connection is errored.", | |
| output.server_name | |
| ); | |
| } | |
| RemoteControlConnectionStatus::Disabled => { | |
| anyhow::bail!("Remote control is disabled on {}.", output.server_name); | |
| } | |
| } | |
| } | |
| enum RemoteControlHumanOutputMode { | |
| Foreground, | |
| Daemon, | |
| } | |
| fn remote_control_start_human_lines( | |
| summary: &AppServerRemoteControlReadyStatus, | |
| mode: RemoteControlHumanOutputMode, | |
| ) -> anyhow::Result<Vec<String>> { | |
| let mut lines = vec![remote_control_start_human_message(summary)?]; | |
| match mode { | |
| RemoteControlHumanOutputMode::Foreground => { | |
| lines.push("Press Ctrl-C to stop.".to_string()); | |
| } | |
| RemoteControlHumanOutputMode::Daemon => {} | |
| } | |
| Ok(lines) | |
| } | |
| fn daemon_app_server_human_lines(output: &AppServerRemoteControlStartOutput) -> Vec<String> { | |
| let (managed_codex_path, managed_codex_version) = daemon_app_server_identity(output); | |
| vec![ | |
| "Daemon used app-server:".to_string(), | |
| format!(" path: {}", managed_codex_path.display()), | |
| format!(" version: {}", managed_codex_version.unwrap_or("unknown")), | |
| ] | |
| } | |
| fn daemon_app_server_identity( | |
| output: &AppServerRemoteControlStartOutput, | |
| ) -> (&std::path::Path, Option<&str>) { | |
| match output { | |
| AppServerRemoteControlStartOutput::Bootstrap(output) => ( | |
| &output.managed_codex_path, | |
| output.managed_codex_version.as_deref(), | |
| ), | |
| AppServerRemoteControlStartOutput::Start(output) => ( | |
| &output.managed_codex_path, | |
| output.managed_codex_version.as_deref(), | |
| ), | |
| } | |
| } | |
| fn print_remote_control_stop_output( | |
| output: &AppServerLifecycleOutput, | |
| json: bool, | |
| ) -> anyhow::Result<()> { | |
| if json { | |
| println!("{}", serde_json::to_string(output)?); | |
| return Ok(()); | |
| } | |
| println!("{}", remote_control_stop_human_message(output)); | |
| Ok(()) | |
| } | |
| fn print_remote_control_pairing_output( | |
| output: &RemoteControlPairingStartResponse, | |
| json: bool, | |
| ) -> anyhow::Result<()> { | |
| println!("{}", format_remote_control_pairing_output(output, json)?); | |
| Ok(()) | |
| } | |
| fn format_remote_control_pairing_output( | |
| output: &RemoteControlPairingStartResponse, | |
| json: bool, | |
| ) -> anyhow::Result<String> { | |
| if json { | |
| return Ok(serde_json::to_string(output)?); | |
| } | |
| let manual_pairing_code = output | |
| .manual_pairing_code | |
| .as_deref() | |
| .context("remote-control pairing response did not include a manual pairing code")?; | |
| Ok(format!("Pairing code: {manual_pairing_code}")) | |
| } | |
| fn remote_control_stop_human_message(output: &AppServerLifecycleOutput) -> String { | |
| match output.status { | |
| AppServerLifecycleStatus::Stopped => "Remote control stopped.".to_string(), | |
| AppServerLifecycleStatus::NotRunning => "Remote control is not running.".to_string(), | |
| AppServerLifecycleStatus::Started | |
| | AppServerLifecycleStatus::Restarted | |
| | AppServerLifecycleStatus::AlreadyRunning | |
| | AppServerLifecycleStatus::Running => { | |
| format!( | |
| "Remote control stop completed with status {:?}.", | |
| output.status | |
| ) | |
| } | |
| } | |
| } | |
| mod tests { | |
| use pretty_assertions::assert_eq; | |
| use serde_json::json; | |
| use std::path::PathBuf; | |
| use super::*; | |
| fn remote_control_status( | |
| status: RemoteControlConnectionStatus, | |
| ) -> AppServerRemoteControlReadyStatus { | |
| AppServerRemoteControlReadyStatus { | |
| status, | |
| server_name: "owen-mbp".to_string(), | |
| environment_id: Some("env_test".to_string()), | |
| timed_out: status == RemoteControlConnectionStatus::Connecting, | |
| } | |
| } | |
| fn daemon_ready_output( | |
| status: RemoteControlConnectionStatus, | |
| ) -> AppServerRemoteControlReadyOutput { | |
| AppServerRemoteControlReadyOutput { | |
| daemon: AppServerRemoteControlStartOutput::Start(AppServerLifecycleOutput { | |
| status: AppServerLifecycleStatus::Started, | |
| backend: None, | |
| pid: Some(42), | |
| managed_codex_path: PathBuf::from("/opt/codex/bin/codex"), | |
| managed_codex_version: Some("1.0.0".to_string()), | |
| socket_path: PathBuf::from("/tmp/app-server-control.sock"), | |
| cli_version: Some("1.0.0".to_string()), | |
| app_server_version: Some("2.0.0".to_string()), | |
| }), | |
| remote_control: AppServerRemoteControlReadyStatus { | |
| status, | |
| server_name: "owen-mbp".to_string(), | |
| environment_id: Some("env_test".to_string()), | |
| timed_out: status == RemoteControlConnectionStatus::Connecting, | |
| }, | |
| } | |
| } | |
| fn pairing_response(manual_pairing_code: Option<&str>) -> RemoteControlPairingStartResponse { | |
| RemoteControlPairingStartResponse { | |
| pairing_code: "pairing-code".to_string(), | |
| manual_pairing_code: manual_pairing_code.map(str::to_string), | |
| environment_id: "env_test".to_string(), | |
| expires_at: 1_700_000_000, | |
| } | |
| } | |
| fn remote_control_human_start_messages_use_server_name() { | |
| assert_eq!( | |
| remote_control_start_human_message(&remote_control_status( | |
| RemoteControlConnectionStatus::Connected | |
| )) | |
| .expect("connected message"), | |
| "This machine is available for remote control as owen-mbp." | |
| ); | |
| assert_eq!( | |
| remote_control_start_human_message(&remote_control_status( | |
| RemoteControlConnectionStatus::Connecting | |
| )) | |
| .expect("connecting message"), | |
| "Remote control is enabled on owen-mbp and still connecting." | |
| ); | |
| assert_eq!( | |
| remote_control_start_human_message(&remote_control_status( | |
| RemoteControlConnectionStatus::Errored | |
| )) | |
| .expect_err("errored status should fail") | |
| .to_string(), | |
| "Remote control is enabled on owen-mbp but the connection is errored." | |
| ); | |
| assert_eq!( | |
| remote_control_start_human_message(&remote_control_status( | |
| RemoteControlConnectionStatus::Disabled | |
| )) | |
| .expect_err("disabled status should fail") | |
| .to_string(), | |
| "Remote control is disabled on owen-mbp." | |
| ); | |
| } | |
| fn remote_control_human_lines_include_foreground_stop_hint_only() { | |
| let summary = remote_control_status(RemoteControlConnectionStatus::Connected); | |
| assert_eq!( | |
| remote_control_start_human_lines(&summary, RemoteControlHumanOutputMode::Foreground) | |
| .expect("foreground lines"), | |
| vec![ | |
| "This machine is available for remote control as owen-mbp.".to_string(), | |
| "Press Ctrl-C to stop.".to_string(), | |
| ] | |
| ); | |
| assert_eq!( | |
| remote_control_start_human_lines(&summary, RemoteControlHumanOutputMode::Daemon) | |
| .expect("daemon lines"), | |
| vec!["This machine is available for remote control as owen-mbp.".to_string()] | |
| ); | |
| } | |
| fn daemon_app_server_human_lines_include_path_and_version() { | |
| assert_eq!( | |
| daemon_app_server_human_lines( | |
| &daemon_ready_output(RemoteControlConnectionStatus::Connected).daemon | |
| ), | |
| vec![ | |
| "Daemon used app-server:".to_string(), | |
| " path: /opt/codex/bin/codex".to_string(), | |
| " version: 1.0.0".to_string(), | |
| ] | |
| ); | |
| } | |
| fn remote_control_json_output_marks_foreground_or_daemon() { | |
| let foreground_summary = remote_control_status(RemoteControlConnectionStatus::Connected); | |
| assert_eq!( | |
| serde_json::to_value(RemoteControlStartJsonOutput::foreground( | |
| &foreground_summary | |
| )) | |
| .expect("foreground JSON"), | |
| json!({ | |
| "mode": "foreground", | |
| "status": "connected", | |
| "serverName": "owen-mbp", | |
| "environmentId": "env_test", | |
| "timedOut": false, | |
| }) | |
| ); | |
| let daemon_output = daemon_ready_output(RemoteControlConnectionStatus::Connected); | |
| assert_eq!( | |
| serde_json::to_value(RemoteControlStartJsonOutput::daemon(&daemon_output)) | |
| .expect("daemon JSON"), | |
| json!({ | |
| "mode": "daemon", | |
| "status": "connected", | |
| "serverName": "owen-mbp", | |
| "environmentId": "env_test", | |
| "timedOut": false, | |
| "daemon": { | |
| "status": "started", | |
| "pid": 42, | |
| "managedCodexPath": "/opt/codex/bin/codex", | |
| "managedCodexVersion": "1.0.0", | |
| "socketPath": "/tmp/app-server-control.sock", | |
| "cliVersion": "1.0.0", | |
| "appServerVersion": "2.0.0", | |
| }, | |
| }) | |
| ); | |
| } | |
| fn remote_control_daemon_json_rejects_unstartable_status() { | |
| assert_eq!( | |
| print_remote_control_start_output( | |
| &daemon_ready_output(RemoteControlConnectionStatus::Errored), | |
| /*json*/ true | |
| ) | |
| .expect_err("errored daemon status should fail") | |
| .to_string(), | |
| "Remote control is enabled on owen-mbp but the connection is errored." | |
| ); | |
| } | |
| fn remote_control_pairing_human_output_labels_the_manual_code() { | |
| assert_eq!( | |
| format_remote_control_pairing_output( | |
| &pairing_response(Some("ABCD-EFGH")), | |
| /*json*/ false, | |
| ) | |
| .expect("manual pairing output"), | |
| "Pairing code: ABCD-EFGH" | |
| ); | |
| } | |
| fn remote_control_pairing_json_output_preserves_pairing_artifacts() { | |
| let output = format_remote_control_pairing_output( | |
| &pairing_response(Some("ABCD-EFGH")), | |
| /*json*/ true, | |
| ) | |
| .expect("pairing JSON output"); | |
| assert_eq!( | |
| serde_json::from_str::<serde_json::Value>(&output).expect("valid JSON"), | |
| json!({ | |
| "pairingCode": "pairing-code", | |
| "manualPairingCode": "ABCD-EFGH", | |
| "environmentId": "env_test", | |
| "expiresAt": 1_700_000_000, | |
| }) | |
| ); | |
| } | |
| fn remote_control_pairing_human_output_requires_manual_code() { | |
| assert_eq!( | |
| format_remote_control_pairing_output( | |
| &pairing_response(/*manual_pairing_code*/ None), | |
| /*json*/ false, | |
| ) | |
| .expect_err("missing manual pairing code should fail") | |
| .to_string(), | |
| "remote-control pairing response did not include a manual pairing code" | |
| ); | |
| } | |
| async fn foreground_wait_aborts_app_server_on_stop_signal() { | |
| let app_server_task = tokio::spawn(std::future::pending::<std::io::Result<()>>()); | |
| let (stop_tx, stop_rx) = tokio::sync::watch::channel(false); | |
| stop_tx.send(true).expect("send stop signal"); | |
| tokio::time::timeout( | |
| std::time::Duration::from_secs(1), | |
| wait_for_foreground_app_server(app_server_task, stop_rx), | |
| ) | |
| .await | |
| .expect("foreground wait should return after stop signal") | |
| .expect("stop signal should shut down cleanly"); | |
| } | |
| async fn foreground_start_wait_stops_before_ready() { | |
| let mut app_server_task = tokio::spawn(std::future::pending::<std::io::Result<()>>()); | |
| let (stop_tx, stop_rx) = tokio::sync::watch::channel(false); | |
| stop_tx.send(true).expect("send stop signal"); | |
| let startup = tokio::time::timeout( | |
| std::time::Duration::from_secs(1), | |
| wait_for_foreground_remote_control_start( | |
| &mut app_server_task, | |
| std::future::pending::<anyhow::Result<AppServerRemoteControlReadyStatus>>(), | |
| stop_rx, | |
| ), | |
| ) | |
| .await | |
| .expect("foreground startup wait should return after stop signal"); | |
| assert!(matches!(startup, ForegroundStartupResult::Stopped)); | |
| app_server_task.abort(); | |
| let _ = app_server_task.await; | |
| } | |
| async fn foreground_start_wait_reports_app_server_exit_before_ready() { | |
| let mut app_server_task = | |
| tokio::spawn(async { Err(std::io::Error::other("startup failed before socket bind")) }); | |
| let (_stop_tx, stop_rx) = tokio::sync::watch::channel(false); | |
| let startup = tokio::time::timeout( | |
| std::time::Duration::from_secs(1), | |
| wait_for_foreground_remote_control_start( | |
| &mut app_server_task, | |
| std::future::pending::<anyhow::Result<AppServerRemoteControlReadyStatus>>(), | |
| stop_rx, | |
| ), | |
| ) | |
| .await | |
| .expect("foreground startup wait should return after app-server exits"); | |
| let ForegroundStartupResult::AppServerExited(error) = startup else { | |
| panic!("expected app-server exit before ready"); | |
| }; | |
| assert_eq!( | |
| error.to_string(), | |
| "foreground app-server exited before remote control became ready" | |
| ); | |
| } | |
| } | |