mxsh 0.2.0

Embeddable POSIX-style shell parser and runtime
Documentation
use std::io::{Read, Write};
use std::os::unix::io::FromRawFd;
use std::thread::JoinHandle;

use super::{FileDescriptor, OsPipe};

/// Test helper: creates an OS pipe and spawns a thread to feed data into the write end.
///
/// The read end fd is available via [`fd()`] and works with both builtins (via `read(2)`) and
/// spawned child processes (via `posix_spawnp` fd actions).
///
/// Call [`join()`] after the consumer is done to ensure the writer thread has finished.
pub struct StringStdioIn {
    read_fd: FileDescriptor,
    writer_thread: Option<JoinHandle<()>>,
}

impl StringStdioIn {
    /// Create a StringStdioIn that feeds `input` line-by-line through a pipe.
    pub fn new(input: &str) -> Self {
        let pipe = OsPipe::new().expect("failed to create pipe for StringStdioIn");
        let data = input.to_string();
        let write_fd = pipe.write_fd;
        let writer_thread = std::thread::spawn(move || {
            let mut file = unsafe { std::fs::File::from_raw_fd(write_fd.into_raw_fd()) };
            let _ = file.write_all(data.as_bytes());
        });
        Self {
            read_fd: pipe.read_fd,
            writer_thread: Some(writer_thread),
        }
    }

    /// The read-end fd. Use this as the shell's stdin.
    pub fn fd(&self) -> FileDescriptor {
        self.read_fd
    }

    /// Wait for the writer thread to finish.
    pub fn join(mut self) {
        self.read_fd.close();
        self.read_fd = FileDescriptor::INVALID;
        if let Some(handle) = self.writer_thread.take() {
            let _ = handle.join();
        }
    }
}

impl Drop for StringStdioIn {
    fn drop(&mut self) {
        if let Some(handle) = self.writer_thread.take() {
            self.read_fd.close();
            let _ = handle.join();
        } else {
            self.read_fd.close();
        }
    }
}

/// Test helper: creates an OS pipe. The write end fd is used as the shell's stdout/stderr.
///
/// Call [`collect()`] to close the write end and drain all output from the read end via a
/// background thread.
pub struct StringStdioOut {
    write_fd: FileDescriptor,
    reader_thread: Option<JoinHandle<String>>,
}

impl StringStdioOut {
    /// Create a new StringStdioOut backed by an OS pipe.
    pub fn new() -> Self {
        let pipe = OsPipe::new().expect("failed to create pipe for StringStdioOut");
        let read_fd = pipe.read_fd;
        let reader_thread = std::thread::spawn(move || {
            let mut file = unsafe { std::fs::File::from_raw_fd(read_fd.into_raw_fd()) };
            let mut output = String::new();
            let _ = file.read_to_string(&mut output);
            output
        });
        Self {
            write_fd: pipe.write_fd,
            reader_thread: Some(reader_thread),
        }
    }

    /// The write-end fd. Use this as the shell's stdout or stderr.
    pub fn fd(&self) -> FileDescriptor {
        self.write_fd
    }

    /// Close the write end and read all output. Blocks until EOF.
    pub fn collect(mut self) -> String {
        self.write_fd.close();
        self.write_fd = FileDescriptor::INVALID;
        if let Some(handle) = self.reader_thread.take() {
            handle.join().unwrap_or_default()
        } else {
            String::new()
        }
    }
}

impl Default for StringStdioOut {
    fn default() -> Self {
        Self::new()
    }
}

impl Drop for StringStdioOut {
    fn drop(&mut self) {
        self.write_fd.close();
        if let Some(handle) = self.reader_thread.take() {
            let _ = handle.join();
        }
    }
}