use crate::storage::Storage;
use crate::{LogLevel, Status};
use anyhow::{anyhow, Context, Result};
use chrono::{DateTime, Utc};
use colored::*;
use log::{debug, error, info, trace, warn};
use serde::{Deserialize, Serialize};
use std::fmt::{Debug, Display, Formatter};
use std::path::{Path, PathBuf};
use std::process::Stdio;
use std::time::{Duration, Instant};
use wait_timeout::ChildExt;
#[derive(Clone, Default, Serialize, Deserialize)]
pub struct Command {
name: String,
args: Vec<String>,
exe_filename: PathBuf,
working_dir: Option<PathBuf>,
timeout: Option<Duration>,
timed_out: bool,
stdout: Vec<u8>,
stderr: Vec<u8>,
exit_code: Option<i32>,
duration: Option<Duration>,
start: Option<DateTime<Utc>>,
user: String,
machine: String,
os: String,
#[serde(skip)]
success_log_level: LogLevel,
#[serde(skip)]
fail_log_level: LogLevel,
}
impl Command {
pub fn new(name: &str) -> Self {
let mut d = Command::default();
let parts: Vec<&str> = name.trim().split_whitespace().collect();
let mut index = 0;
for part in parts {
if index == 0 {
d.name = part.to_string();
} else {
d.arg(part);
}
index += 1;
}
d.user = whoami::username();
d.machine = whoami::hostname();
d.os = whoami::distro();
d
}
pub fn collect(search: &str, limit: usize) -> Result<Vec<Command>> {
Storage::search(search, limit)
.with_context(|| format!("Command::collect('{}',{})", search, limit))
}
fn which(name: &str) -> Result<PathBuf> {
let extensions = vec![".exe", ".bat", ".cmd", ""];
for ext in extensions.iter() {
let exe_name = format!("{}{}", name, ext);
match Command::which_exact(&exe_name) {
Some(path) => {
return Ok(path);
}
None => {}
};
}
Err(anyhow!("unrecognized command '{}'", name))
}
fn which_exact(name: &str) -> Option<PathBuf> {
std::env::var_os("PATH").and_then(|paths| {
std::env::split_paths(&paths)
.filter_map(|dir| {
let full_path = dir.join(&name);
if full_path.is_file() {
Some(full_path)
} else {
None
}
})
.next()
})
}
}
impl Command {
pub fn name(&self) -> &str {
&self.name
}
pub fn args(&self) -> &Vec<String> {
&self.args
}
pub fn exe_filename(&self) -> &Path {
&self.exe_filename
}
pub fn working_directory(&self) -> Option<&Path> {
match &self.working_dir {
Some(path) => Some(&path),
None => None,
}
}
pub fn get_timeout(&self) -> Option<&Duration> {
match &self.timeout {
Some(timeout) => Some(&timeout),
None => None,
}
}
pub fn stderr(&self) -> &Vec<u8> {
&self.stderr
}
pub fn timed_out(&self) -> bool {
self.timed_out
}
pub fn exit_code(&self) -> Option<i32> {
self.exit_code
}
pub fn start(&self) -> &Option<DateTime<Utc>> {
&self.start
}
pub fn duration(&self) -> Option<Duration> {
self.duration
}
pub fn duration_string(&self) -> String {
let duration = match self.duration {
Some(duration) => {
let ms = duration.as_millis();
let mut elapsed = format!("{} ms", ms);
if ms > 999 {
if ms < 90000 {
elapsed = format!("{} s", ms / 1000);
} else {
elapsed = format!("{} m", ms / 60000);
}
}
elapsed
}
None => format!(""),
};
duration
}
pub fn stdout(&self) -> &Vec<u8> {
&self.stdout
}
pub fn text(&self) -> Result<String> {
Ok(format!(
"{}\n{}",
std::str::from_utf8(&self.stdout)?,
std::str::from_utf8(&self.stderr)?
)
.trim()
.to_string())
}
pub fn status(&self) -> Status {
if self.timed_out {
Status::TimedOut
} else {
match self.exit_code() {
Some(exit_code) => {
if exit_code == 0 {
Status::Ok
} else {
Status::Error
}
}
None => Status::Unknown,
}
}
}
pub fn user(&self) -> &str {
&self.user
}
pub fn machine(&self) -> &str {
&self.machine
}
pub fn os(&self) -> &str {
&self.os
}
pub fn matches(&self, search: &str) -> bool {
if search.len() == 0 {
true
} else {
let words = search.split_whitespace();
for word in words {
println!("word: {}", word);
if self.name.contains(word) {
return true;
}
match self.text() {
Ok(text) => {
if text.contains(word) {
return true;
}
}
Err(_) => {}
}
for arg in &self.args {
if arg.contains(word) {
return true;
}
}
}
false
}
}
}
impl Command {
pub fn arg(&mut self, arg: &str) -> &mut Command {
self.args.push(arg.to_string());
self
}
pub fn current_dir<P: AsRef<Path>>(&mut self, dir: P) -> &mut Command {
self.working_dir = Some(dir.as_ref().to_path_buf());
self
}
pub fn timeout(&mut self, timeout: Duration) -> &mut Command {
self.timeout = Some(timeout);
self
}
pub fn success_log_level(&mut self, level_filter: log::LevelFilter) -> &mut Command {
self.success_log_level = LogLevel::new(level_filter);
self
}
pub fn fail_log_level(&mut self, level_filter: log::LevelFilter) -> &mut Command {
self.fail_log_level = LogLevel::new(level_filter);
self
}
fn output(&mut self) -> Result<std::process::Output> {
match self.timeout {
Some(timeout) => {
let mut cmd = std::process::Command::new(&self.exe_filename);
for arg in &self.args {
cmd.arg(&arg);
}
match &self.working_dir {
Some(dir) => {
cmd.current_dir(dir);
}
None => {}
};
let mut child = cmd
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.spawn()
.with_context(|| format!("spawning {} {}", &self.name, &self.args.join(" ")))?;
match child.wait_timeout(timeout)? {
Some(_) => {}
None => {
child.kill()?;
child.wait()?;
self.timed_out = true;
}
};
Ok(child.wait_with_output()?)
}
None => {
let mut cmd = std::process::Command::new(&self.exe_filename);
for arg in &self.args {
cmd.arg(&arg);
}
match &self.working_dir {
Some(dir) => {
cmd.current_dir(dir);
}
None => {}
};
Ok(cmd
.output()
.with_context(|| format!("spawning {} {}", &self.name, &self.args.join(" ")))?)
}
}
}
pub fn exec(&mut self) -> Result<Command> {
let start_instant = Instant::now();
let start = Utc::now();
let exe_filename = Command::which(&self.name)?;
if exe_filename.exists() {
self.exe_filename = exe_filename.to_path_buf();
let output = self.output()?;
let command = Command {
exe_filename: exe_filename,
working_dir: match &self.working_dir {
Some(dir) => Some(dir.to_path_buf()),
None => None,
},
name: self.name.to_string(),
args: self.args.clone(),
stdout: output.stdout.clone(),
stderr: output.stderr.clone(),
exit_code: output.status.code(),
duration: Some(start_instant.elapsed()),
timeout: self.timeout,
timed_out: self.timed_out,
start: Some(start),
user: whoami::username(),
machine: whoami::hostname(),
os: whoami::distro(),
success_log_level: self.success_log_level,
fail_log_level: self.fail_log_level,
};
Storage::add(&command)?;
command.log();
Ok(command)
} else {
Err(anyhow!(
"unable to determine executable filename for {}",
&self.name
))
}
}
pub fn set_stdout(&mut self, stdout: Vec<u8>) {
self.stdout = stdout
}
pub fn set_stderr(&mut self, stderr: Vec<u8>) {
self.stderr = stderr
}
fn log(&self) {
match self.exit_code() {
Some(exit_code) => {
if exit_code == 0 {
Command::log_msg(&format!("{}", &self), self.success_log_level);
} else {
Command::log_msg(&format!("{}", &self), self.fail_log_level);
}
}
None => {}
}
}
fn log_msg(msg: &str, level: LogLevel) {
match level {
LogLevel::Off => {}
LogLevel::Trace => {
trace!("{}", msg);
}
LogLevel::Debug => {
debug!("{}", msg);
}
LogLevel::Info => {
info!("{}", msg);
}
LogLevel::Warn => {
warn!("{}", msg);
}
LogLevel::Error => {
error!("{}", msg);
}
}
}
}
impl Display for Command {
fn fmt(&self, f: &mut Formatter) -> std::fmt::Result {
let duration = self.duration_string();
match &self.working_dir {
Some(dir) => {
let cur_dir = format!("{}", dir.display());
write!(
f,
"{} {} {} ({},{})",
&self.status(),
&self.name().yellow().bold(),
&self.args.join(" ").white().bold(),
cur_dir,
duration
)
}
None => write!(
f,
"{} {} {} ({})",
&self.status(),
&self.name().yellow().bold(),
&self.args.join(" ").white().bold(),
duration,
),
}
}
}
impl Debug for Command {
fn fmt(&self, f: &mut Formatter) -> std::fmt::Result {
write!(f, "\n{:>15}: {}", "name", &self.name().green())?;
if self.args.len() > 0 {
write!(f, "\n{:>15}: {}", "args", &self.args.join(" ").green())?;
}
write!(f, "\n{:>15}: {:#?}", "exe filename", &self.exe_filename)?;
match &self.working_dir {
Some(dir) => {
write!(f, "\n{:>15}: {}", "dir", dir.display())?;
}
None => {}
};
match &self.timeout {
Some(timeout) => {
write!(f, "\n{:>15}: {:#?}", "timeout", timeout)?;
write!(f, "\n{:>15}: {}", "timed out", self.timed_out)?;
}
None => {}
}
match &self.exit_code {
Some(exit_code) => {
write!(f, "\n{:>15}: {}", "exit code", exit_code)?;
}
None => {}
}
match &self.start() {
Some(dt) => {
write!(f, "\n{:>15}: {}", "start", dt)?;
}
None => {}
}
match &self.duration() {
Some(duration) => {
write!(f, "\n{:>15}: {:#?}", "duration", duration)?;
}
None => {}
}
let text = match self.text() {
Ok(t) => t,
Err(_) => format!("[{} bytes]", self.stdout.len() + self.stderr.len()),
};
write!(
f,
"\n{:>15}: {}",
"output",
text.split("\n")
.collect::<Vec<&str>>()
.join("\n ")
.trim()
)
}
}