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 cleanup_failed: bool,
31 eof: bool,
32 finished: bool,
33}
34
35type Shared = (Mutex<State>, Condvar);
36
37#[derive(Clone, Copy, Debug, Eq, PartialEq)]
39pub(crate) enum Stream {
40 Stdout,
41 Stderr,
42}
43
44#[cfg(test)]
47pub(crate) fn inject_read_fault(stream: Stream, kind: io::ErrorKind) {
48 test_fault::arm(stream, kind);
49}
50
51pub(crate) type ChunkSink = Box<dyn FnMut(&[u8], &AtomicBool) + Send + 'static>;
55
56struct Done(Arc<Shared>);
57
58impl Drop for Done {
59 fn drop(&mut self) {
60 self.0 .0.lock().unwrap_or_else(|e| e.into_inner()).finished = true;
61 self.0 .1.notify_all();
62 }
63}
64
65pub struct NativeOutputCapture {
80 shared: Arc<Shared>,
81 stop: Arc<AtomicBool>,
82 handle: Option<JoinHandle<()>>,
83}
84
85impl NativeOutputCapture {
86 pub fn stdout(pipe: ChildStdout, max_bytes: usize) -> io::Result<Self> {
89 Self::start(platform::stdout(pipe), max_bytes, None, Stream::Stdout)
90 }
91
92 pub fn stderr(pipe: ChildStderr, max_bytes: usize) -> io::Result<Self> {
94 Self::start(platform::stderr(pipe), max_bytes, None, Stream::Stderr)
95 }
96
97 pub(crate) fn stdout_streaming(pipe: ChildStdout, sink: ChunkSink) -> io::Result<Self> {
100 Self::start(platform::stdout(pipe), 0, Some(sink), Stream::Stdout)
101 }
102
103 pub(crate) fn stderr_streaming(pipe: ChildStderr, sink: ChunkSink) -> io::Result<Self> {
105 Self::start(platform::stderr(pipe), 0, Some(sink), Stream::Stderr)
106 }
107
108 fn start(
109 pipe: File,
110 max_bytes: usize,
111 mut sink: Option<ChunkSink>,
112 stream: Stream,
113 ) -> io::Result<Self> {
114 let prepared = platform::prepare(pipe)?;
115 #[cfg(test)]
118 let mut fault = test_fault::take(stream);
119 #[cfg(not(test))]
120 let _ = stream;
121 let shared: Arc<Shared> = Arc::default();
122 let stop = Arc::new(AtomicBool::new(false));
123 let state = Arc::clone(&shared);
124 let cancelled = Arc::clone(&stop);
125 let handle = thread::Builder::new()
126 .name("rightkit-native-output-capture".into())
127 .spawn(move || {
128 let _done = Done(Arc::clone(&state));
129 let mut pipe = platform::reader(prepared);
130 let mut buffer = [0; CHUNK];
131 while !cancelled.load(Ordering::Acquire) {
132 #[cfg(test)]
133 let read = match fault.take() {
134 Some(kind) => Err(io::Error::new(kind, "injected pipe read failure")),
135 None => platform::read(&mut pipe, &mut buffer),
136 };
137 #[cfg(not(test))]
138 let read = platform::read(&mut pipe, &mut buffer);
139 match read {
140 Ok(0) => {
141 state.0.lock().unwrap_or_else(|e| e.into_inner()).eof = true;
142 break;
143 }
144 Ok(count) => {
145 if let Some(sink) = sink.as_mut() {
146 sink(&buffer[..count], &cancelled);
147 }
148 state
149 .0
150 .lock()
151 .unwrap_or_else(|e| e.into_inner())
152 .output
153 .push(&buffer[..count], max_bytes);
154 }
155 Err(error) if error.kind() == io::ErrorKind::WouldBlock => {
156 thread::park_timeout(IDLE_WAIT);
157 }
158 Err(error) if error.kind() == io::ErrorKind::Interrupted => {}
159 Err(error) => {
160 state.0.lock().unwrap_or_else(|e| e.into_inner()).error = Some(error);
161 break;
162 }
163 }
164 }
165 if let Err(error) = platform::shutdown(&mut pipe) {
166 let mut state = state.0.lock().unwrap_or_else(|e| e.into_inner());
167 if state.error.is_none() {
168 state.error = Some(error);
169 state.cleanup_failed = true;
170 }
171 }
172 drop(pipe);
174 })?;
175 Ok(Self {
176 shared,
177 stop,
178 handle: Some(handle),
179 })
180 }
181
182 pub fn snapshot(&self) -> CapturedOutput {
184 self.shared
185 .0
186 .lock()
187 .unwrap_or_else(|e| e.into_inner())
188 .output
189 .clone()
190 }
191
192 pub fn is_complete(&self) -> bool {
195 self.shared.0.lock().unwrap_or_else(|e| e.into_inner()).eof
196 }
197
198 pub fn take_error(&self) -> Option<io::Error> {
200 self.shared
201 .0
202 .lock()
203 .unwrap_or_else(|e| e.into_inner())
204 .error
205 .take()
206 }
207
208 pub(crate) fn cleanup_failed(&self) -> bool {
209 self.shared
210 .0
211 .lock()
212 .unwrap_or_else(|e| e.into_inner())
213 .cleanup_failed
214 }
215
216 pub fn finish_within(&mut self, timeout: Duration) -> CapturedOutput {
220 {
221 let state = self.shared.0.lock().unwrap_or_else(|e| e.into_inner());
222 drop(
223 self.shared
224 .1
225 .wait_timeout_while(state, timeout, |state| !state.finished)
226 .unwrap_or_else(|e| e.into_inner()),
227 );
228 }
229 self.stop_and_join();
230 self.snapshot()
231 }
232
233 pub fn cancel(&mut self) -> CapturedOutput {
235 self.stop_and_join();
236 self.snapshot()
237 }
238
239 fn stop_and_join(&mut self) {
240 self.stop.store(true, Ordering::Release);
241 if let Some(handle) = self.handle.take() {
242 handle.thread().unpark();
243 if handle.join().is_err() {
244 let mut state = self.shared.0.lock().unwrap_or_else(|e| e.into_inner());
245 state.error = Some(io::Error::other("native pipe reader panicked"));
246 state.cleanup_failed = true;
247 }
248 }
249 }
250}
251
252impl Drop for NativeOutputCapture {
253 fn drop(&mut self) {
254 self.stop_and_join();
255 }
256}
257
258#[cfg(test)]
259pub(crate) mod test_fault {
260 use super::Stream;
261 use std::{cell::Cell, io};
262
263 thread_local! {
264 static ARMED: Cell<[Option<io::ErrorKind>; 2]> = const { Cell::new([None, None]) };
267 }
268
269 pub(crate) fn arm(stream: Stream, kind: io::ErrorKind) {
270 ARMED.with(|armed| {
271 let mut slots = armed.get();
272 slots[stream as usize] = Some(kind);
273 armed.set(slots);
274 });
275 }
276
277 pub(super) fn take(stream: Stream) -> Option<io::ErrorKind> {
278 ARMED.with(|armed| {
279 let mut slots = armed.get();
280 let kind = slots[stream as usize].take();
281 armed.set(slots);
282 kind
283 })
284 }
285}