1use std::{
4 collections::BTreeMap,
5 ffi::OsString,
6 future::Future,
7 io,
8 path::PathBuf,
9 process::{ExitStatus, Stdio},
10 time::Duration,
11};
12
13use thiserror::Error;
14use tokio::{
15 io::{AsyncRead, AsyncReadExt, AsyncWriteExt},
16 time::{Instant, sleep, sleep_until, timeout},
17};
18use tokio_util::sync::CancellationToken;
19
20use crate::platform::{self, Child};
21
22const OUTPUT_LIMIT: usize = 128 * 1024;
24const CLEANUP_GRACE: Duration = Duration::from_secs(1);
26const CAPTURE_BUFFER_BYTES: usize = 8192;
28
29#[derive(Debug, Clone)]
31pub struct Command {
32 pub program: OsString,
33 pub args: Vec<OsString>,
34 pub cwd: PathBuf,
35 pub env: BTreeMap<OsString, OsString>,
37 pub timeout: Duration,
38}
39
40#[derive(
44 Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, serde::Serialize, serde::Deserialize,
45)]
46#[serde(transparent)]
47pub struct ProcessId(std::num::NonZeroU32);
48
49impl ProcessId {
50 pub fn new(pid: u32) -> Self {
52 Self(std::num::NonZeroU32::new(pid).expect("a real process id is never 0"))
53 }
54
55 pub fn get(self) -> u32 {
56 self.0.get()
57 }
58}
59
60impl From<std::num::NonZeroU32> for ProcessId {
61 fn from(pid: std::num::NonZeroU32) -> Self {
62 Self(pid)
63 }
64}
65
66impl std::fmt::Display for ProcessId {
67 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
68 self.0.fmt(f)
69 }
70}
71
72impl rusqlite::types::ToSql for ProcessId {
73 fn to_sql(&self) -> rusqlite::Result<rusqlite::types::ToSqlOutput<'_>> {
74 Ok(rusqlite::types::ToSqlOutput::from(i64::from(self.0.get())))
75 }
76}
77
78impl rusqlite::types::FromSql for ProcessId {
79 fn column_result(value: rusqlite::types::ValueRef<'_>) -> rusqlite::types::FromSqlResult<Self> {
80 let pid = u32::try_from(value.as_i64()?)
81 .map_err(|error| rusqlite::types::FromSqlError::Other(error.into()))?;
82 std::num::NonZeroU32::new(pid).map(Self).ok_or_else(|| {
83 rusqlite::types::FromSqlError::Other(
84 "a saved owner_pid of 0 is not a process id".into(),
85 )
86 })
87 }
88}
89
90#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
92pub struct ChildIdentity {
93 pub pid: ProcessId,
94 pub start_time: u64,
97}
98
99#[derive(Debug)]
100pub struct Output {
101 pub status: ExitStatus,
102 pub stdout: Vec<u8>,
103 pub stderr: Vec<u8>,
104 pub truncated: bool,
105 pub duration: Duration,
106}
107
108pub(crate) fn tool_output<'a>(
112 program: &str,
113 args: impl IntoIterator<Item = &'a std::ffi::OsStr>,
114) -> Option<Vec<u8>> {
115 platform::program::tool_output(program, args)
116}
117
118pub(crate) struct Launcher {
122 pub env: BTreeMap<OsString, OsString>,
123}
124
125impl artifactize_tools::launch::Launcher for Launcher {
126 fn run<'a>(
127 &'a self,
128 launch: artifactize_tools::launch::Launch,
129 cancellation: &'a CancellationToken,
130 ) -> artifactize_tools::launch::Running<'a> {
131 use artifactize_tools::launch::{Finished, LaunchError};
132 Box::pin(async move {
133 let command = Command {
134 program: launch.program.into(),
135 args: launch.args,
136 cwd: launch.cwd,
137 env: self.env.clone(),
138 timeout: launch.timeout,
139 };
140 let ready = |_| async { Ok(()) };
141 match run_with_input_limit(
142 command,
143 None,
144 launch.output_limit,
145 cancellation.clone(),
146 ready,
147 )
148 .await
149 {
150 Ok(output) => Ok(Finished {
151 status: output.status,
152 stdout: output.stdout,
153 stderr: output.stderr,
154 truncated: output.truncated,
155 }),
156 Err(Error::Timeout) => Err(LaunchError::TimedOut),
157 Err(Error::Cancelled) => Err(LaunchError::Cancelled),
158 Err(error) => Err(LaunchError::Failed(
159 error
160 .argument_refusal()
161 .unwrap_or_else(|| error.to_string()),
162 )),
163 }
164 })
165 }
166}
167
168#[derive(Debug, Error)]
169pub enum Error {
170 #[error("process could not be spawned: {0}")]
171 Spawn(#[source] io::Error),
172 #[error("child registration failed: {0}")]
173 Registration(#[source] io::Error),
174 #[error("process supervision failed: {0}")]
175 Io(#[from] io::Error),
176 #[error("process cleanup could not be confirmed: {0}")]
177 Cleanup(#[source] io::Error),
178 #[error("process supervision task failed: {0}")]
179 Task(#[from] tokio::task::JoinError),
180 #[error("process timed out")]
181 Timeout,
182 #[error("process was cancelled")]
183 Cancelled,
184 #[error("process output pipes did not close after cleanup")]
185 OutputDidNotClose,
186}
187
188impl Error {
189 pub(crate) fn cleanup_confirmed(&self) -> bool {
192 match self {
193 Self::Cleanup(_) | Self::Task(_) => false,
194 Self::Spawn(_)
195 | Self::Registration(_)
196 | Self::Io(_)
197 | Self::Timeout
198 | Self::Cancelled
199 | Self::OutputDidNotClose => true,
200 }
201 }
202
203 pub(crate) fn argument_refusal(&self) -> Option<String> {
204 match self {
205 Self::Spawn(error) if error.kind() == io::ErrorKind::InvalidInput => Some(format!(
206 "Process arguments could not be passed safely: {error}"
207 )),
208 _ => None,
209 }
210 }
211}
212
213pub async fn run<F, R>(
217 command: Command,
218 cancellation: CancellationToken,
219 register: F,
220) -> Result<Output, Error>
221where
222 F: FnOnce(ChildIdentity) -> R + Send + 'static,
223 R: Future<Output = io::Result<()>> + Send + 'static,
224{
225 run_with_input(command, None, cancellation, register).await
226}
227
228pub(crate) async fn run_with_input<F, R>(
229 command: Command,
230 input: Option<Vec<u8>>,
231 cancellation: CancellationToken,
232 register: F,
233) -> Result<Output, Error>
234where
235 F: FnOnce(ChildIdentity) -> R + Send + 'static,
236 R: Future<Output = io::Result<()>> + Send + 'static,
237{
238 run_with_input_limit(command, input, OUTPUT_LIMIT, cancellation, register).await
239}
240
241pub(crate) async fn run_with_input_limit<F, R>(
242 command: Command,
243 input: Option<Vec<u8>>,
244 output_limit: usize,
245 cancellation: CancellationToken,
246 register: F,
247) -> Result<Output, Error>
248where
249 F: FnOnce(ChildIdentity) -> R + Send + 'static,
250 R: Future<Output = io::Result<()>> + Send + 'static,
251{
252 let cancellation = cancellation.child_token();
253 let _cancel_on_drop = cancellation.clone().drop_guard();
254 crate::task::joined(
255 tokio::spawn(run_inner(
256 command,
257 input,
258 output_limit,
259 cancellation,
260 register,
261 ))
262 .await,
263 )?
264}
265
266async fn run_inner<F, R>(
267 command: Command,
268 input: Option<Vec<u8>>,
269 output_limit: usize,
270 cancellation: CancellationToken,
271 register: F,
272) -> Result<Output, Error>
273where
274 F: FnOnce(ChildIdentity) -> R,
275 R: Future<Output = io::Result<()>>,
276{
277 let started = Instant::now();
278 let deadline = started + command.timeout;
279 if cancellation.is_cancelled() {
280 return Err(Error::Cancelled);
281 }
282 if command.timeout.is_zero() {
283 return Err(Error::Timeout);
284 }
285
286 let mut child = spawn_owned(
287 &command,
288 input.is_some(),
289 cancellation.clone(),
290 deadline,
291 register,
292 )
293 .await?;
294
295 let stdout = child.stdout().take().expect("stdout is piped");
296 let stderr = child.stderr().take().expect("stderr is piped");
297 let stdin = child.stdin().take();
298 let reaped = CancellationToken::new();
299 let feed = async {
300 if let (Some(mut stdin), Some(input)) = (stdin, input) {
301 tokio::select! {
302 _ = reaped.cancelled() => {},
303 _ = cancellation.cancelled() => {},
304 _ = sleep_until(deadline) => {},
305 result = stdin.write_all(&input) => {
306 if let Err(error) = result && error.kind() != io::ErrorKind::BrokenPipe {
308 return Err(Error::Io(error));
309 }
310 }
311 }
312 }
313 Ok(())
314 };
315 let wait = async {
316 let result = tokio::select! {
317 biased;
318 _ = cancellation.cancelled() => Err(Error::Cancelled),
319 _ = sleep_until(deadline) => Err(Error::Timeout),
320 status = child.wait() => status.map_err(Error::Io),
321 };
322 let cleanup = if result.is_err() {
323 terminate(&mut child).await
324 } else {
325 child.cleanup().await.map(|_| ())
327 };
328 reaped.cancel();
329 cleanup.map_err(Error::Cleanup)?;
330 result
331 };
332 let capture = async {
333 let read = async {
334 tokio::try_join!(capture(stdout, output_limit), capture(stderr, output_limit))
335 };
336 tokio::pin!(read);
337 tokio::select! {
338 result = &mut read => result.map_err(Error::Io),
339 _ = reaped.cancelled() => {
340 timeout(CLEANUP_GRACE, read).await
341 .map_err(|_| Error::OutputDidNotClose)?
342 .map_err(Error::Io)
343 }
344 }
345 };
346 let (status, output, input) = tokio::join!(wait, capture, feed);
347 let status = status?;
348 input?;
349 let ((stdout, stdout_truncated), (stderr, stderr_truncated)) = output?;
350 Ok(Output {
351 status,
352 stdout,
353 stderr,
354 truncated: stdout_truncated || stderr_truncated,
355 duration: started.elapsed(),
356 })
357}
358
359pub(crate) async fn spawn_owned<F, R>(
362 command: &Command,
363 input: bool,
364 cancellation: CancellationToken,
365 deadline: Instant,
366 register: F,
367) -> Result<Child, Error>
368where
369 F: FnOnce(ChildIdentity) -> R,
370 R: Future<Output = io::Result<()>>,
371{
372 let program = platform::program::resolve_with(&command.program, &command.cwd, |name| {
373 environment_value(&command.env, name).map(ToOwned::to_owned)
374 })
375 .map_err(Error::Spawn)?;
376 let mut child = tokio::process::Command::new(&program);
377 child
378 .args(&command.args)
379 .current_dir(&command.cwd)
380 .env_clear()
381 .envs(&command.env)
382 .stdin(if input { Stdio::piped() } else { Stdio::null() })
383 .stdout(Stdio::piped())
384 .stderr(Stdio::piped())
385 .kill_on_drop(true);
386 let (gate, mut spawning) = platform::spawn_gated(child).map_err(Error::Spawn)?;
387 let admission = tokio::select! {
388 biased;
389 _ = cancellation.cancelled() => Err(Error::Cancelled),
390 _ = sleep_until(deadline) => Err(Error::Timeout),
391 result = async {
392 let pid = gate.pid().await?;
393 let child = child_identity(pid).map_err(Error::Registration)?;
394 register(child).await.map_err(Error::Registration)?;
395 Ok::<_, Error>(())
396 } => result,
397 };
398 let admission = admission.and_then(|()| {
400 if cancellation.is_cancelled() {
401 Err(Error::Cancelled)
402 } else if Instant::now() >= deadline {
403 Err(Error::Timeout)
404 } else {
405 gate.admit().map_err(Error::Io)
406 }
407 });
408 drop(gate);
409 let spawned = crate::task::joined((&mut spawning).await)?;
410 let mut child = match spawned {
411 Ok(child) => child,
412 Err(error) => {
413 return Err(match admission {
414 Err(error @ (Error::Registration(_) | Error::Cancelled | Error::Timeout)) => error,
415 _ => Error::Spawn(error),
416 });
417 }
418 };
419 if let Err(error) = admission {
420 terminate(&mut child).await.map_err(Error::Cleanup)?;
421 return Err(error);
422 }
423
424 Ok(child)
425}
426
427fn environment_value<'a>(
428 environment: &'a BTreeMap<OsString, OsString>,
429 name: &str,
430) -> Option<&'a std::ffi::OsStr> {
431 crate::platform::environment::lookup(environment, name)
432}
433
434pub(crate) fn launch_detached(
436 program: &std::ffi::OsStr,
437 args: &[String],
438 cwd: &std::path::Path,
439) -> Result<(), Error> {
440 let program = platform::program::resolve(program, cwd).map_err(Error::Spawn)?;
441 let mut command = tokio::process::Command::new(program);
442 command
443 .args(args)
444 .current_dir(cwd)
445 .stdin(Stdio::null())
446 .stdout(Stdio::null())
447 .stderr(Stdio::null())
448 .kill_on_drop(false);
449 let mut child = platform::spawn_detached(command).map_err(Error::Spawn)?;
450 tokio::spawn(async move {
451 let _ = child.wait().await;
452 });
453 Ok(())
454}
455
456pub(crate) fn child_identity(pid: u32) -> io::Result<ChildIdentity> {
457 let start_time = platform::process_start_time(pid)?;
458 Ok(ChildIdentity {
459 pid: ProcessId::new(pid),
460 start_time,
461 })
462}
463
464pub(crate) fn is_alive(owner: Option<ChildIdentity>) -> io::Result<bool> {
467 let Some(owner) = owner else {
468 return Ok(false);
469 };
470 match platform::running_process_start_time(owner.pid.get()) {
471 Ok(start_time) => Ok(start_time == owner.start_time),
472 Err(error) if error.kind() == io::ErrorKind::NotFound => Ok(false),
473 Err(error) => Err(error),
474 }
475}
476
477async fn terminate(child: &mut Child) -> io::Result<()> {
478 let _ = child.interrupt();
479 tokio::select! {
480 _ = sleep(CLEANUP_GRACE) => {},
481 _ = child.wait() => {},
482 }
483 child.cleanup().await?;
484 Ok(())
485}
486
487async fn capture(mut pipe: impl AsyncRead + Unpin, limit: usize) -> io::Result<(Vec<u8>, bool)> {
488 let mut output = Vec::new();
489 let mut buffer = [0; CAPTURE_BUFFER_BYTES];
490 let mut truncated = false;
491 loop {
492 let count = pipe.read(&mut buffer).await?;
493 if count == 0 {
494 return Ok((output, truncated));
495 }
496 let retained = count.min(limit - output.len());
497 output.extend_from_slice(&buffer[..retained]);
498 truncated |= retained < count;
499 }
500}