1use std::time::Duration;
2
3use tokio::process::Command;
4use tracing::debug;
5
6use crate::Codex;
7use crate::error::{Error, Result};
8
9#[derive(Debug, Clone)]
11pub struct CommandOutput {
12 pub stdout: String,
13 pub stderr: String,
14 pub exit_code: i32,
15 pub success: bool,
16}
17
18pub async fn run_codex(codex: &Codex, args: Vec<String>) -> Result<CommandOutput> {
24 run_codex_with_retry(codex, args, None).await
25}
26
27pub async fn run_codex_with_retry(
29 codex: &Codex,
30 args: Vec<String>,
31 retry_override: Option<&crate::retry::RetryPolicy>,
32) -> Result<CommandOutput> {
33 let policy = retry_override.or(codex.retry_policy.as_ref());
34
35 match policy {
36 Some(policy) => {
37 crate::retry::with_retry(policy, || run_codex_once(codex, args.clone())).await
38 }
39 None => run_codex_once(codex, args).await,
40 }
41}
42
43async fn run_codex_once(codex: &Codex, args: Vec<String>) -> Result<CommandOutput> {
44 let mut command_args = Vec::new();
45
46 command_args.extend(codex.global_args.clone());
48
49 command_args.extend(args);
51
52 debug!(binary = %codex.binary.display(), args = ?command_args, "executing codex command");
53
54 let output = if let Some(timeout) = codex.timeout {
55 run_with_timeout(
56 &codex.binary,
57 &command_args,
58 &codex.env,
59 codex.working_dir.as_deref(),
60 timeout,
61 )
62 .await?
63 } else {
64 run_internal(
65 &codex.binary,
66 &command_args,
67 &codex.env,
68 codex.working_dir.as_deref(),
69 )
70 .await?
71 };
72
73 Ok(output)
74}
75
76pub async fn run_codex_allow_exit_codes(
78 codex: &Codex,
79 args: Vec<String>,
80 allowed_codes: &[i32],
81) -> Result<CommandOutput> {
82 let output = run_codex(codex, args).await;
83
84 match output {
85 Err(Error::CommandFailed {
86 exit_code,
87 stdout,
88 stderr,
89 ..
90 }) if allowed_codes.contains(&exit_code) => Ok(CommandOutput {
91 stdout,
92 stderr,
93 exit_code,
94 success: false,
95 }),
96 other => other,
97 }
98}
99
100async fn run_internal(
101 binary: &std::path::Path,
102 args: &[String],
103 env: &std::collections::HashMap<String, String>,
104 working_dir: Option<&std::path::Path>,
105) -> Result<CommandOutput> {
106 let mut cmd = Command::new(binary);
107 cmd.args(args);
108
109 cmd.stdin(std::process::Stdio::null());
111
112 if let Some(dir) = working_dir {
113 cmd.current_dir(dir);
114 }
115
116 for (key, value) in env {
117 cmd.env(key, value);
118 }
119
120 let output = cmd.output().await.map_err(|e| Error::Io {
121 message: format!("failed to spawn codex: {e}"),
122 source: e,
123 working_dir: working_dir.map(|p| p.to_path_buf()),
124 })?;
125
126 let stdout = String::from_utf8_lossy(&output.stdout).to_string();
127 let stderr = String::from_utf8_lossy(&output.stderr).to_string();
128 let exit_code = output.status.code().unwrap_or(-1);
129
130 if !output.status.success() {
131 return Err(Error::CommandFailed {
132 command: format!("{} {}", binary.display(), args.join(" ")),
133 exit_code,
134 stdout,
135 stderr,
136 working_dir: working_dir.map(|p| p.to_path_buf()),
137 });
138 }
139
140 Ok(CommandOutput {
141 stdout,
142 stderr,
143 exit_code,
144 success: true,
145 })
146}
147
148async fn run_with_timeout(
149 binary: &std::path::Path,
150 args: &[String],
151 env: &std::collections::HashMap<String, String>,
152 working_dir: Option<&std::path::Path>,
153 timeout: Duration,
154) -> Result<CommandOutput> {
155 tokio::time::timeout(timeout, run_internal(binary, args, env, working_dir))
156 .await
157 .map_err(|_| Error::Timeout {
158 timeout_seconds: timeout.as_secs(),
159 })?
160}