rightkit_child_process/
capture.rs1use std::io::{self, Read};
12use std::sync::{Arc, Condvar, Mutex};
13use std::thread::{self, JoinHandle};
14use std::time::Duration;
15
16const CHUNK: usize = 8 * 1024;
17
18#[derive(Clone, Debug, Default, Eq, PartialEq)]
20pub struct CapturedOutput {
21 pub bytes: Vec<u8>,
23 pub truncated: bool,
25 pub total_bytes: u64,
27}
28
29impl CapturedOutput {
30 pub(crate) fn push(&mut self, chunk: &[u8], max_bytes: usize) {
31 self.total_bytes = self.total_bytes.saturating_add(chunk.len() as u64);
32 let room = max_bytes.saturating_sub(self.bytes.len());
33 let keep = room.min(chunk.len());
34 self.bytes.extend_from_slice(&chunk[..keep]);
35 if keep < chunk.len() {
36 self.truncated = true;
37 }
38 }
39}
40
41pub fn read_bounded<R: Read>(mut reader: R, max_bytes: usize) -> io::Result<CapturedOutput> {
44 let mut captured = CapturedOutput::default();
45 let mut buffer = [0u8; CHUNK];
46 loop {
47 match reader.read(&mut buffer) {
48 Ok(0) => return Ok(captured),
49 Ok(count) => captured.push(&buffer[..count], max_bytes),
50 Err(error) if error.kind() == io::ErrorKind::Interrupted => {}
51 Err(error) => return Err(error),
52 }
53 }
54}
55
56type Done = (Mutex<bool>, Condvar);
57
58struct DoneGuard(Arc<Done>);
60
61impl Drop for DoneGuard {
62 fn drop(&mut self) {
63 *self.0 .0.lock().unwrap_or_else(|e| e.into_inner()) = true;
64 self.0 .1.notify_all();
65 }
66}
67
68pub struct OutputCapture {
71 data: Arc<Mutex<CapturedOutput>>,
72 error: Arc<Mutex<Option<io::Error>>>,
74 handle: Option<JoinHandle<()>>,
75 done: Arc<Done>,
76}
77
78impl OutputCapture {
79 pub fn capture<R: Read + Send + 'static>(reader: R, max_bytes: usize) -> Self {
83 Self::try_capture(reader, max_bytes).unwrap_or_else(|_| Self {
84 data: Arc::default(),
85 error: Arc::default(),
86 handle: None,
87 done: Arc::new((Mutex::new(true), Condvar::new())),
88 })
89 }
90
91 pub fn try_capture<R: Read + Send + 'static>(reader: R, max_bytes: usize) -> io::Result<Self> {
94 let data: Arc<Mutex<CapturedOutput>> = Arc::default();
95 let error: Arc<Mutex<Option<io::Error>>> = Arc::default();
96 let done: Arc<Done> = Arc::new((Mutex::new(false), Condvar::new()));
97 let guard = DoneGuard(Arc::clone(&done));
98 let shared = Arc::clone(&data);
99 let failure = Arc::clone(&error);
100 let handle = thread::Builder::new()
101 .name("rightkit-output-capture".into())
102 .spawn(move || {
103 let _guard = guard;
104 let mut reader = reader;
105 let mut buffer = [0u8; CHUNK];
106 loop {
107 match reader.read(&mut buffer) {
108 Ok(0) => return,
109 Ok(count) => shared
110 .lock()
111 .unwrap_or_else(|e| e.into_inner())
112 .push(&buffer[..count], max_bytes),
113 Err(e) if e.kind() == io::ErrorKind::Interrupted => {}
114 Err(e) => {
115 *failure.lock().unwrap_or_else(|e| e.into_inner()) = Some(e);
116 return;
117 }
118 }
119 }
120 })?;
121 Ok(Self {
122 data,
123 error,
124 handle: Some(handle),
125 done,
126 })
127 }
128
129 pub fn snapshot(&self) -> CapturedOutput {
131 self.data.lock().unwrap_or_else(|e| e.into_inner()).clone()
132 }
133
134 pub fn take_error(&self) -> Option<io::Error> {
137 self.error.lock().unwrap_or_else(|e| e.into_inner()).take()
138 }
139
140 pub fn finish(&mut self) -> CapturedOutput {
143 if let Some(handle) = self.handle.take() {
144 let _ = handle.join();
145 }
146 self.snapshot()
147 }
148
149 pub fn finish_within(&mut self, timeout: Duration) -> CapturedOutput {
153 {
154 let guard = self.done.0.lock().unwrap_or_else(|e| e.into_inner());
155 let _ = self
156 .done
157 .1
158 .wait_timeout_while(guard, timeout, |over| !*over)
159 .unwrap_or_else(|e| e.into_inner());
160 }
161 self.snapshot()
162 }
163
164 pub fn is_finished(&self) -> bool {
166 *self.done.0.lock().unwrap_or_else(|e| e.into_inner())
167 }
168}
169
170#[cfg(test)]
171mod tests {
172 use super::*;
173 use std::io::Cursor;
174
175 #[test]
176 fn keeps_bytes_exactly_including_trailing_newline_and_non_utf8() {
177 let input: Vec<u8> = vec![b'{', b'}', b'\n', 0xff, 0x00, b'\r', b'\n'];
178 let out = read_bounded(Cursor::new(input.clone()), 1024).unwrap();
179 assert_eq!(out.bytes, input);
180 assert!(!out.truncated);
181 assert_eq!(out.total_bytes, input.len() as u64);
182 }
183
184 #[test]
185 fn keeps_the_first_max_bytes_and_counts_the_rest() {
186 let input = vec![7u8; CHUNK * 3 + 5];
187 let out = read_bounded(Cursor::new(input), 10).unwrap();
188 assert_eq!(out.bytes, vec![7u8; 10]);
189 assert!(out.truncated);
190 assert_eq!(out.total_bytes, (CHUNK * 3 + 5) as u64);
191 }
192
193 #[test]
194 fn exactly_the_cap_is_not_truncated() {
195 let out = read_bounded(Cursor::new(vec![1u8; 64]), 64).unwrap();
196 assert_eq!(out.bytes.len(), 64);
197 assert!(!out.truncated);
198 let zero = read_bounded(Cursor::new(vec![1u8; 3]), 0).unwrap();
199 assert!(zero.bytes.is_empty() && zero.truncated);
200 }
201
202 #[test]
203 fn a_read_error_is_returned_by_the_blocking_reader() {
204 struct Bad;
205 impl Read for Bad {
206 fn read(&mut self, _: &mut [u8]) -> io::Result<usize> {
207 Err(io::Error::other("boom"))
208 }
209 }
210 assert!(read_bounded(Bad, 8).is_err());
211 }
212
213 #[test]
214 fn the_background_capture_matches_the_blocking_reader() {
215 let input: Vec<u8> = (0..=255u8).cycle().take(CHUNK * 2 + 17).collect();
216 let mut capture = OutputCapture::capture(Cursor::new(input.clone()), 1000);
217 let out = capture.finish();
218 assert_eq!(out.bytes, input[..1000].to_vec());
219 assert!(out.truncated);
220 assert_eq!(out.total_bytes, input.len() as u64);
221 assert!(capture.is_finished());
222 assert!(capture.take_error().is_none());
223 }
224
225 #[test]
226 fn the_background_capture_records_a_read_error() {
227 struct Bad(bool);
228 impl Read for Bad {
229 fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
230 if self.0 {
231 return Err(io::Error::other("boom"));
232 }
233 self.0 = true;
234 buf[..2].copy_from_slice(b"ok");
235 Ok(2)
236 }
237 }
238 let mut capture = OutputCapture::capture(Bad(false), 8);
239 let out = capture.finish();
240 assert_eq!(out.bytes, b"ok");
241 assert!(capture.take_error().is_some());
242 }
243
244 #[test]
245 fn finish_within_returns_early_data_while_the_stream_stays_open() {
246 let (reader, mut writer) = std::io::pipe().expect("pipe");
247 use std::io::Write;
248 writer.write_all(b"partial").unwrap();
249 let mut capture = OutputCapture::capture(reader, 64);
250 let out = capture.finish_within(Duration::from_millis(300));
251 assert!(!capture.is_finished());
252 assert!(out.bytes.is_empty() || out.bytes == b"partial");
253 drop(writer);
254 let out = capture.finish();
255 assert_eq!(out.bytes, b"partial");
256 assert!(!out.truncated);
257 }
258}