use std::io::{Read, Write};
use std::process::Stdio;
use std::sync::{Arc, Mutex};
use tokio::io::{AsyncRead, AsyncReadExt};
use tokio::sync::mpsc::UnboundedSender;
use crate::{ShellOutputLine, ShellStream};
pub(crate) enum OutTarget {
Lines {
tx: UnboundedSender<ShellOutputLine>,
stream: ShellStream,
},
Capture(Arc<Mutex<Vec<u8>>>),
File(std::fs::File),
Pipe(os_pipe::PipeWriter),
}
pub(crate) enum InTarget {
Null,
File(std::fs::File),
Pipe(os_pipe::PipeReader),
}
pub(crate) struct CommandIo {
pub stdin: InTarget,
pub stdout: OutTarget,
pub stderr: OutTarget,
}
impl CommandIo {
pub(crate) fn defaults(
stdout: &OutTarget,
output: &UnboundedSender<ShellOutputLine>,
) -> std::io::Result<Self> {
Ok(Self {
stdin: InTarget::Null,
stdout: stdout.try_clone()?,
stderr: OutTarget::Lines {
tx: output.clone(),
stream: ShellStream::Stderr,
},
})
}
pub(crate) fn can_block(&self) -> bool {
self.stdin.can_block() || self.stdout.can_block() || self.stderr.can_block()
}
}
impl OutTarget {
pub(crate) fn try_clone(&self) -> std::io::Result<Self> {
Ok(match self {
Self::Lines { tx, stream } => Self::Lines {
tx: tx.clone(),
stream: *stream,
},
Self::Capture(buffer) => Self::Capture(buffer.clone()),
Self::File(file) => Self::File(file.try_clone()?),
Self::Pipe(writer) => Self::Pipe(writer.try_clone()?),
})
}
fn can_block(&self) -> bool {
matches!(self, Self::File(_) | Self::Pipe(_))
}
pub(crate) fn into_stdio(self) -> (Stdio, Option<Self>) {
match self {
Self::File(file) => (Stdio::from(file), None),
Self::Pipe(writer) => (Stdio::from(writer), None),
sink @ (Self::Lines { .. } | Self::Capture(_)) => (Stdio::piped(), Some(sink)),
}
}
pub(crate) async fn forward(self, mut reader: impl AsyncRead + Unpin) {
let mut writer = self.writer();
let mut buffer = [0u8; 8192];
loop {
match reader.read(&mut buffer).await {
Ok(0) | Err(_) => break,
Ok(read) => {
if writer.write_all(&buffer[..read]).is_err() {
break;
}
}
}
}
}
pub(crate) fn writer(self) -> Box<dyn Write + Send> {
match self {
Self::Lines { tx, stream } => Box::new(LineWriter {
tx,
stream,
pending: Vec::new(),
}),
Self::Capture(buffer) => Box::new(CaptureWriter { buffer }),
Self::File(file) => Box::new(file),
Self::Pipe(writer) => Box::new(writer),
}
}
}
impl InTarget {
pub(crate) fn into_stdio(self) -> Stdio {
match self {
Self::Null => Stdio::null(),
Self::File(file) => Stdio::from(file),
Self::Pipe(reader) => Stdio::from(reader),
}
}
fn can_block(&self) -> bool {
matches!(self, Self::File(_) | Self::Pipe(_))
}
pub(crate) fn reader(self) -> Box<dyn Read + Send> {
match self {
Self::Null => Box::new(std::io::empty()),
Self::File(file) => Box::new(file),
Self::Pipe(reader) => Box::new(reader),
}
}
}
struct LineWriter {
tx: UnboundedSender<ShellOutputLine>,
stream: ShellStream,
pending: Vec<u8>,
}
impl LineWriter {
fn emit(&self, bytes: &[u8]) {
let bytes = match bytes.last() {
Some(b'\r') => &bytes[..bytes.len() - 1],
_ => bytes,
};
let _ = self.tx.send(ShellOutputLine {
stream: self.stream,
text: String::from_utf8_lossy(bytes).into_owned(),
});
}
}
impl Write for LineWriter {
fn write(&mut self, data: &[u8]) -> std::io::Result<usize> {
self.pending.extend_from_slice(data);
while let Some(end) = self.pending.iter().position(|byte| *byte == b'\n') {
let line: Vec<u8> = self.pending.drain(..=end).collect();
self.emit(&line[..end]);
}
Ok(data.len())
}
fn flush(&mut self) -> std::io::Result<()> {
Ok(())
}
}
impl Drop for LineWriter {
fn drop(&mut self) {
if !self.pending.is_empty() {
let pending = std::mem::take(&mut self.pending);
self.emit(&pending);
}
}
}
struct CaptureWriter {
buffer: Arc<Mutex<Vec<u8>>>,
}
impl Write for CaptureWriter {
fn write(&mut self, data: &[u8]) -> std::io::Result<usize> {
self.buffer
.lock()
.expect("capture buffer poisoned")
.extend_from_slice(data);
Ok(data.len())
}
fn flush(&mut self) -> std::io::Result<()> {
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
fn collect(chunks: &[&[u8]]) -> Vec<String> {
let (tx, mut rx) = tokio::sync::mpsc::unbounded_channel();
{
let mut writer = OutTarget::Lines {
tx,
stream: ShellStream::Stdout,
}
.writer();
for chunk in chunks {
writer.write_all(chunk).unwrap();
}
}
let mut lines = Vec::new();
while let Ok(line) = rx.try_recv() {
lines.push(line.text);
}
lines
}
#[test]
fn splits_whole_lines() {
assert_eq!(collect(&[b"first\nsecond\n"]), vec!["first", "second"]);
}
#[test]
fn strips_a_cr_before_the_newline() {
assert_eq!(collect(&[b"hello\r\nworld\r\n"]), vec!["hello", "world"]);
}
#[test]
fn a_lone_cr_not_followed_by_newline_is_preserved() {
assert_eq!(collect(&[b"a\rb\n"]), vec!["a\rb"]);
}
#[test]
fn flushes_a_final_unterminated_line_on_drop() {
assert_eq!(collect(&[b"first\nsecond"]), vec!["first", "second"]);
}
#[test]
fn reassembles_a_line_split_across_writes() {
assert_eq!(collect(&[b"he", b"ll", b"o\n"]), vec!["hello"]);
}
#[test]
fn writing_nothing_emits_nothing() {
assert_eq!(collect(&[]), Vec::<String>::new());
}
#[test]
fn handles_a_very_long_line() {
let long_line = "x".repeat(200_000);
let mut data = long_line.clone().into_bytes();
data.push(b'\n');
assert_eq!(collect(&[&data]), vec![long_line]);
}
#[test]
fn a_capture_writer_keeps_every_byte_verbatim() {
let buffer = Arc::new(Mutex::new(Vec::new()));
{
let mut writer = OutTarget::Capture(buffer.clone()).writer();
writer.write_all(b"one\ntwo").unwrap();
}
assert_eq!(&*buffer.lock().unwrap(), b"one\ntwo");
}
}