rightkit_child_process/
native_capture.rs1use crate::CapturedOutput;
4use std::{
5 fs::File,
6 io,
7 process::{ChildStderr, ChildStdout},
8 sync::{
9 atomic::{AtomicBool, Ordering},
10 Arc, Condvar, Mutex,
11 },
12 thread::{self, JoinHandle},
13 time::Duration,
14};
15
16#[cfg(unix)]
17#[path = "native_capture/unix.rs"]
18mod platform;
19#[cfg(windows)]
20#[path = "native_capture/windows.rs"]
21pub(crate) mod platform;
22
23const CHUNK: usize = 8 * 1024;
24const IDLE_WAIT: Duration = Duration::from_millis(20);
25
26#[derive(Default)]
27struct State {
28 output: CapturedOutput,
29 error: Option<io::Error>,
30 eof: bool,
31 finished: bool,
32}
33
34type Shared = (Mutex<State>, Condvar);
35
36#[derive(Clone, Copy, Debug, Eq, PartialEq)]
38pub(crate) enum Stream {
39 Stdout,
40 Stderr,
41}
42
43#[cfg(test)]
46pub(crate) fn inject_read_fault(stream: Stream, kind: io::ErrorKind) {
47 test_fault::arm(stream, kind);
48}
49
50pub(crate) type ChunkSink = Box<dyn FnMut(&[u8], &AtomicBool) + Send + 'static>;
54
55struct Done(Arc<Shared>);
56
57impl Drop for Done {
58 fn drop(&mut self) {
59 self.0 .0.lock().unwrap_or_else(|e| e.into_inner()).finished = true;
60 self.0 .1.notify_all();
61 }
62}
63
64pub struct NativeOutputCapture {
79 shared: Arc<Shared>,
80 stop: Arc<AtomicBool>,
81 handle: Option<JoinHandle<()>>,
82}
83
84impl NativeOutputCapture {
85 pub fn stdout(pipe: ChildStdout, max_bytes: usize) -> io::Result<Self> {
88 Self::start(platform::stdout(pipe), max_bytes, None, Stream::Stdout)
89 }
90
91 pub fn stderr(pipe: ChildStderr, max_bytes: usize) -> io::Result<Self> {
93 Self::start(platform::stderr(pipe), max_bytes, None, Stream::Stderr)
94 }
95
96 pub(crate) fn stdout_streaming(pipe: ChildStdout, sink: ChunkSink) -> io::Result<Self> {
99 Self::start(platform::stdout(pipe), 0, Some(sink), Stream::Stdout)
100 }
101
102 pub(crate) fn stderr_streaming(pipe: ChildStderr, sink: ChunkSink) -> io::Result<Self> {
104 Self::start(platform::stderr(pipe), 0, Some(sink), Stream::Stderr)
105 }
106
107 fn start(
108 pipe: File,
109 max_bytes: usize,
110 mut sink: Option<ChunkSink>,
111 stream: Stream,
112 ) -> io::Result<Self> {
113 let prepared = platform::prepare(pipe)?;
114 #[cfg(test)]
117 let mut fault = test_fault::take(stream);
118 #[cfg(not(test))]
119 let _ = stream;
120 let shared: Arc<Shared> = Arc::default();
121 let stop = Arc::new(AtomicBool::new(false));
122 let state = Arc::clone(&shared);
123 let cancelled = Arc::clone(&stop);
124 let handle = thread::Builder::new()
125 .name("rightkit-native-output-capture".into())
126 .spawn(move || {
127 let _done = Done(Arc::clone(&state));
128 let mut pipe = platform::reader(prepared);
129 let mut buffer = [0; CHUNK];
130 while !cancelled.load(Ordering::Acquire) {
131 #[cfg(test)]
132 let read = match fault.take() {
133 Some(kind) => Err(io::Error::new(kind, "injected pipe read failure")),
134 None => platform::read(&mut pipe, &mut buffer),
135 };
136 #[cfg(not(test))]
137 let read = platform::read(&mut pipe, &mut buffer);
138 match read {
139 Ok(0) => {
140 state.0.lock().unwrap_or_else(|e| e.into_inner()).eof = true;
141 break;
142 }
143 Ok(count) => {
144 if let Some(sink) = sink.as_mut() {
145 sink(&buffer[..count], &cancelled);
146 }
147 state
148 .0
149 .lock()
150 .unwrap_or_else(|e| e.into_inner())
151 .output
152 .push(&buffer[..count], max_bytes);
153 }
154 Err(error) if error.kind() == io::ErrorKind::WouldBlock => {
155 thread::park_timeout(IDLE_WAIT);
156 }
157 Err(error) if error.kind() == io::ErrorKind::Interrupted => {}
158 Err(error) => {
159 state.0.lock().unwrap_or_else(|e| e.into_inner()).error = Some(error);
160 break;
161 }
162 }
163 }
164 if let Err(error) = platform::shutdown(&mut pipe) {
165 let mut state = state.0.lock().unwrap_or_else(|e| e.into_inner());
166 if state.error.is_none() {
167 state.error = Some(error);
168 }
169 }
170 drop(pipe);
172 })?;
173 Ok(Self {
174 shared,
175 stop,
176 handle: Some(handle),
177 })
178 }
179
180 pub fn snapshot(&self) -> CapturedOutput {
182 self.shared
183 .0
184 .lock()
185 .unwrap_or_else(|e| e.into_inner())
186 .output
187 .clone()
188 }
189
190 pub fn is_complete(&self) -> bool {
193 self.shared.0.lock().unwrap_or_else(|e| e.into_inner()).eof
194 }
195
196 pub fn take_error(&self) -> Option<io::Error> {
198 self.shared
199 .0
200 .lock()
201 .unwrap_or_else(|e| e.into_inner())
202 .error
203 .take()
204 }
205
206 pub fn finish_within(&mut self, timeout: Duration) -> CapturedOutput {
210 {
211 let state = self.shared.0.lock().unwrap_or_else(|e| e.into_inner());
212 drop(
213 self.shared
214 .1
215 .wait_timeout_while(state, timeout, |state| !state.finished)
216 .unwrap_or_else(|e| e.into_inner()),
217 );
218 }
219 self.stop_and_join();
220 self.snapshot()
221 }
222
223 pub fn cancel(&mut self) -> CapturedOutput {
225 self.stop_and_join();
226 self.snapshot()
227 }
228
229 fn stop_and_join(&mut self) {
230 self.stop.store(true, Ordering::Release);
231 if let Some(handle) = self.handle.take() {
232 handle.thread().unpark();
233 if handle.join().is_err() {
234 self.shared
235 .0
236 .lock()
237 .unwrap_or_else(|e| e.into_inner())
238 .error = Some(io::Error::other("native pipe reader panicked"));
239 }
240 }
241 }
242}
243
244impl Drop for NativeOutputCapture {
245 fn drop(&mut self) {
246 self.stop_and_join();
247 }
248}
249
250#[cfg(test)]
251pub(crate) mod test_fault {
252 use super::Stream;
253 use std::{cell::Cell, io};
254
255 thread_local! {
256 static ARMED: Cell<[Option<io::ErrorKind>; 2]> = const { Cell::new([None, None]) };
259 }
260
261 pub(crate) fn arm(stream: Stream, kind: io::ErrorKind) {
262 ARMED.with(|armed| {
263 let mut slots = armed.get();
264 slots[stream as usize] = Some(kind);
265 armed.set(slots);
266 });
267 }
268
269 pub(super) fn take(stream: Stream) -> Option<io::ErrorKind> {
270 ARMED.with(|armed| {
271 let mut slots = armed.get();
272 let kind = slots[stream as usize].take();
273 armed.set(slots);
274 kind
275 })
276 }
277}