mod failure;
mod streaming;
pub(crate) use failure::{failure, stop_after_failure, terminate_runner};
pub use failure::{LeaderTermination, RunnerFailure, RunnerPhase};
pub use streaming::{run_streaming, StreamOptions, StreamOutput};
use crate::{CapturedOutput, NativeOutputCapture, OwnedCommand};
use std::{
ffi::OsStr,
io,
process::{ChildStdin, ExitStatus},
sync::{
atomic::{AtomicBool, Ordering},
mpsc, Arc,
},
thread::JoinHandle,
time::{Duration, Instant},
};
use zeroize::Zeroizing;
const DRAIN_WAIT: Duration = Duration::from_secs(2);
pub(crate) const RUNNER_REAP_WAIT: Duration = Duration::from_secs(5);
const STDIN_WAIT: Duration = Duration::from_millis(500);
const STDIN_CANCEL_WAIT: Duration = Duration::from_secs(2);
const CANCEL_RETRY: Duration = Duration::from_millis(25);
#[cfg(windows)]
const WRITE_CHUNK: usize = 64 * 1024;
#[cfg(unix)]
const WRITE_POLL: Duration = Duration::from_millis(50);
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct RunOptions {
pub timeout: Duration,
pub stdout_cap: usize,
pub stderr_cap: usize,
pub no_window: bool,
}
impl RunOptions {
pub fn new(timeout: Duration) -> Self {
Self {
timeout,
stdout_cap: 1024 * 1024,
stderr_cap: 1024 * 1024,
no_window: true,
}
}
pub fn stdout_cap(mut self, bytes: usize) -> Self {
self.stdout_cap = bytes;
self
}
pub fn stderr_cap(mut self, bytes: usize) -> Self {
self.stderr_cap = bytes;
self
}
pub fn no_window(mut self, hidden: bool) -> Self {
self.no_window = hidden;
self
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
#[non_exhaustive]
pub struct CaptureOptions {
pub timeout: Option<Duration>,
pub stdout_cap: usize,
pub stderr_cap: usize,
pub no_window: bool,
pub drain_grace: Duration,
}
impl Default for CaptureOptions {
fn default() -> Self {
Self {
timeout: None,
stdout_cap: 1024 * 1024,
stderr_cap: 1024 * 1024,
no_window: true,
drain_grace: DRAIN_WAIT,
}
}
}
impl CaptureOptions {
pub fn new() -> Self {
Self::default()
}
pub fn timeout(mut self, value: Duration) -> Self {
self.timeout = Some(value);
self
}
pub fn stdout_cap(mut self, value: usize) -> Self {
self.stdout_cap = value;
self
}
pub fn stderr_cap(mut self, value: usize) -> Self {
self.stderr_cap = value;
self
}
pub fn no_window(mut self, value: bool) -> Self {
self.no_window = value;
self
}
pub fn drain_grace(mut self, value: Duration) -> Self {
self.drain_grace = value;
self
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct RunOutput {
pub status: ExitStatus,
pub timed_out: bool,
pub pid: u32,
pub usage: Option<crate::ChildUsage>,
pub stdout: CapturedOutput,
pub stderr: CapturedOutput,
}
pub fn run_with_stdin<I, S>(
program: impl AsRef<OsStr>,
args: I,
input: &[u8],
options: &RunOptions,
) -> io::Result<RunOutput>
where
I: IntoIterator<Item = S>,
S: AsRef<OsStr>,
{
run_inner(program, args, Some(Zeroizing::new(input.to_vec())), options)
}
pub fn run<I, S>(program: impl AsRef<OsStr>, args: I, options: &RunOptions) -> io::Result<RunOutput>
where
I: IntoIterator<Item = S>,
S: AsRef<OsStr>,
{
run_inner(program, args, None, options)
}
fn run_inner<I, S>(
program: impl AsRef<OsStr>,
args: I,
input: Option<Zeroizing<Vec<u8>>>,
options: &RunOptions,
) -> io::Result<RunOutput>
where
I: IntoIterator<Item = S>,
S: AsRef<OsStr>,
{
let mut command = OwnedCommand::new(program);
command.command_mut().args(args);
run_owned(command, input, options)
}
pub fn run_command(
command: OwnedCommand,
input: Option<&[u8]>,
options: &RunOptions,
) -> io::Result<RunOutput> {
run_owned(
command,
input.map(|bytes| Zeroizing::new(bytes.to_vec())),
options,
)
}
pub fn status_with_timeout(
command: OwnedCommand,
timeout: Duration,
) -> io::Result<Option<ExitStatus>> {
let mut child = command.spawn()?;
match child.wait_timeout(timeout)? {
Some(status) => Ok(Some(status)),
None => {
child.terminate_tree_bounded(RUNNER_REAP_WAIT)?;
Ok(None)
}
}
}
fn run_owned(
command: OwnedCommand,
input: Option<Zeroizing<Vec<u8>>>,
options: &RunOptions,
) -> io::Result<RunOutput> {
run_captured(
command,
input,
&CaptureOptions {
timeout: Some(options.timeout),
stdout_cap: options.stdout_cap,
stderr_cap: options.stderr_cap,
no_window: options.no_window,
drain_grace: DRAIN_WAIT,
},
)
}
pub fn run_command_capture(
command: OwnedCommand,
input: Option<&[u8]>,
options: &CaptureOptions,
) -> io::Result<RunOutput> {
run_captured(
command,
input.map(|bytes| Zeroizing::new(bytes.to_vec())),
options,
)
}
pub fn run_command_capture_secret_stdin(
command: OwnedCommand,
input: Zeroizing<Vec<u8>>,
options: &CaptureOptions,
) -> io::Result<RunOutput> {
run_captured(command, Some(input), options)
}
fn run_captured(
mut command: OwnedCommand,
input: Option<Zeroizing<Vec<u8>>>,
options: &CaptureOptions,
) -> io::Result<RunOutput> {
command.pipe_stdio(input.is_some());
command.bound_runner_cleanup(RUNNER_REAP_WAIT);
if options.no_window {
command.windows_hide();
} else {
command.windows_visible();
}
let mut child = command
.spawn()
.map_err(|error| failure(RunnerPhase::Spawn, LeaderTermination::NotStarted, error))?;
let pid = child.id();
let writer = match (input, child.take_stdin()) {
(Some(bytes), Some(stdin)) => Some(
StdinWriter::spawn(stdin, bytes)
.map_err(|error| stop_after_failure(&mut child, RunnerPhase::Setup, error))?,
),
_ => None,
};
let mut stdout = child
.take_stdout()
.map(|pipe| NativeOutputCapture::stdout(pipe, options.stdout_cap))
.transpose()
.map_err(|error| stop_after_failure(&mut child, RunnerPhase::Setup, error))?;
let mut stderr = child
.take_stderr()
.map(|pipe| NativeOutputCapture::stderr(pipe, options.stderr_cap))
.transpose()
.map_err(|error| stop_after_failure(&mut child, RunnerPhase::Setup, error))?;
let (status, timed_out) = match options.timeout {
Some(timeout) => match child
.wait_exit_then_sweep(timeout)
.map_err(|error| stop_after_failure(&mut child, RunnerPhase::Wait, error))?
{
Some(status) => (status, false),
None => (terminate_runner(&mut child, RUNNER_REAP_WAIT)?, true),
},
None => loop {
if let Some(status) = child
.wait_exit_then_sweep(Duration::from_millis(100))
.map_err(|error| stop_after_failure(&mut child, RunnerPhase::Wait, error))?
{
break (status, false);
}
},
};
let stdout_output = stdout
.as_mut()
.map_or_else(CapturedOutput::default, |capture| {
capture.finish_within(options.drain_grace)
});
let stderr_output = stderr
.as_mut()
.map_or_else(CapturedOutput::default, |capture| {
capture.finish_within(options.drain_grace)
});
let writer_result = writer.map(StdinWriter::finish).transpose();
let stdout_failure = capture_failure("stdout", stdout.as_ref());
let stderr_failure = capture_failure("stderr", stderr.as_ref());
stdout_failure?;
stderr_failure?;
writer_result
.map_err(|error| failure(RunnerPhase::Stdin, LeaderTermination::Confirmed, error))?;
let usage = child.resource_usage().ok().flatten();
Ok(RunOutput {
status,
timed_out,
pid,
usage,
stdout: stdout_output,
stderr: stderr_output,
})
}
pub(crate) fn capture_failure(
stream: &str,
capture: Option<&NativeOutputCapture>,
) -> io::Result<()> {
match capture.and_then(NativeOutputCapture::take_error) {
Some(error) => Err(failure(
if capture.is_some_and(NativeOutputCapture::cleanup_failed) { RunnerPhase::Cleanup }
else if stream == "stdout" { RunnerPhase::Stdout } else { RunnerPhase::Stderr },
LeaderTermination::Confirmed,
error,
)),
None if capture.is_some_and(|capture| !capture.is_complete()) => Err(failure(
RunnerPhase::Drain, LeaderTermination::Confirmed,
io::Error::new(io::ErrorKind::TimedOut,
format!("{stream} capture did not reach EOF within its drain bound, output is incomplete")),
)),
None => Ok(()),
}
}
struct WriterState {
cancel: AtomicBool,
#[cfg(windows)]
handle: std::sync::Mutex<Option<usize>>,
#[cfg(unix)]
pipe: std::sync::Mutex<Option<ChildStdin>>,
}
impl WriterState {
fn new() -> Self {
Self {
cancel: AtomicBool::new(false),
#[cfg(windows)]
handle: std::sync::Mutex::new(None),
#[cfg(unix)]
pipe: std::sync::Mutex::new(None),
}
}
#[cfg(unix)]
fn pipe(&self) -> std::sync::MutexGuard<'_, Option<ChildStdin>> {
self.pipe
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
}
#[cfg(unix)]
fn close_pipe(&self) {
drop(self.pipe().take());
}
fn cancelled(&self) -> bool {
self.cancel.load(Ordering::Acquire)
}
fn cancel(&self) {
self.cancel.store(true, Ordering::Release);
#[cfg(windows)]
{
let held = self
.handle
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
if let Some(raw) = *held {
unsafe { crate::native_capture::platform::cancel_all_io(raw as _) };
}
}
}
}
struct StdinWriter {
state: Arc<WriterState>,
done: mpsc::Receiver<io::Result<()>>,
handle: Option<JoinHandle<()>>,
}
impl StdinWriter {
fn spawn(stdin: ChildStdin, bytes: Zeroizing<Vec<u8>>) -> io::Result<Self> {
let state = Arc::new(WriterState::new());
let (sender, done) = mpsc::channel();
let shared = Arc::clone(&state);
#[cfg(unix)]
{
*state.pipe() = Some(stdin);
}
let handle = std::thread::Builder::new()
.name("rightkit-run-stdin".into())
.spawn(move || {
#[cfg(unix)]
let result = {
let result = write_input(&bytes, &shared);
shared.close_pipe();
result
};
#[cfg(windows)]
let result = {
let mut stdin = stdin;
let result = write_input(&mut stdin, &bytes, &shared);
drop(stdin);
result
};
let _ = sender.send(result);
})?;
Ok(Self {
state,
done,
handle: Some(handle),
})
}
fn finish(mut self) -> io::Result<()> {
let result = match self.done.recv_timeout(STDIN_WAIT) {
Err(mpsc::RecvTimeoutError::Timeout) => self.cancel_and_wait(),
other => other,
};
self.settle(result)
}
fn cancel_and_wait(&self) -> Result<io::Result<()>, mpsc::RecvTimeoutError> {
let deadline = Instant::now() + STDIN_CANCEL_WAIT;
loop {
self.state.cancel();
match self.done.recv_timeout(CANCEL_RETRY) {
Err(mpsc::RecvTimeoutError::Timeout) if Instant::now() < deadline => {}
other => return other,
}
}
}
fn settle(&mut self, result: Result<io::Result<()>, mpsc::RecvTimeoutError>) -> io::Result<()> {
let handle = self.handle.take();
match result {
Ok(result) => {
if let Some(handle) = handle {
let _ = handle.join();
}
match result {
Err(error) if error.kind() != io::ErrorKind::BrokenPipe => Err(error),
_ => Ok(()),
}
}
Err(mpsc::RecvTimeoutError::Timeout) => {
#[cfg(unix)]
self.state.close_pipe();
Err(failure(
RunnerPhase::Cleanup,
LeaderTermination::Confirmed,
io::Error::new(
io::ErrorKind::TimedOut,
"the stdin writer did not stop within its cancellation bound",
),
))
}
Err(mpsc::RecvTimeoutError::Disconnected) => {
if let Some(handle) = handle {
let _ = handle.join();
}
Err(failure(
RunnerPhase::Cleanup,
LeaderTermination::Confirmed,
io::Error::other("the stdin writer thread panicked"),
))
}
}
}
}
impl Drop for StdinWriter {
fn drop(&mut self) {
if self.handle.is_some() {
let result = self.cancel_and_wait();
let _ = self.settle(result);
}
}
}
#[cfg(unix)]
fn write_input(bytes: &[u8], state: &WriterState) -> io::Result<()> {
use std::os::fd::AsRawFd;
let fd = {
let guard = state.pipe();
let Some(pipe) = guard.as_ref() else {
return Ok(());
};
let fd = pipe.as_raw_fd();
unsafe {
let flags = libc::fcntl(fd, libc::F_GETFL);
if flags < 0 || libc::fcntl(fd, libc::F_SETFL, flags | libc::O_NONBLOCK) < 0 {
return Err(io::Error::last_os_error());
}
}
fd
};
let mut written = 0;
while written < bytes.len() {
if state.cancelled() {
return Ok(());
}
let rest = &bytes[written..];
let chunk = rest.len().min(1 << 20);
let outcome = {
let guard = state.pipe();
let Some(pipe) = guard.as_ref() else {
return Ok(());
};
let count = unsafe { libc::write(pipe.as_raw_fd(), rest.as_ptr().cast(), chunk) };
if count >= 0 {
Ok(count as usize)
} else {
Err(io::Error::last_os_error())
}
};
match outcome {
Ok(count) => written += count,
Err(error) => match error.kind() {
io::ErrorKind::Interrupted => {}
io::ErrorKind::WouldBlock => {
let mut entry = libc::pollfd {
fd,
events: libc::POLLOUT,
revents: 0,
};
unsafe { libc::poll(&mut entry, 1, WRITE_POLL.as_millis() as libc::c_int) };
}
_ => return Err(error),
},
}
}
Ok(())
}
#[cfg(windows)]
fn write_input(stdin: &mut ChildStdin, bytes: &[u8], state: &WriterState) -> io::Result<()> {
use std::{io::Write, os::windows::io::AsRawHandle};
let publish = |value: Option<usize>| {
*state
.handle
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner) = value;
};
publish(Some(stdin.as_raw_handle() as usize));
let mut result = Ok(());
for chunk in bytes.chunks(WRITE_CHUNK) {
if state.cancelled() {
break;
}
if let Err(error) = stdin.write_all(chunk) {
result = Err(error);
break;
}
}
publish(None);
match result {
Err(_) if state.cancelled() => Ok(()),
other => other,
}
}