use crate::CapturedOutput;
use std::{
fs::File,
io,
process::{ChildStderr, ChildStdout},
sync::{
atomic::{AtomicBool, Ordering},
Arc, Condvar, Mutex,
},
thread::{self, JoinHandle},
time::Duration,
};
#[cfg(unix)]
#[path = "native_capture/unix.rs"]
mod platform;
#[cfg(windows)]
#[path = "native_capture/windows.rs"]
pub(crate) mod platform;
const CHUNK: usize = 8 * 1024;
const IDLE_WAIT: Duration = Duration::from_millis(20);
#[derive(Default)]
struct State {
output: CapturedOutput,
error: Option<io::Error>,
eof: bool,
finished: bool,
}
type Shared = (Mutex<State>, Condvar);
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub(crate) enum Stream {
Stdout,
Stderr,
}
#[cfg(test)]
pub(crate) fn inject_read_fault(stream: Stream, kind: io::ErrorKind) {
test_fault::arm(stream, kind);
}
pub(crate) type ChunkSink = Box<dyn FnMut(&[u8], &AtomicBool) + Send + 'static>;
struct Done(Arc<Shared>);
impl Drop for Done {
fn drop(&mut self) {
self.0 .0.lock().unwrap_or_else(|e| e.into_inner()).finished = true;
self.0 .1.notify_all();
}
}
pub struct NativeOutputCapture {
shared: Arc<Shared>,
stop: Arc<AtomicBool>,
handle: Option<JoinHandle<()>>,
}
impl NativeOutputCapture {
pub fn stdout(pipe: ChildStdout, max_bytes: usize) -> io::Result<Self> {
Self::start(platform::stdout(pipe), max_bytes, None, Stream::Stdout)
}
pub fn stderr(pipe: ChildStderr, max_bytes: usize) -> io::Result<Self> {
Self::start(platform::stderr(pipe), max_bytes, None, Stream::Stderr)
}
pub(crate) fn stdout_streaming(pipe: ChildStdout, sink: ChunkSink) -> io::Result<Self> {
Self::start(platform::stdout(pipe), 0, Some(sink), Stream::Stdout)
}
pub(crate) fn stderr_streaming(pipe: ChildStderr, sink: ChunkSink) -> io::Result<Self> {
Self::start(platform::stderr(pipe), 0, Some(sink), Stream::Stderr)
}
fn start(
pipe: File,
max_bytes: usize,
mut sink: Option<ChunkSink>,
stream: Stream,
) -> io::Result<Self> {
let prepared = platform::prepare(pipe)?;
#[cfg(test)]
let mut fault = test_fault::take(stream);
#[cfg(not(test))]
let _ = stream;
let shared: Arc<Shared> = Arc::default();
let stop = Arc::new(AtomicBool::new(false));
let state = Arc::clone(&shared);
let cancelled = Arc::clone(&stop);
let handle = thread::Builder::new()
.name("rightkit-native-output-capture".into())
.spawn(move || {
let _done = Done(Arc::clone(&state));
let mut pipe = platform::reader(prepared);
let mut buffer = [0; CHUNK];
while !cancelled.load(Ordering::Acquire) {
#[cfg(test)]
let read = match fault.take() {
Some(kind) => Err(io::Error::new(kind, "injected pipe read failure")),
None => platform::read(&mut pipe, &mut buffer),
};
#[cfg(not(test))]
let read = platform::read(&mut pipe, &mut buffer);
match read {
Ok(0) => {
state.0.lock().unwrap_or_else(|e| e.into_inner()).eof = true;
break;
}
Ok(count) => {
if let Some(sink) = sink.as_mut() {
sink(&buffer[..count], &cancelled);
}
state
.0
.lock()
.unwrap_or_else(|e| e.into_inner())
.output
.push(&buffer[..count], max_bytes);
}
Err(error) if error.kind() == io::ErrorKind::WouldBlock => {
thread::park_timeout(IDLE_WAIT);
}
Err(error) if error.kind() == io::ErrorKind::Interrupted => {}
Err(error) => {
state.0.lock().unwrap_or_else(|e| e.into_inner()).error = Some(error);
break;
}
}
}
if let Err(error) = platform::shutdown(&mut pipe) {
let mut state = state.0.lock().unwrap_or_else(|e| e.into_inner());
if state.error.is_none() {
state.error = Some(error);
}
}
drop(pipe);
})?;
Ok(Self {
shared,
stop,
handle: Some(handle),
})
}
pub fn snapshot(&self) -> CapturedOutput {
self.shared
.0
.lock()
.unwrap_or_else(|e| e.into_inner())
.output
.clone()
}
pub fn is_complete(&self) -> bool {
self.shared.0.lock().unwrap_or_else(|e| e.into_inner()).eof
}
pub fn take_error(&self) -> Option<io::Error> {
self.shared
.0
.lock()
.unwrap_or_else(|e| e.into_inner())
.error
.take()
}
pub fn finish_within(&mut self, timeout: Duration) -> CapturedOutput {
{
let state = self.shared.0.lock().unwrap_or_else(|e| e.into_inner());
drop(
self.shared
.1
.wait_timeout_while(state, timeout, |state| !state.finished)
.unwrap_or_else(|e| e.into_inner()),
);
}
self.stop_and_join();
self.snapshot()
}
pub fn cancel(&mut self) -> CapturedOutput {
self.stop_and_join();
self.snapshot()
}
fn stop_and_join(&mut self) {
self.stop.store(true, Ordering::Release);
if let Some(handle) = self.handle.take() {
handle.thread().unpark();
if handle.join().is_err() {
self.shared
.0
.lock()
.unwrap_or_else(|e| e.into_inner())
.error = Some(io::Error::other("native pipe reader panicked"));
}
}
}
}
impl Drop for NativeOutputCapture {
fn drop(&mut self) {
self.stop_and_join();
}
}
#[cfg(test)]
pub(crate) mod test_fault {
use super::Stream;
use std::{cell::Cell, io};
thread_local! {
static ARMED: Cell<[Option<io::ErrorKind>; 2]> = const { Cell::new([None, None]) };
}
pub(crate) fn arm(stream: Stream, kind: io::ErrorKind) {
ARMED.with(|armed| {
let mut slots = armed.get();
slots[stream as usize] = Some(kind);
armed.set(slots);
});
}
pub(super) fn take(stream: Stream) -> Option<io::ErrorKind> {
ARMED.with(|armed| {
let mut slots = armed.get();
let kind = slots[stream as usize].take();
armed.set(slots);
kind
})
}
}