eska_tool_runtime/
capture.rs1use crate::{
4 Component, Error, Operation, QueryPolicy,
5 process::OwnedChild,
6 query::{Stream, reader},
7};
8#[cfg(unix)]
9const TERMINATE: i32 = signal_hook::consts::signal::SIGTERM;
10#[cfg(not(unix))]
11const TERMINATE: i32 = 0;
12
13use std::{
14 process::{Command, Output, Stdio},
15 sync::mpsc,
16 thread,
17 time::Instant,
18};
19
20pub fn capture(
25 component: Component,
26 command: &mut Command,
27 scope: &Operation,
28 policy: QueryPolicy,
29 on_failure: impl FnOnce(),
30) -> Result<Output, Error> {
31 run(component, command, Some(scope), policy, on_failure)
32}
33
34pub fn run(
36 component: Component,
37 command: &mut Command,
38 scope: Option<&Operation>,
39 policy: QueryPolicy,
40 on_failure: impl FnOnce(),
41) -> Result<Output, Error> {
42 if scope.is_some_and(Operation::is_cancelled) {
43 return Err(Error::Cancelled);
44 }
45 let child = command
46 .stdin(Stdio::null())
47 .stdout(Stdio::piped())
48 .stderr(Stdio::piped())
49 .spawn()
50 .map_err(|source| Error::Process { component, source })?;
51 let mut child = OwnedChild(child);
52 let result = receive(component, &mut child, scope, policy);
53 if result.is_err() && !matches!(child.0.try_wait(), Ok(Some(_))) {
54 on_failure();
55 }
56 result
57}
58
59fn receive(
61 component: Component,
62 child: &mut OwnedChild,
63 scope: Option<&Operation>,
64 policy: QueryPolicy,
65) -> Result<Output, Error> {
66 let stdout = child.0.stdout.take().ok_or(Error::InvalidResponse)?;
67 let stderr = child.0.stderr.take().ok_or(Error::InvalidResponse)?;
68 let (sender, receiver) = mpsc::channel();
69 reader(
70 component,
71 stdout,
72 Stream::Stdout,
73 sender.clone(),
74 policy.output_limit,
75 )?;
76 reader(
77 component,
78 stderr,
79 Stream::Stderr,
80 sender,
81 policy.output_limit,
82 )?;
83 let started = Instant::now();
84 let mut stopping = None;
85 let mut status = None;
86 let mut stdout = None;
87 let mut stderr = None;
88 loop {
89 while let Ok((stream, result)) = receiver.try_recv() {
90 match stream {
91 Stream::Stdout => stdout = Some(result?),
92 Stream::Stderr => stderr = Some(result?),
93 }
94 }
95 let cancelled = scope.is_some_and(Operation::is_cancelled);
96 if stopping.is_none() && (cancelled || started.elapsed() >= policy.timeout) {
97 if policy.cancellation_grace.is_zero() {
98 return Err(reason(cancelled));
99 }
100 signal(
101 child,
102 scope
103 .and_then(Operation::received_signal)
104 .unwrap_or(TERMINATE),
105 )?;
106 stopping = Some((Instant::now(), cancelled));
107 }
108 if status.is_none() {
109 status = child
110 .0
111 .try_wait()
112 .map_err(|source| Error::Process { component, source })?;
113 }
114 if let Some((started, cancelled)) = stopping {
115 if status.is_some() || started.elapsed() >= policy.cancellation_grace {
116 return Err(reason(cancelled));
117 }
118 } else if let Some(status) = status
119 && stdout.is_some()
120 && stderr.is_some()
121 {
122 return Ok(Output {
123 status,
124 stdout: stdout.take().ok_or(Error::InvalidResponse)?,
125 stderr: stderr.take().ok_or(Error::InvalidResponse)?,
126 });
127 }
128 thread::sleep(std::time::Duration::from_millis(10));
129 }
130}
131
132const fn reason(cancelled: bool) -> Error {
134 if cancelled {
135 Error::Cancelled
136 } else {
137 Error::Timeout
138 }
139}
140
141#[cfg(unix)]
143fn signal(child: &OwnedChild, signal: i32) -> Result<(), Error> {
144 use nix::{
145 errno::Errno,
146 sys::signal::{Signal, kill},
147 unistd::Pid,
148 };
149 let pid =
150 i32::try_from(child.0.id()).map_err(|error| Error::Signal(std::io::Error::other(error)))?;
151 let signal = Signal::try_from(signal).map_err(|error| Error::Signal(error.into()))?;
152 match kill(Pid::from_raw(pid), signal) {
153 Ok(()) | Err(Errno::ESRCH) => Ok(()),
154 Err(error) => Err(Error::Signal(error.into())),
155 }
156}
157
158#[cfg(not(unix))]
160fn signal(child: &mut OwnedChild, _: i32) -> Result<(), Error> {
161 child.0.kill().map_err(Error::Signal)
162}