#![allow(dead_code, unused_imports, unused_qualifications, unreachable_patterns)]
use std::io::{self, BufRead, BufReader, Read};
use std::process::{Child, ExitStatus, Output, Stdio};
use std::sync::mpsc;
use std::thread;
use std::time::{Duration, Instant};
#[derive(Debug)]
pub enum TimeoutResult<T> {
Completed(T),
TimedOut,
}
impl<T> TimeoutResult<T> {
pub fn into_option(self) -> Option<T> {
match self {
TimeoutResult::Completed(v) => Some(v),
TimeoutResult::TimedOut => None,
}
}
pub fn is_timed_out(&self) -> bool {
matches!(self, TimeoutResult::TimedOut)
}
}
const POLL_INTERVAL: Duration = Duration::from_millis(50);
pub fn wait_with_timeout(
child: &mut Child,
timeout: Duration,
) -> io::Result<TimeoutResult<ExitStatus>> {
let start = Instant::now();
loop {
match child.try_wait()? {
Some(status) => return Ok(TimeoutResult::Completed(status)),
None => {
if start.elapsed() >= timeout {
return Ok(TimeoutResult::TimedOut);
}
thread::sleep(POLL_INTERVAL);
}
}
}
}
pub fn wait_output_with_timeout(
mut child: Child,
timeout: Duration,
) -> io::Result<TimeoutResult<Output>> {
let stdout_thread = child.stdout.take().map(|mut s| {
thread::Builder::new()
.name("enclaveapp-child-stdout".into())
.spawn(move || -> io::Result<Vec<u8>> {
let mut buf = Vec::new();
s.read_to_end(&mut buf)?;
Ok(buf)
})
});
let stderr_thread = child.stderr.take().map(|mut s| {
thread::Builder::new()
.name("enclaveapp-child-stderr".into())
.spawn(move || -> io::Result<Vec<u8>> {
let mut buf = Vec::new();
s.read_to_end(&mut buf)?;
Ok(buf)
})
});
match wait_with_timeout(&mut child, timeout)? {
TimeoutResult::Completed(status) => {
let stdout = match stdout_thread {
Some(Ok(t)) => t.join().unwrap_or_else(|_| Ok(Vec::new()))?,
_ => Vec::new(),
};
let stderr = match stderr_thread {
Some(Ok(t)) => t.join().unwrap_or_else(|_| Ok(Vec::new()))?,
_ => Vec::new(),
};
Ok(TimeoutResult::Completed(Output {
status,
stdout,
stderr,
}))
}
TimeoutResult::TimedOut => {
drop(child.kill());
drop(child.wait());
Ok(TimeoutResult::TimedOut)
}
}
}
pub fn run_with_timeout(
mut cmd: std::process::Command,
timeout: Duration,
) -> io::Result<TimeoutResult<Output>> {
cmd.stdout(Stdio::piped()).stderr(Stdio::piped());
let child = cmd.spawn()?;
wait_output_with_timeout(child, timeout)
}
pub fn run_status_with_timeout(
mut cmd: std::process::Command,
timeout: Duration,
) -> io::Result<TimeoutResult<ExitStatus>> {
let mut child = cmd.spawn()?;
match wait_with_timeout(&mut child, timeout)? {
TimeoutResult::Completed(status) => Ok(TimeoutResult::Completed(status)),
TimeoutResult::TimedOut => {
drop(child.kill());
drop(child.wait());
Ok(TimeoutResult::TimedOut)
}
}
}
#[derive(Debug)]
pub struct LineReaderWithTimeout {
rx: mpsc::Receiver<io::Result<String>>,
_thread: thread::JoinHandle<()>,
}
impl LineReaderWithTimeout {
pub fn new<R: Read + Send + 'static>(reader: R) -> Self {
Self::spawn(reader, None)
}
pub fn with_max_line_bytes<R: Read + Send + 'static>(reader: R, max_line_bytes: usize) -> Self {
Self::spawn(reader, Some(max_line_bytes))
}
fn spawn<R: Read + Send + 'static>(reader: R, max_line_bytes: Option<usize>) -> Self {
let (tx, rx) = mpsc::channel();
let thread = thread::Builder::new()
.name("enclaveapp-line-reader".into())
.spawn(move || {
let mut buf_reader = BufReader::new(reader);
loop {
let result = match max_line_bytes {
Some(max) => read_line_bounded(&mut buf_reader, max),
None => {
let mut line = String::new();
match buf_reader.read_line(&mut line) {
Ok(0) => Ok(None),
Ok(_) => Ok(Some(line)),
Err(e) => Err(e),
}
}
};
match result {
Ok(None) => break, Ok(Some(line)) => {
if tx.send(Ok(line)).is_err() {
break;
}
}
Err(e) => {
drop(tx.send(Err(e)));
break;
}
}
}
})
.expect("spawn line reader thread");
Self {
rx,
_thread: thread,
}
}
pub fn recv_line(&self, timeout: Duration) -> TimeoutResult<io::Result<String>> {
match self.rx.recv_timeout(timeout) {
Ok(result) => TimeoutResult::Completed(result),
Err(mpsc::RecvTimeoutError::Timeout) => TimeoutResult::TimedOut,
Err(mpsc::RecvTimeoutError::Disconnected) => {
TimeoutResult::Completed(Ok(String::new()))
}
}
}
}
pub fn read_line_bounded<R: BufRead>(
reader: &mut R,
max_bytes: usize,
) -> io::Result<Option<String>> {
let mut buf: Vec<u8> = Vec::new();
loop {
let available = match reader.fill_buf() {
Ok(b) => b,
Err(e) if e.kind() == io::ErrorKind::Interrupted => continue,
Err(e) => return Err(e),
};
if available.is_empty() {
return if buf.is_empty() {
Ok(None)
} else {
String::from_utf8(buf)
.map(Some)
.map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))
};
}
let remaining = max_bytes.saturating_sub(buf.len());
let usable = &available[..available.len().min(remaining + 1)];
if let Some(pos) = usable.iter().position(|&b| b == b'\n') {
buf.extend_from_slice(&usable[..=pos]);
reader.consume(pos + 1);
return String::from_utf8(buf)
.map(Some)
.map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e));
}
if remaining == 0 {
return Err(io::Error::new(
io::ErrorKind::InvalidData,
format!("line exceeds {max_bytes}-byte cap before newline"),
));
}
let take = remaining.min(available.len());
buf.extend_from_slice(&available[..take]);
reader.consume(take);
}
}
#[cfg(test)]
#[allow(clippy::unwrap_used, clippy::panic)]
mod pure_tests {
use super::*;
use std::io::{self, Cursor};
#[test]
fn timeout_result_completed_into_option_is_some() {
let r: TimeoutResult<i32> = TimeoutResult::Completed(42);
assert_eq!(r.into_option(), Some(42));
}
#[test]
fn timeout_result_timed_out_into_option_is_none() {
let r: TimeoutResult<i32> = TimeoutResult::TimedOut;
assert_eq!(r.into_option(), None);
}
#[test]
fn timeout_result_completed_is_not_timed_out() {
let r: TimeoutResult<i32> = TimeoutResult::Completed(1);
assert!(!r.is_timed_out());
}
#[test]
fn timeout_result_timed_out_is_timed_out() {
let r: TimeoutResult<i32> = TimeoutResult::TimedOut;
assert!(r.is_timed_out());
}
#[test]
fn read_line_bounded_empty_reader_returns_none() {
let mut cursor = Cursor::new(b"");
let result = read_line_bounded(&mut cursor, 1024).unwrap();
assert!(result.is_none());
}
#[test]
fn read_line_bounded_single_line_with_newline() {
let mut cursor = Cursor::new(b"hello\n");
let result = read_line_bounded(&mut cursor, 1024).unwrap();
assert_eq!(result.as_deref(), Some("hello\n"));
}
#[test]
fn read_line_bounded_eof_without_newline() {
let mut cursor = Cursor::new(b"hello");
let result = read_line_bounded(&mut cursor, 1024).unwrap();
assert_eq!(result.as_deref(), Some("hello"));
}
#[test]
fn read_line_bounded_multiple_lines_reads_sequentially() {
let mut cursor = Cursor::new(b"first\nsecond\n");
let line1 = read_line_bounded(&mut cursor, 1024).unwrap();
let line2 = read_line_bounded(&mut cursor, 1024).unwrap();
let line3 = read_line_bounded(&mut cursor, 1024).unwrap();
assert_eq!(line1.as_deref(), Some("first\n"));
assert_eq!(line2.as_deref(), Some("second\n"));
assert!(line3.is_none());
}
#[test]
fn read_line_bounded_line_exceeds_cap_returns_invalid_data() {
let mut cursor = Cursor::new(b"hello\n");
let err = read_line_bounded(&mut cursor, 3).unwrap_err();
assert_eq!(err.kind(), io::ErrorKind::InvalidData);
}
#[test]
fn read_line_bounded_line_at_exact_cap_succeeds() {
let mut cursor = Cursor::new(b"hello\n");
let result = read_line_bounded(&mut cursor, 5).unwrap();
assert_eq!(result.as_deref(), Some("hello\n"));
}
#[test]
fn read_line_bounded_max_bytes_zero_newline_first_succeeds() {
let mut cursor = Cursor::new(b"\nhello");
let result = read_line_bounded(&mut cursor, 0).unwrap();
assert_eq!(result.as_deref(), Some("\n"));
}
#[test]
fn read_line_bounded_max_bytes_zero_non_newline_first_is_error() {
let mut cursor = Cursor::new(b"a\nhello");
let err = read_line_bounded(&mut cursor, 0).unwrap_err();
assert_eq!(err.kind(), io::ErrorKind::InvalidData);
}
#[test]
fn read_line_bounded_utf8_content() {
let input = "héllo\n";
let mut cursor = Cursor::new(input.as_bytes());
let result = read_line_bounded(&mut cursor, 128).unwrap();
assert_eq!(result.as_deref(), Some("héllo\n"));
}
#[test]
fn read_line_bounded_exactly_max_bytes_at_eof_no_newline() {
let mut cursor = Cursor::new(b"abc");
let result = read_line_bounded(&mut cursor, 3).unwrap();
assert_eq!(result.as_deref(), Some("abc"));
}
#[test]
fn read_line_bounded_large_cap_long_line() {
let line: Vec<u8> = std::iter::repeat(b'x').take(100).chain([b'\n']).collect();
let mut cursor = Cursor::new(line);
let result = read_line_bounded(&mut cursor, 200).unwrap();
let s = result.unwrap();
assert_eq!(s.len(), 101);
assert!(s.starts_with('x'));
assert!(s.ends_with('\n'));
}
#[test]
fn read_line_bounded_empty_line_newline_only() {
let mut cursor = Cursor::new(b"\n");
let result = read_line_bounded(&mut cursor, 1024).unwrap();
assert_eq!(result.as_deref(), Some("\n"));
}
#[test]
fn read_line_bounded_after_eof_returns_none() {
let mut cursor = Cursor::new(b"hi\n");
let _unused = read_line_bounded(&mut cursor, 1024).unwrap();
let eof = read_line_bounded(&mut cursor, 1024).unwrap();
assert!(eof.is_none());
}
#[test]
fn read_line_bounded_only_newlines() {
let mut cursor = Cursor::new(b"\n\n\n");
let r1 = read_line_bounded(&mut cursor, 10).unwrap();
let r2 = read_line_bounded(&mut cursor, 10).unwrap();
let r3 = read_line_bounded(&mut cursor, 10).unwrap();
let r4 = read_line_bounded(&mut cursor, 10).unwrap();
assert_eq!(r1.as_deref(), Some("\n"));
assert_eq!(r2.as_deref(), Some("\n"));
assert_eq!(r3.as_deref(), Some("\n"));
assert!(r4.is_none());
}
#[test]
fn read_line_bounded_single_char_at_eof() {
let mut cursor = Cursor::new(b"x");
let result = read_line_bounded(&mut cursor, 1).unwrap();
assert_eq!(result.as_deref(), Some("x"));
}
#[test]
fn read_line_bounded_error_message_contains_cap() {
let mut cursor = Cursor::new(b"toolongline\n");
let err = read_line_bounded(&mut cursor, 5).unwrap_err();
assert!(err.to_string().contains("5"));
}
}
#[cfg(all(test, unix))]
#[allow(clippy::unwrap_used, clippy::panic, let_underscore_drop)]
mod tests {
use super::*;
use std::process::Command;
#[cfg(unix)]
#[test]
fn run_with_timeout_completes_fast_command() {
let result = run_with_timeout(
{
let mut c = Command::new("/bin/sh");
c.args(["-c", "echo hello"]);
c
},
Duration::from_secs(5),
)
.unwrap();
match result {
TimeoutResult::Completed(output) => {
assert!(output.status.success());
assert_eq!(String::from_utf8_lossy(&output.stdout).trim(), "hello");
}
TimeoutResult::TimedOut => panic!("fast command should not time out"),
}
}
#[cfg(unix)]
#[test]
fn run_with_timeout_kills_slow_command() {
let start = Instant::now();
let result = run_with_timeout(
{
let mut c = Command::new("/bin/sh");
c.args(["-c", "sleep 10"]);
c
},
Duration::from_millis(200),
)
.unwrap();
assert!(result.is_timed_out());
assert!(start.elapsed() < Duration::from_secs(2));
}
#[cfg(unix)]
#[test]
fn line_reader_delivers_line_within_timeout() {
use std::io::Write;
let mut cmd = Command::new("/bin/sh");
cmd.args(["-c", "cat"])
.stdin(Stdio::piped())
.stdout(Stdio::piped());
let mut child = cmd.spawn().unwrap();
let r = child.stdout.take().unwrap();
let mut w = child.stdin.take().unwrap();
let reader = LineReaderWithTimeout::new(r);
writeln!(w, "hello world").unwrap();
w.flush().unwrap();
match reader.recv_line(Duration::from_secs(2)) {
TimeoutResult::Completed(Ok(line)) => assert_eq!(line.trim(), "hello world"),
other => panic!("unexpected result: {:?}", other),
}
drop(w);
drop(child.wait());
}
#[cfg(unix)]
#[test]
fn bounded_line_reader_aborts_when_line_exceeds_cap() {
use std::io::Write;
let mut cmd = Command::new("/bin/sh");
cmd.args(["-c", "seq 1 200 | xargs printf 'x%.0s' && printf '\\n'"])
.stdin(Stdio::null())
.stdout(Stdio::piped());
let mut child = cmd.spawn().unwrap();
let stdout = child.stdout.take().unwrap();
let reader = LineReaderWithTimeout::with_max_line_bytes(stdout, 100);
match reader.recv_line(Duration::from_secs(2)) {
TimeoutResult::Completed(Err(e)) => {
assert_eq!(e.kind(), io::ErrorKind::InvalidData);
}
other => panic!("expected InvalidData, got: {:?}", other),
}
drop(child.kill());
drop(child.wait());
let _ = Write::flush(&mut io::stdout());
}
#[cfg(unix)]
#[test]
fn bounded_line_reader_accepts_line_within_cap() {
use std::io::Write;
let mut cmd = Command::new("/bin/sh");
cmd.args(["-c", "printf 'short line\\n'"])
.stdin(Stdio::null())
.stdout(Stdio::piped());
let mut child = cmd.spawn().unwrap();
let stdout = child.stdout.take().unwrap();
let reader = LineReaderWithTimeout::with_max_line_bytes(stdout, 1024);
match reader.recv_line(Duration::from_secs(2)) {
TimeoutResult::Completed(Ok(line)) => assert_eq!(line, "short line\n"),
other => panic!("expected short line, got: {:?}", other),
}
drop(child.wait());
let _ = Write::flush(&mut io::stdout());
}
#[cfg(unix)]
#[test]
fn line_reader_times_out_when_no_data() {
let mut cmd = Command::new("/bin/sh");
cmd.args(["-c", "sleep 10"]).stdout(Stdio::piped());
let mut child = cmd.spawn().unwrap();
let stdout = child.stdout.take().unwrap();
let reader = LineReaderWithTimeout::new(stdout);
let start = Instant::now();
let result = reader.recv_line(Duration::from_millis(200));
assert!(result.is_timed_out());
assert!(start.elapsed() < Duration::from_secs(1));
drop(child.kill());
drop(child.wait());
}
#[cfg(unix)]
#[test]
fn run_status_with_timeout_completes_fast_command() {
let mut cmd = Command::new("/bin/sh");
cmd.args(["-c", "exit 0"]);
let result = run_status_with_timeout(cmd, Duration::from_secs(5)).unwrap();
match result {
TimeoutResult::Completed(status) => assert!(status.success()),
TimeoutResult::TimedOut => panic!("fast command should not time out"),
}
}
#[cfg(unix)]
#[test]
fn run_status_with_timeout_kills_slow_command() {
let start = Instant::now();
let mut cmd = Command::new("/bin/sh");
cmd.args(["-c", "sleep 10"]);
let result = run_status_with_timeout(cmd, Duration::from_millis(200)).unwrap();
assert!(result.is_timed_out());
assert!(start.elapsed() < Duration::from_secs(2));
}
#[test]
fn line_reader_eof_disconnects_and_returns_empty_string() {
let reader = LineReaderWithTimeout::new(io::Cursor::new(b""));
thread::sleep(Duration::from_millis(50));
let result = reader.recv_line(Duration::from_millis(200));
assert!(
matches!(result, TimeoutResult::Completed(Ok(ref s)) if s.is_empty()),
"expected Completed(Ok(\"\")) after sender disconnect, got: {result:?}"
);
}
}