reef 0.0.28

a package to execute and log system commands
Documentation
use super::errors::*;
use super::{Command, Env, Settings, Status};
use chrono::prelude::*;
use colored::*;
use log::{debug, error, info, warn};
use std::cmp::Ordering;
use std::ffi::{OsStr, OsString};
use std::fmt::{Display, Formatter};
use std::io::Read;
use std::path::Path;
use std::time::{Duration, Instant};
use uuid::Uuid;

impl Command {
    pub fn new(command: &str, path: &Path) -> Command {
        let (name, args) = parse_command(command);
        let mut c = super::Command::default();
        c.timeout = Duration::from_secs(300);
        c.name = name;
        c.args = args;
        c.dir = path.to_path_buf();
        c
    }

    pub fn load(path: &Path) -> super::errors::Result<Command> {
        let mut file = std::fs::File::open(&path)?;
        let mut json = String::new();
        file.read_to_string(&mut json)?;
        let command = serde_json::from_str::<Command>(&json)?;
        Ok(command)
    }

    /// execute a command
    pub fn exec(&mut self) -> Result<Command> {
        _exec(self)
    }

    pub fn status(&self) -> Status {
        match self.success {
            true => Status::Ok,
            false => Status::Error,
        }
    }

    pub fn success_symbol(&self) -> String {
        match self.success {
            true => "✓".to_string(),
            false => "X".to_string(),
        }
    }

    pub fn duration_string(&self) -> String {
        super::duration::format(&self.duration)
    }

    pub fn show(&self) {
        info!(
            target: "reef",
            "{} {:>6} {} {} [{}]",
            self.status().symbol(),
            super::duration::format(&self.duration).as_str().clear(),
            &self.name,
            &self.args.join(" "),
            &self.dir.display()
        )
    }

    pub fn summary(&self) -> String {
        format!(
            "{} {:>6} {} {} [{}]",
            &self.status().symbol(),
            &self.duration_string(),
            &self.name,
            &self.args.join(" "),
            &self.dir.display()
        )
    }

    pub fn details(&self) -> Vec<String> {
        let mut details = Vec::new();
        details.push(self.summary());
        details.push("stdout".to_string());
        for line in self.stdout.lines() {
            details.push(line.to_string());
        }
        details.push("stderr".to_string());
        for line in self.stderr.lines() {
            details.push(line.to_string());
        }
        details.push(format!("success {:?}", self.success));
        details.push(format!("exit code {:?}", self.exit_code));
        details
    }

    pub fn start_utc(&self) -> DateTime<Utc> {
        match &self.start.parse::<DateTime<Utc>>() {
            Ok(dt) => *dt,
            Err(_) => Utc::now(),
        }
    }

    pub fn start_local(&self) -> DateTime<Local> {
        match &self.start.parse::<DateTime<Local>>() {
            Ok(lt) => *lt,
            Err(_) => Local::now(),
        }
    }

    pub fn to_json(&self) -> Result<String> {
        Ok(serde_json::to_string_pretty(&self)?)
    }

    pub fn log(&self) -> Result<()> {
        match self.success {
            true => info!(target: "reef","{}",&self.summary()),
            false => {
                error!(target: "reef","{}",&self.summary());
                for line in self.stdout.lines() {
                    info!(target:"reef","{:>9}{}"," ",line);
                }
                for line in self.stderr.lines() {
                    info!(target:"reef","{:>9}{}"," ",line);
                }
            }
        }

        let settings = Settings::default();
        let filename = &settings.log_path.join(&self.uuid);
        if !settings.log_path.exists() {
            std::fs::create_dir_all(&settings.log_path)?;
        }
        let json = self.to_json()?;
        std::fs::write(&filename, &json)?;

        Ok(())
    }

    pub fn to_err_string(&self) -> String {
        let mut result = String::new();
        result.push_str(&self.summary());
        for line in self.stdout.lines() {
            result.push_str(&format!("\n{:>9}{}", " ", line));
        }
        for line in self.stderr.lines() {
            result.push_str(&format!("\n{:>9}{}", " ", line));
        }
        result
    }
}

impl Display for Command {
    fn fmt(&self, f: &mut Formatter) -> std::fmt::Result {
        match self.success {
            true => write!(
                f,
                "{} {} {} {}{}",
                &self.status().symbol(),
                &self.duration_string().normal(),
                &self.name.yellow().bold(),
                &self.args.join(" ").yellow().bold(),
                "".clear()
            ),
            false => write!(
                f,
                "{} {} {} {}\n{}\n{}",
                &self.status().symbol(),
                &self.duration_string().normal(),
                &self.name.yellow().bold(),
                &self.args.join(" ").yellow().bold(),
                &self.stdout.as_str().clear(),
                &self.stderr.as_str().clear()
            ),
        }
    }
}

impl PartialOrd for Command {
    fn partial_cmp(&self, other: &Command) -> Option<Ordering> {
        Some(self.cmp(other))
    }
}

impl Ord for Command {
    fn cmp(&self, other: &Command) -> Ordering {
        self.start_utc().cmp(&other.start_utc())
    }
}
impl From<std::io::Error> for Command {
    fn from(error: std::io::Error) -> Self {
        let mut cmd = Command::default();
        cmd.exit_code = 1;
        cmd.stderr = format!("{}", error);
        cmd
    }
}

// TODO: add timeout support: https://docs.rs/wait-timeout/0.2.0/wait_timeout/
fn _exec(cmd: &mut Command) -> Result<Command> {
    let mut command = cmd.clone();
    let now = Instant::now();
    command.env = super::Env::default();

    let mut process_command = std::process::Command::new(&command.name);
    process_command.current_dir(&command.dir);
    process_command.args(get_vec_osstring(&command.args.clone()));
    command.start = Utc::now().to_string();
    command.uuid = format!("{}", Uuid::new_v4());

    // test that the command.name is known by the system
    match Env::which(&command.name) {
        Ok(_) => {}
        Err(e) => {
            warn!(target: "reef","unrecognized command: {}",&command.name);
            command.success = false;
            command.log()?;
            return Err(e);
        }
    };

    command.success = true;
    command.exit_code = 0;

    let output = process_command.output()?;
    command.stdout = match std::str::from_utf8(&output.stdout) {
        Ok(text) => text.to_string(),
        Err(_) => "".to_string(),
    };

    command.stderr = match std::str::from_utf8(&output.stderr) {
        Ok(text) => text.to_string(),
        Err(_) => "".to_string(),
    };

    command.success = output.status.success();
    command.duration = now.elapsed();

    // log the command
    command.log()?;

    match command.success {
        true => Ok(command),
        false => Err(Error::from_kind(ErrorKind::CommandFailed(command))),
    }
}

fn get_vec_osstring<I, S>(args: I) -> Vec<OsString>
where
    I: IntoIterator<Item = S>,
    S: AsRef<OsStr>,
{
    let mut results = Vec::new();
    for arg in args.into_iter() {
        let s: &OsStr = arg.as_ref();
        results.push(s.to_os_string());
    }
    results
}

fn parse_command(command_text: &str) -> (String, Vec<String>) {
    let words: Vec<&str> = command_text.split(' ').collect();
    let mut name = "".to_string();
    let mut args = Vec::new();
    if words.len() > 0 {
        name = words[0].to_string();
        if words.len() > 1 {
            for i in 1..words.len() {
                args.push(words[i].to_string());
            }
        }
    }

    // test for file with content similar to:
    // #! ruby
    // #!/bin/bash
    match super::path::which(&name) {
        Ok(path) => match super::path::shebang(&path) {
            Ok(shebang) => match super::path::which(&shebang) {
                Ok(_) => {
                    let name2 = shebang;
                    let mut args2 = Vec::new();
                    args2.push(path.display().to_string());
                    for arg in args {
                        args2.push(arg);
                    }
                    return (name2, args2);
                }
                Err(_) => {}
            },
            Err(_) => {}
        },
        Err(_) => {}
    }

    (name, args)
}

#[cfg(test)]
#[test]
fn parse_command_test() {
    match super::Env::which("rake") {
        Ok(_) => {
            let (name, args) = parse_command("rake default");
            assert!(name.contains("ruby"), "name");
            assert_eq!(2, args.len(), "args: {:?}", args)
        }
        Err(_) => {}
    }
}