sopht 0.3.0

cute program for managing long running processes in a (slightly) more sophisticated way than tmux
Documentation
//! this module contains all necessary logic for starting and interacting with
//! processes across all platforms
//!
use crate::{Error, Result};
use std::io::{BufRead, BufReader, Read, Write};
use std::marker::Send;
use std::path::{Path, PathBuf};
use std::process::{Child, ChildStdin, Command, ExitStatus, Stdio};
use std::sync::mpsc::{self, Receiver, Sender};
use std::thread::{self, JoinHandle};

/// PIDs are represented internally by a plain `usize`, though this may change
/// in the future!
pub type PID = usize;

/// contains all information about a running or previously-running process,
/// including the `Child` structure and the handles to the threads reading the
/// output streams of the process. this structure also maintains a `Vec<String>`
/// containing all the output lines of its child process. for more info about
/// how this output is read and stored, see [`Process::start`]
pub struct Process {
    /// the path of the process' program
    pub prog: String,
    /// list of arguments to the process
    pub args: Vec<String>,
    /// handle to the child process. note: the `stdin`, `stdout`, and `stderr`
    /// are all `None` as they have been moved into this structure
    pub child: Child,
    /// handle to the thread reading the standard output stream of the child
    _stdout_thread: JoinHandle<()>,
    /// handle to the thread reading the standard error stream of the child
    _stderr_thread: JoinHandle<()>,
    /// receiver collecting lines of output sequentially from both reader
    /// threads
    receiver: Receiver<String>,
    /// list of all lines of output collected by the receiver. this list can be
    /// populated by calling [`Process::gather_output`], which will move lines
    /// from the receiver into this list
    pub output: Vec<u8>,
    /// handle to the standard input stream of the child process
    stdin: ChildStdin,
    /// working directory of the process
    pub working_dir: PathBuf,
}

impl Process {
    /// attempts to start and create a [`Process`] from a command string.
    /// this function can fail if:
    ///   * the process fails to start ([`ProcessError`](Error::ProcessError))
    ///   * the supplied command string is empty
    ///     ([`EmptyProgram`](Error::EmptyProgram))
    ///
    /// this function also starts two threads to read the `stdout` and `stderr`
    /// of the child process without blocking the main thread. this is
    /// considered an acceptable overhead, since these reader threads create
    /// [`BufReader`]s internally and will spend most of their time sleeping and
    /// waiting for the OS to give them more data
    pub fn start(
        prog: &String,
        args: &Vec<String>,
        working_dir: &Path,
    ) -> Result<Self> {
        let (tx, receiver) = mpsc::channel::<String>();

        let mut child = match Command::new(prog)
            .args(args)
            .stdout(Stdio::piped())
            .stderr(Stdio::piped())
            .stdin(Stdio::piped())
            .current_dir(working_dir)
            .spawn()
        {
            Ok(child) => child,
            Err(e) => return Err(Error::ProcessError(e)),
        };

        let stdin = child.stdin.take().unwrap();
        let stdout = child.stdout.take().unwrap();
        let stderr = child.stderr.take().unwrap();

        let _stdout_thread = read_thread(tx.clone(), stdout);
        let _stderr_thread = read_thread(tx, stderr);

        let output = Vec::new();
        Ok(Self {
            prog: prog.clone(),
            args: args.clone(),
            child,
            _stdout_thread,
            _stderr_thread,
            receiver,
            output,
            stdin,
            working_dir: working_dir.to_owned(),
        })
    }

    /// takes all output lines from the internal receiver and pushes them into
    /// the `output` array
    pub fn gather_output(&mut self) {
        for line in self.receiver.try_iter() {
            self.output.append(&mut line.into_bytes());
            self.output.push(b'\n');
        }
    }

    /// checks if the process is alive. this function returns an `Option<bool>`
    /// for completeness because it is technically possible for the status to be
    /// unknown (see the Win32 docs for `WaitForSingleObject`), though the
    /// return value of this function can probably be safely unwrapped most of
    /// the time since failure should be rare
    pub fn is_alive(&mut self) -> Option<bool> {
        match self.child.try_wait() {
            Ok(Some(_)) => Some(false),
            Ok(None) => Some(true),
            _ => None,
        }
    }

    /// retrieves the exit status of the process. if the process has not exited
    /// or the status is unavailable, returns `None`
    pub fn exit_status(&mut self) -> Option<ExitStatus> {
        match self.child.try_wait() {
            Ok(s) => s,
            _ => None,
        }
    }

    /// attempts to kill the process. returns a
    /// [`ProcessError`](Error::ProcessError) on failure
    pub fn kill(&mut self) -> Result<()> {
        match self.child.kill() {
            Ok(_) => {
                self.child.wait().expect("failed to wait on process");
                Ok(())
            }
            Err(e) => Err(Error::ProcessError(e)),
        }
    }

    /// attempts to send input to the process. this function can fail if the
    /// write implementation for [`ChildStdin`] fails, and in that case
    /// will return a [`ProcessError`](Error::ProcessError) containing the
    /// internal error
    /// <strong>note: this function appends a newline to all strings!</strong>
    pub fn send_input(&mut self, line: &str) -> Result<()> {
        let mut input = line.to_owned();
        input.push('\n');

        if let Err(e) = self.stdin.write_all(input.as_bytes()) {
            Err(Error::ProcessError(e))
        } else {
            Ok(())
        }
    }
}

/// takes a [`Sender`] and something implementing [`Read`] and creates a new
/// thread that reads all lines from the read instance and sends them over `tx`
/// to the calling thread. this function is used by [`Process::start`] to create
/// the reader threads for `stdout` and `stderr` so it can collect their output
/// continuously and without blocking
fn read_thread<R: Read + Send + 'static>(
    tx: Sender<String>,
    instance: R,
) -> JoinHandle<()> {
    thread::spawn(move || {
        let reader = BufReader::new(instance);

        for line in reader.lines() {
            match line {
                Ok(line) => tx.send(line).expect("failed to send"),
                Err(e) => {
                    eprintln!("failed to read line: {}", e);
                    return;
                }
            }
        }
    })
}