Skip to main content

rskit_process/runner/
run.rs

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
24/// Execute a subprocess with the given configuration and cancellation token.
25pub 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    // Detach the child's pipe handles before the child moves into the scope guard,
155    // which then owns it for the rest of the run.
156    let stdout_pipe = child.stdout.take();
157    let stderr_pipe = child.stderr.take();
158    let stdin_pipe = child.stdin.take();
159
160    // Own the child and the spawned I/O tasks in a scope guard:
161    // any early return below aborts the reader/stdin tasks
162    // and best-effort kills the child rather than detaching them (a dropped Tokio `JoinHandle` is detached, which would leak the task and keep the child's pipes alive).
163    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    // The child has exited; drain the workers within the grace period.
195    // A reader still blocked because a surviving descendant holds the pipe open is aborted rather than awaited forever,
196    // and the partial bytes it captured are still recovered from the shared buffer.
197    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    // The run completed normally: the tasks are joined and the child is reaped,
207    // so hand ownership back instead of aborting/killing on drop.
208    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}