use std::io;
use std::process::ExitStatus;
use std::time::Duration;
use process_wrap::tokio::ChildWrapper;
use tokio::io::{AsyncBufReadExt, AsyncRead, BufReader};
use super::OnLine;
use crate::events::Stream;
const DRAIN_AFTER_EXIT: Duration = Duration::from_secs(2);
pub(super) async fn forward_lines(child: &mut dyn ChildWrapper, on_line: OnLine<'_>) -> io::Result<ExitStatus> {
let not_piped = || io::Error::other("the process's output wasn't piped");
let stdout = child.stdout().take().ok_or_else(not_piped)?;
let stderr = child.stderr().take().ok_or_else(not_piped)?;
let mut out = Reader::new(stdout);
let mut err = Reader::new(stderr);
let mut exited: Option<ExitStatus> = None;
let drain = tokio::time::sleep(DRAIN_AFTER_EXIT);
tokio::pin!(drain);
while !(out.done && err.done) {
tokio::select! {
read = out.next(), if !out.done => if let Some(line) = read? { on_line(Stream::Stdout, &line) },
read = err.next(), if !err.done => if let Some(line) = read? { on_line(Stream::Stderr, &line) },
status = child.inner_mut().wait(), if exited.is_none() => {
exited = Some(status?);
drain.as_mut().reset(tokio::time::Instant::now() + DRAIN_AFTER_EXIT);
},
() = &mut drain, if exited.is_some() => break,
}
}
for (stream, buf) in [(Stream::Stdout, &mut out.buf), (Stream::Stderr, &mut err.buf)] {
if !buf.is_empty() {
on_line(stream, &take_line(buf));
}
}
match exited {
Some(status) => Ok(status),
None => child.inner_mut().wait().await,
}
}
struct Reader<R> {
inner: BufReader<R>,
buf: Vec<u8>,
done: bool,
}
impl<R: AsyncRead + Unpin> Reader<R> {
fn new(inner: R) -> Reader<R> {
Reader {
inner: BufReader::new(inner),
buf: Vec::new(),
done: false,
}
}
async fn next(&mut self) -> io::Result<Option<String>> {
let read = self.inner.read_until(b'\n', &mut self.buf).await?;
if read == 0 && self.buf.is_empty() {
self.done = true;
return Ok(None);
}
Ok(Some(take_line(&mut self.buf)))
}
}
pub(super) fn take_line(buf: &mut Vec<u8>) -> String {
let line = buf.strip_suffix(b"\n").unwrap_or(buf);
let line = line.strip_suffix(b"\r").unwrap_or(line);
let line = String::from_utf8_lossy(line).into_owned();
buf.clear();
line
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn a_line_loses_its_ending_only() {
let mut buf = b"a\r\r\n".to_vec();
assert_eq!(take_line(&mut buf), "a\r");
assert_eq!(buf, Vec::<u8>::new());
assert_eq!(take_line(&mut b"last".to_vec()), "last");
assert_eq!(take_line(&mut b"\n".to_vec()), "");
}
#[tokio::test]
async fn a_child_whose_output_isnt_piped_is_an_error_not_a_panic() {
let program = if cfg!(windows) { "cmd" } else { "true" };
let mut cmd = process_wrap::tokio::CommandWrap::with_new(program, |c| {
if cfg!(windows) {
c.args(["/C", "exit 0"]);
}
c.stdout(std::process::Stdio::null());
});
let mut child = cmd.spawn().unwrap();
let err = forward_lines(child.as_mut(), &mut |_, _| {}).await.unwrap_err();
assert_eq!(err.to_string(), "the process's output wasn't piped");
assert!(child.wait().await.unwrap().success());
}
#[tokio::test]
async fn lines_are_read_lossily_whatever_the_read_boundaries() {
let mut input = "a\r\nb\rc\n\u{20ac}\u{20ac}\n".as_bytes().to_vec();
input.extend_from_slice(&[0xff, b'd']);
let mut reader = Reader {
inner: BufReader::with_capacity(2, &input[..]),
buf: Vec::new(),
done: false,
};
let mut lines = Vec::new();
while let Some(line) = reader.next().await.unwrap() {
lines.push(line);
}
assert_eq!(lines, ["a", "b\rc", "\u{20ac}\u{20ac}", "\u{fffd}d"]);
assert!(reader.done);
}
}