1use std::{
4 process::{Child, Command as StdCommand, Stdio},
5 sync::{Arc, atomic::AtomicBool, mpsc},
6 time::{Duration, Instant},
7};
8
9use parking_lot::Mutex;
10use tokio_util::sync::CancellationToken;
11
12use crate::{
13 AppError, AppResult, EnvPolicy, ErrorCode, InputPolicy, ProcessConfig, ProcessIo, ProcessSpec,
14 SignalPolicy, process_group::isolate,
15};
16
17mod cancel;
18mod config;
19mod error;
20mod io;
21mod process;
22mod readiness;
23
24#[cfg(all(test, unix))]
25mod tests;
26
27pub use config::{PersistentConfig, PersistentOutput, PersistentOutputStream, PersistentReadiness};
28pub use error::{PersistentStartErrorKind, persistent_start_error_kind};
29pub use process::{PersistentProcess, ShutdownOutcome};
30
31use cancel::spawn_cancel_thread;
32use config::PersistentReadiness::{Command as CommandReadiness, OutputContains, Started};
33use error::persistent_start_error;
34use io::{
35 CapturedOutput, ReaderThread, StdinThread, spawn_output_readers, spawn_stdin_writer,
36 take_capture,
37};
38use process::{cleanup_spawned_child, new_process};
39use readiness::{
40 readiness_wait_error, run_readiness_command, validate_readiness, wait_for_readiness,
41};
42
43#[derive(Debug, Clone)]
45pub struct PersistentStartup {
46 pub stdout: String,
48 pub stdout_bytes: Vec<u8>,
50 pub stderr: String,
52 pub stderr_bytes: Vec<u8>,
54 pub stdout_truncated: bool,
56 pub stderr_truncated: bool,
58 pub duration: Duration,
60}
61
62#[derive(Debug)]
64pub struct PersistentRun {
65 pub startup: PersistentStartup,
67 pub process: PersistentProcess,
69}
70
71pub fn start_persistent_with_cancel(
73 spec: &ProcessSpec,
74 process_config: &ProcessConfig,
75 persistent_config: &PersistentConfig,
76 cancel: CancellationToken,
77) -> AppResult<PersistentRun> {
78 if spec.program.as_os_str().is_empty() {
79 return Err(AppError::invalid_input("program", "must not be empty"));
80 }
81 if cancel.is_cancelled() {
82 return Err(AppError::cancelled("persistent process startup"));
83 }
84 validate_readiness(&persistent_config.readiness)?;
85
86 let start = Instant::now();
87 let input = process_input(process_config)?;
88 let mut child = spawn_child(spec, process_config, input)?;
89 let stdout = Arc::new(Mutex::new(CapturedOutput::default()));
90 let stderr = Arc::new(Mutex::new(CapturedOutput::default()));
91 let cancelled = Arc::new(AtomicBool::new(false));
92 let cancel_thread = match spawn_cancel_thread(
93 child.id(),
94 cancel.clone(),
95 Arc::clone(&cancelled),
96 process_config.signal,
97 persistent_config.shutdown_grace_period,
98 ) {
99 Ok(thread) => Some(thread),
100 Err(error) => {
101 let _ = cleanup_spawned_child(
102 &mut child,
103 process_config.signal,
104 persistent_config.shutdown_grace_period,
105 );
106 return Err(error);
107 }
108 };
109 let (ready_tx, ready_rx) = mpsc::channel();
110 let (stdout_thread, stderr_thread) = spawn_output_readers(
111 &mut child,
112 &stdout,
113 &stderr,
114 &ready_tx,
115 &persistent_config.readiness,
116 persistent_config.output,
117 persistent_config.max_capture_bytes,
118 );
119 let stdin_thread = spawn_stdin_writer(&mut child, predefined_stdin(input));
120
121 match &persistent_config.readiness {
122 Started => {
123 let _ = ready_tx.send(());
124 }
125 CommandReadiness(command) => {
126 if let Err(error) = run_readiness_command(
127 command,
128 process_config,
129 persistent_config.readiness_timeout,
130 persistent_config.shutdown_grace_period,
131 cancel.clone(),
132 ) {
133 let mut process = persistent_process(
134 child,
135 stdin_thread,
136 stdout_thread,
137 stderr_thread,
138 cancel_thread,
139 cancelled,
140 stdout,
141 stderr,
142 start,
143 process_config.signal,
144 persistent_config,
145 );
146 let _ = process.shutdown_inner();
149 return Err(error);
150 }
151 let _ = ready_tx.send(());
152 }
153 OutputContains(_) => {}
154 }
155 drop(ready_tx);
156
157 if let Err(error) = wait_for_readiness(
158 &ready_rx,
159 persistent_config.readiness_timeout,
160 &cancel,
161 &cancelled,
162 ) {
163 let readiness_error = readiness_wait_error(&mut child, error, &cancelled)?;
164 let mut process = persistent_process(
165 child,
166 stdin_thread,
167 stdout_thread,
168 stderr_thread,
169 cancel_thread,
170 cancelled,
171 stdout,
172 stderr,
173 start,
174 process_config.signal,
175 persistent_config,
176 );
177 let _ = process.shutdown_inner();
179 return Err(readiness_error);
180 }
181
182 let stdout_startup = take_capture(&stdout);
183 let stderr_startup = take_capture(&stderr);
184 let process = persistent_process(
185 child,
186 stdin_thread,
187 stdout_thread,
188 stderr_thread,
189 cancel_thread,
190 cancelled,
191 stdout,
192 stderr,
193 start,
194 process_config.signal,
195 persistent_config,
196 );
197
198 Ok(PersistentRun {
199 startup: PersistentStartup {
200 stdout: String::from_utf8_lossy(&stdout_startup.bytes).into_owned(),
201 stdout_bytes: stdout_startup.bytes,
202 stderr: String::from_utf8_lossy(&stderr_startup.bytes).into_owned(),
203 stderr_bytes: stderr_startup.bytes,
204 stdout_truncated: stdout_startup.truncated,
205 stderr_truncated: stderr_startup.truncated,
206 duration: start.elapsed(),
207 },
208 process,
209 })
210}
211
212fn process_input(config: &ProcessConfig) -> AppResult<&InputPolicy> {
213 match &config.io {
214 ProcessIo::Captured(io)
215 if matches!(io.input, InputPolicy::Closed | InputPolicy::Bytes(_)) =>
216 {
217 Ok(&io.input)
218 }
219 ProcessIo::Captured(_) => Err(AppError::invalid_input(
220 "process.io.input",
221 "persistent processes support only closed stdin or predefined stdin bytes",
222 )),
223 ProcessIo::Inherited(_) => Err(AppError::invalid_input(
224 "process.io",
225 "persistent processes use PersistentOutput for output handling; inherited mode is not supported",
226 )),
227 ProcessIo::Observed(_) => Err(AppError::invalid_input(
228 "process.io",
229 "persistent processes use PersistentOutput for observation; observed mode is not supported",
230 )),
231 }
232}
233
234fn predefined_stdin(input: &InputPolicy) -> Option<Vec<u8>> {
235 match input {
236 InputPolicy::Bytes(bytes) => Some(bytes.clone()),
237 InputPolicy::Closed | InputPolicy::Inherit => None,
238 }
239}
240
241fn spawn_child(
242 spec: &ProcessSpec,
243 config: &ProcessConfig,
244 input: &InputPolicy,
245) -> AppResult<Child> {
246 let mut cmd = StdCommand::new(&spec.program);
247 cmd.args(&spec.args)
248 .stdin(if matches!(input, InputPolicy::Bytes(_)) {
249 Stdio::piped()
250 } else {
251 Stdio::null()
252 })
253 .stdout(Stdio::piped())
254 .stderr(Stdio::piped());
255
256 if let Some(dir) = &spec.dir {
257 cmd.current_dir(dir);
258 }
259 if matches!(spec.env_policy, EnvPolicy::Empty) {
260 cmd.env_clear();
261 }
262 for (key, value) in &spec.env {
263 cmd.env(key, value);
264 }
265 if config.signal.create_process_group {
266 isolate(&mut cmd);
267 }
268
269 cmd.spawn().map_err(|error| {
270 persistent_start_error(
271 PersistentStartErrorKind::SpawnFailed,
272 ErrorCode::Internal,
273 format!("failed to spawn persistent process: {error}"),
274 )
275 .with_cause(error)
276 })
277}
278
279#[allow(clippy::too_many_arguments)]
280fn persistent_process(
281 child: Child,
282 stdin_thread: StdinThread,
283 stdout_thread: ReaderThread,
284 stderr_thread: ReaderThread,
285 cancel_thread: Option<cancel::CancelThread>,
286 cancelled: Arc<AtomicBool>,
287 stdout: io::Capture,
288 stderr: io::Capture,
289 start: Instant,
290 signal: SignalPolicy,
291 config: &PersistentConfig,
292) -> PersistentProcess {
293 new_process(
294 child,
295 stdin_thread,
296 stdout_thread,
297 stderr_thread,
298 cancel_thread,
299 cancelled,
300 stdout,
301 stderr,
302 start,
303 signal,
304 config.shutdown_grace_period,
305 )
306}