use std::{
collections::HashMap,
fs,
io::Read,
path::{Path, PathBuf},
process::{Child, Command, ExitStatus, Output, Stdio},
time::{Duration, Instant},
};
use anyhow::{anyhow, bail, Result};
use log::{debug, info, log, trace, Level};
use serde_derive::{Deserialize, Serialize};
use crate::{
args::{GenerateGitConfig, LinkOptions, UpdateSelfOptions},
generate, tasks,
tasks::{defaults::DefaultsConfig, git::GitConfig, ResolveEnv, TasksError},
};
#[derive(Debug)]
pub enum TaskStatus {
New,
Blocked,
Running(Child, Instant),
Skipped,
Passed,
Failed(anyhow::Error),
}
#[derive(Debug)]
pub struct Task {
pub name: String,
pub path: PathBuf,
pub config: TaskConfig,
pub start_time: Instant,
pub status: TaskStatus,
}
#[derive(Debug, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct TaskConfig {
pub name: Option<String>,
pub constraints: Option<HashMap<String, String>>,
pub requires: Option<Vec<String>>,
pub auto_run: Option<bool>,
pub run_lib: Option<String>,
pub check_cmd: Option<Vec<String>>,
pub run_cmd: Option<Vec<String>>,
pub data: Option<toml::Value>,
pub description: Option<String>,
}
#[derive(Debug)]
pub enum CommandType {
Check,
Run,
}
impl Task {
pub fn from(path: &Path) -> Result<Self> {
let start_time = Instant::now();
let s = fs::read_to_string(&path).map_err(|e| TasksError::ReadFile {
path: path.to_owned(),
source: e,
})?;
trace!("Task '{:?}' contents: <<<{}>>>", &path, &s);
let config = toml::from_str::<TaskConfig>(&s).map_err(|e| TasksError::InvalidToml {
path: path.to_owned(),
source: e,
})?;
let name = match &config.name {
Some(n) => n.clone(),
None => path
.file_stem()
.ok_or_else(|| anyhow!("Task had no path."))?
.to_str()
.ok_or(TasksError::None {})?
.to_owned(),
};
let status = TaskStatus::New;
let task = Self {
name,
path: path.to_owned(),
config,
status,
start_time,
};
debug!("Task '{}': {:?}", &task.name, task);
Ok(task)
}
pub fn try_start<F>(&mut self, env_fn: F, env: &HashMap<String, String>) -> Result<()>
where
F: Fn(&str) -> Result<String>,
{
self.status = TaskStatus::Blocked;
self.start(env_fn, env)
}
pub fn start<F>(&mut self, env_fn: F, env: &HashMap<String, String>) -> Result<()>
where
F: Fn(&str) -> Result<String>,
{
info!("Running task '{}'", &self.name);
self.status = TaskStatus::Passed;
if let Some(lib) = &self.config.run_lib {
let run_lib_result = match lib.as_str() {
"link" => {
let mut data = self
.config
.data
.as_ref()
.ok_or_else(|| anyhow!("Task '{}' data had no value.", &self.name))?
.clone()
.try_into::<LinkOptions>()?;
data.resolve_env(env_fn)?;
tasks::link::run(data)
}
"git" => {
let mut data = self
.config
.data
.as_ref()
.ok_or_else(|| anyhow!("Task '{}' data had no value.", &self.name))?
.clone()
.try_into::<Vec<GitConfig>>()?;
data.resolve_env(env_fn)?;
tasks::git::run(data)
}
"generate_git" => {
let mut data = self
.config
.data
.as_ref()
.ok_or_else(|| anyhow!("Task '{}' data had no value.", &self.name))?
.clone()
.try_into::<Vec<GenerateGitConfig>>()?;
data.resolve_env(env_fn)?;
generate::git::run(&data)
}
"defaults" => {
let mut data = self
.config
.data
.as_ref()
.ok_or_else(|| anyhow!("Task '{}' data had no value.", &self.name))?
.clone()
.try_into::<DefaultsConfig>()?;
data.resolve_env(env_fn)?;
tasks::defaults::run(data)
}
"self" => {
let options = if let Some(raw_data) = self.config.data.as_ref() {
let mut raw_opts = raw_data.clone().try_into::<UpdateSelfOptions>()?;
raw_opts.resolve_env(env_fn)?;
raw_opts
} else {
UpdateSelfOptions::default()
};
tasks::update_self::run(&options)
}
_ => Err(anyhow!("This run_lib is invalid or not yet implemented.")),
};
if let Err(e) = run_lib_result {
self.status = TaskStatus::Failed(e);
} else {
self.status = TaskStatus::Passed;
}
return Ok(());
}
if let Some(mut cmd) = self.config.check_cmd.clone() {
debug!("Running '{}' check command.", &self.name);
for s in &mut cmd {
*s = env_fn(s)?;
}
let check_output = self.run_check_cmd(&cmd, env)?;
if check_output.status.success() {
debug!("Skipping task '{}' as check command passed.", &self.name);
self.status = TaskStatus::Skipped;
return Ok(());
}
} else {
debug!(
"You haven't specified a check command for '{}', so it will always be run",
&self.name
)
}
if let Some(mut cmd) = self.config.run_cmd.clone() {
debug!("Running '{}' run command.", &self.name);
for s in &mut cmd {
*s = env_fn(s)?;
}
let (child, start_time) = Self::start_command(&cmd, env)?;
self.status = TaskStatus::Running(child, start_time);
return Ok(());
}
bail!(TasksError::MissingCmd {
name: self.name.clone()
});
}
pub fn try_finish(&mut self) -> Result<()> {
let (child, start_time) = match &mut self.status {
TaskStatus::Running(child, start_time) => (child, start_time),
_ => bail!(anyhow!("Can't finish non-running task.")),
};
if let Some(status) = child.try_wait()? {
debug!("Task '{}' complete.", &self.name);
let elapsed_time = start_time.elapsed();
let mut stdout = String::new();
child
.stdout
.as_mut()
.ok_or_else(|| anyhow!("Missing stdout"))?
.read_to_string(&mut stdout)?;
let mut stderr = String::new();
child
.stderr
.as_mut()
.ok_or_else(|| anyhow!("Missing stderr"))?
.read_to_string(&mut stderr)?;
self.log_command_output(CommandType::Run, status, &stdout, &stderr, elapsed_time);
if status.success() {
self.status = TaskStatus::Passed;
} else {
self.status = TaskStatus::Failed(anyhow!("Task {} failed.", self.name));
}
} else {
}
Ok(())
}
pub fn run_check_cmd(&self, cmd: &[String], env: &HashMap<String, String>) -> Result<Output> {
let mut command = Self::get_command(cmd, env)?;
let now = Instant::now();
let output = command.output().map_err(|e| TasksError::CheckCmdFailed {
name: self.name.clone(),
cmd: cmd.into(),
source: e,
})?;
let elapsed_time = now.elapsed();
let stdout = String::from_utf8_lossy(&output.stdout);
let stderr = String::from_utf8_lossy(&output.stderr);
self.log_command_output(
CommandType::Check,
output.status,
&stdout,
&stderr,
elapsed_time,
);
Ok(output)
}
pub fn start_command(
cmd: &[String],
env: &HashMap<String, String>,
) -> Result<(Child, Instant)> {
let command = Self::get_command(cmd, env);
let now = Instant::now();
let child = command?
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.spawn()?;
Ok((child, now))
}
pub fn get_command(cmd: &[String], env: &HashMap<String, String>) -> Result<Command> {
let mut command = Command::new(
&cmd.get(0)
.ok_or_else(|| anyhow!("Task '{}' command was empty."))?,
);
command
.args(cmd.get(1..).unwrap_or(&[]))
.env_clear()
.envs(env.iter())
.stdin(Stdio::inherit());
trace!("Running command: {:?}", &command);
Ok(command)
}
pub fn log_command_output(
&self,
command_type: CommandType,
status: ExitStatus,
stdout: &str,
stderr: &str,
elapsed_time: Duration,
) {
let (level, stdout_stderr_level) = match (command_type, status.success()) {
(_, true) => (Level::Debug, Level::Debug),
(CommandType::Run, false) => (Level::Error, Level::Error),
(CommandType::Check, false) => (Level::Info, Level::Debug),
};
log!(
level,
"Task '{}' command ran in {:?} with status: {}",
&self.name,
elapsed_time,
status
);
if !stdout.is_empty() {
log!(
stdout_stderr_level,
"Task '{}' command stdout:\n<<<\n{}>>>\n",
&self.name,
stdout,
);
}
if !stderr.is_empty() {
log!(
stdout_stderr_level,
"Task '{}' command stderr:\n<<<\n{}>>>\n",
&self.name,
stderr
);
}
}
}