codex/commands/
app_server.rs1use std::fs as std_fs;
2
3use tokio::{process::Command, time};
4
5use crate::{
6 builder::{apply_cli_overrides, resolve_cli_overrides},
7 process::{spawn_with_retry, tee_stream, ConsoleTarget},
8 AppServerCodegenOutput, AppServerCodegenRequest, AppServerProxyRequest, AppServerRequest,
9 CodexClient, CodexError,
10};
11
12impl CodexClient {
13 pub fn start_app_server(
15 &self,
16 request: AppServerRequest,
17 ) -> Result<tokio::process::Child, CodexError> {
18 let AppServerRequest {
19 listen,
20 ws_audience,
21 ws_auth,
22 ws_issuer,
23 ws_max_clock_skew_seconds,
24 ws_shared_secret_file,
25 ws_token_file,
26 ws_token_sha256,
27 working_dir,
28 overrides,
29 } = request;
30
31 let resolved_overrides =
32 resolve_cli_overrides(&self.cli_overrides, &overrides, self.model.as_deref());
33
34 let mut command = Command::new(self.command_env.binary_path());
35 command
36 .stdin(std::process::Stdio::piped())
37 .stdout(std::process::Stdio::piped())
38 .stderr(std::process::Stdio::piped())
39 .kill_on_drop(true)
40 .current_dir(self.sandbox_working_dir(working_dir)?);
41
42 apply_cli_overrides(&mut command, &resolved_overrides, true);
43 command.arg("app-server");
44
45 if let Some(listen) = listen {
46 command.arg("--listen").arg(listen);
47 }
48 if let Some(ws_audience) = ws_audience {
49 command.arg("--ws-audience").arg(ws_audience);
50 }
51 if let Some(ws_auth) = ws_auth {
52 command.arg("--ws-auth").arg(ws_auth);
53 }
54 if let Some(ws_issuer) = ws_issuer {
55 command.arg("--ws-issuer").arg(ws_issuer);
56 }
57 if let Some(ws_max_clock_skew_seconds) = ws_max_clock_skew_seconds {
58 command
59 .arg("--ws-max-clock-skew-seconds")
60 .arg(ws_max_clock_skew_seconds.to_string());
61 }
62 if let Some(ws_shared_secret_file) = ws_shared_secret_file {
63 command
64 .arg("--ws-shared-secret-file")
65 .arg(ws_shared_secret_file);
66 }
67 if let Some(ws_token_file) = ws_token_file {
68 command.arg("--ws-token-file").arg(ws_token_file);
69 }
70 if let Some(ws_token_sha256) = ws_token_sha256 {
71 command.arg("--ws-token-sha256").arg(ws_token_sha256);
72 }
73
74 self.command_env.apply(&mut command)?;
75
76 spawn_with_retry(&mut command, self.command_env.binary_path())
77 }
78
79 pub fn start_app_server_proxy(
81 &self,
82 request: AppServerProxyRequest,
83 ) -> Result<tokio::process::Child, CodexError> {
84 let AppServerProxyRequest {
85 socket_path,
86 working_dir,
87 overrides,
88 } = request;
89
90 if socket_path
91 .as_ref()
92 .is_some_and(|path| path.as_os_str().is_empty())
93 {
94 return Err(CodexError::EmptySocketPath);
95 }
96
97 let resolved_overrides =
98 resolve_cli_overrides(&self.cli_overrides, &overrides, self.model.as_deref());
99
100 let mut command = Command::new(self.command_env.binary_path());
101 command
102 .stdin(std::process::Stdio::piped())
103 .stdout(std::process::Stdio::piped())
104 .stderr(std::process::Stdio::piped())
105 .kill_on_drop(true)
106 .current_dir(self.sandbox_working_dir(working_dir)?);
107
108 apply_cli_overrides(&mut command, &resolved_overrides, true);
109 command.arg("app-server").arg("proxy");
110
111 if let Some(socket_path) = socket_path.as_ref() {
112 command.arg("--sock").arg(socket_path);
113 }
114
115 self.command_env.apply(&mut command)?;
116
117 spawn_with_retry(&mut command, self.command_env.binary_path())
118 }
119
120 pub async fn generate_app_server_bindings(
128 &self,
129 request: AppServerCodegenRequest,
130 ) -> Result<AppServerCodegenOutput, CodexError> {
131 let AppServerCodegenRequest {
132 target,
133 out_dir,
134 experimental,
135 overrides,
136 } = request;
137
138 std_fs::create_dir_all(&out_dir).map_err(|source| CodexError::PrepareOutputDirectory {
139 path: out_dir.clone(),
140 source,
141 })?;
142
143 let dir_ctx = self.directory_context()?;
144 let resolved_overrides =
145 resolve_cli_overrides(&self.cli_overrides, &overrides, self.model.as_deref());
146
147 let mut command = Command::new(self.command_env.binary_path());
148 command
149 .stdout(std::process::Stdio::piped())
150 .stderr(std::process::Stdio::piped())
151 .kill_on_drop(true)
152 .current_dir(dir_ctx.path());
153
154 apply_cli_overrides(&mut command, &resolved_overrides, true);
155 command
156 .arg("app-server")
157 .arg(target.subcommand())
158 .arg("--out")
159 .arg(&out_dir);
160
161 if experimental {
162 command.arg("--experimental");
163 }
164
165 if let Some(prettier) = target.prettier() {
166 command.arg("--prettier").arg(prettier);
167 }
168
169 self.command_env.apply(&mut command)?;
170
171 let mut child = spawn_with_retry(&mut command, self.command_env.binary_path())?;
172
173 let stdout = child.stdout.take().ok_or(CodexError::StdoutUnavailable)?;
174 let stderr = child.stderr.take().ok_or(CodexError::StderrUnavailable)?;
175
176 let stdout_task = tokio::spawn(tee_stream(
177 stdout,
178 ConsoleTarget::Stdout,
179 self.mirror_stdout,
180 ));
181 let stderr_task = tokio::spawn(tee_stream(stderr, ConsoleTarget::Stderr, !self.quiet));
182
183 let wait_task = async move {
184 let status = child
185 .wait()
186 .await
187 .map_err(|source| CodexError::Wait { source })?;
188 let stdout_bytes = stdout_task
189 .await
190 .map_err(CodexError::Join)?
191 .map_err(CodexError::CaptureIo)?;
192 let stderr_bytes = stderr_task
193 .await
194 .map_err(CodexError::Join)?
195 .map_err(CodexError::CaptureIo)?;
196 Ok::<_, CodexError>((status, stdout_bytes, stderr_bytes))
197 };
198
199 let (status, stdout_bytes, stderr_bytes) = if self.timeout.is_zero() {
200 wait_task.await?
201 } else {
202 match time::timeout(self.timeout, wait_task).await {
203 Ok(result) => result?,
204 Err(_) => {
205 return Err(CodexError::Timeout {
206 timeout: self.timeout,
207 });
208 }
209 }
210 };
211
212 if !status.success() {
213 return Err(CodexError::NonZeroExit {
214 status,
215 stderr: String::from_utf8(stderr_bytes)?,
216 });
217 }
218
219 Ok(AppServerCodegenOutput {
220 status,
221 stdout: String::from_utf8(stdout_bytes)?,
222 stderr: String::from_utf8(stderr_bytes)?,
223 out_dir,
224 })
225 }
226}