1use std::process::Stdio;
2use std::time::Instant;
3
4use tokio::process::Command as TokioCommand;
5use tokio_util::sync::CancellationToken;
6use tracing::debug;
7
8use crate::{
9 AppError, AppResult, ErrorCode, InheritedIo, InputPolicy, OutputPolicy, ProcessConfig,
10 ProcessIo, ProcessResult, ProcessSpec,
11 capture::{append_line_bounded, shared_output},
12};
13
14use super::lifecycle::wait_for_completion;
15use super::observer::OutputObserver;
16use super::output::{captured, join_within, spawn_reader};
17use super::pipe_io::{PipeIo, inherited_config, pipe_stdin_stdio, spawn_stdin_writer, stdin_stdio};
18#[cfg(unix)]
19use super::pty::run_pty_mode;
20use super::redaction::RedactedArgs;
21use super::scope::ChildScope;
22use super::spawn::{PipeStdio, configure_command};
23
24pub async fn run_with_cancel(
26 spec: &ProcessSpec,
27 config: &ProcessConfig,
28 cancel: CancellationToken,
29) -> AppResult<ProcessResult> {
30 match &config.io {
31 ProcessIo::Captured(io) => run_pipe_mode(spec, config, cancel, io, None).await,
32 ProcessIo::Observed(io) => {
33 run_pipe_mode(spec, config, cancel, io, Some(io.observer.clone())).await
34 }
35 ProcessIo::Inherited(io) => run_inherited_mode(spec, config, cancel, io).await,
36 #[cfg(unix)]
37 ProcessIo::Pty(io) => run_pty_mode(spec, config, cancel, io).await,
38 }
39}
40
41async fn run_pipe_mode(
42 spec: &ProcessSpec,
43 config: &ProcessConfig,
44 cancel: CancellationToken,
45 io: &impl PipeIo,
46 observer: Option<OutputObserver>,
47) -> AppResult<ProcessResult> {
48 let stdout_observer = observer
49 .as_ref()
50 .and_then(|observer| observer.stdout_line.clone());
51 let stderr_observer = observer
52 .as_ref()
53 .and_then(|observer| observer.stderr_line.clone());
54 let stdout_bytes_observer = observer
55 .as_ref()
56 .and_then(|observer| observer.stdout_bytes.clone());
57 let stderr_bytes_observer = observer
58 .as_ref()
59 .and_then(|observer| observer.stderr_bytes.clone());
60 let output = io.output();
61 let pipe_stdout =
62 output.capture_stdout || stdout_observer.is_some() || stdout_bytes_observer.is_some();
63 let pipe_stderr =
64 output.capture_stderr || stderr_observer.is_some() || stderr_bytes_observer.is_some();
65
66 let stdio = PipeStdio {
67 stdin: pipe_stdin_stdio(io.input())?,
68 stdout: if pipe_stdout {
69 Stdio::piped()
70 } else {
71 Stdio::null()
72 },
73 stderr: if pipe_stderr {
74 Stdio::piped()
75 } else {
76 Stdio::null()
77 },
78 };
79
80 run_process(
81 spec,
82 config,
83 cancel,
84 stdio,
85 io.input(),
86 Some(output),
87 observer,
88 )
89 .await
90}
91
92async fn run_inherited_mode(
93 spec: &ProcessSpec,
94 config: &ProcessConfig,
95 cancel: CancellationToken,
96 io: &InheritedIo,
97) -> AppResult<ProcessResult> {
98 let inherited_config = inherited_config(config);
99 let stdio = PipeStdio {
100 stdin: stdin_stdio(&io.input),
101 stdout: Stdio::inherit(),
102 stderr: Stdio::inherit(),
103 };
104 run_process(
105 spec,
106 &inherited_config,
107 cancel,
108 stdio,
109 &io.input,
110 None,
111 None,
112 )
113 .await
114}
115
116async fn run_process(
117 spec: &ProcessSpec,
118 config: &ProcessConfig,
119 cancel: CancellationToken,
120 stdio: PipeStdio,
121 input: &InputPolicy,
122 output: Option<&OutputPolicy>,
123 observer: Option<OutputObserver>,
124) -> AppResult<ProcessResult> {
125 if spec.program.as_os_str().is_empty() {
126 return Err(AppError::invalid_input("program", "must not be empty"));
127 }
128
129 let start = Instant::now();
130 let stdout_observer = observer
131 .as_ref()
132 .and_then(|observer| observer.stdout_line.clone());
133 let stderr_observer = observer
134 .as_ref()
135 .and_then(|observer| observer.stderr_line.clone());
136 let stdout_bytes_observer = observer
137 .as_ref()
138 .and_then(|observer| observer.stdout_bytes.clone());
139 let stderr_bytes_observer = observer
140 .as_ref()
141 .and_then(|observer| observer.stderr_bytes.clone());
142
143 let mut cmd = TokioCommand::new(&spec.program);
144 configure_command(&mut cmd, spec, config, stdio);
145
146 debug!(program = %spec.program.display(), args = ?RedactedArgs::new(&spec.args, &config.arg_redaction), "spawning process");
147 let mut child = cmd.spawn().map_err(|error| {
148 AppError::new(
149 ErrorCode::Internal,
150 format!("failed to spawn process: {error}"),
151 )
152 })?;
153
154 let stdout_pipe = child.stdout.take();
157 let stderr_pipe = child.stderr.take();
158 let stdin_pipe = child.stdin.take();
159
160 let mut scope = ChildScope::new(child);
164
165 let max_output_bytes = output.and_then(|output| output.max_output_bytes);
166 let capture_stdout = output.is_some_and(|output| output.capture_stdout);
167 let capture_stderr = output.is_some_and(|output| output.capture_stderr);
168 let stdout_capture = shared_output();
169 let stderr_capture = shared_output();
170 let stdout_task = spawn_reader(
171 stdout_pipe,
172 stdout_capture.clone(),
173 max_output_bytes,
174 stdout_observer,
175 stdout_bytes_observer,
176 capture_stdout,
177 );
178 scope.register(&stdout_task);
179 let stderr_task = spawn_reader(
180 stderr_pipe,
181 stderr_capture.clone(),
182 max_output_bytes,
183 stderr_observer,
184 stderr_bytes_observer,
185 capture_stderr,
186 );
187 scope.register(&stderr_task);
188
189 let stdin_task = spawn_stdin_writer(stdin_pipe, input);
190 scope.register(&stdin_task);
191
192 let completion = wait_for_completion(scope.child_mut(), spec, config, cancel).await?;
193
194 let grace = config.signal.grace_period;
198 join_within(stdin_task, grace).await?;
199 join_within(stdout_task, grace).await?;
200 join_within(stderr_task, grace).await?;
201 let stdout_output = captured(&stdout_capture);
202 let stdout_bytes = stdout_output.bytes;
203 let stdout_truncated = stdout_output.truncated;
204 let stderr_output = captured(&stderr_capture);
205
206 scope.disarm();
209
210 let mut stderr_bytes = stderr_output.bytes;
211 let mut stderr_truncated = stderr_output.truncated;
212 if let Some(extra_stderr) = completion.synthetic_stderr {
213 stderr_truncated |=
214 append_line_bounded(&mut stderr_bytes, extra_stderr.as_bytes(), max_output_bytes);
215 }
216
217 let result = ProcessResult::completed(
218 completion.exit_code,
219 stdout_bytes,
220 stderr_bytes,
221 stdout_truncated,
222 stderr_truncated,
223 start.elapsed(),
224 completion.timed_out,
225 completion.cancelled,
226 );
227
228 debug!(
229 exit_code = ?result.exit_code,
230 duration = ?result.duration,
231 timed_out = result.timed_out,
232 "process completed"
233 );
234
235 Ok(result)
236}