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)
}
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
}
}
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());
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();
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());
}
}
}
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(_) => {}
}
}