use std::io::{
BufRead as _,
BufReader,
};
use std::process::Stdio;
use std::thread;
use anyhow::Context as _;
use indicatif::ProgressDrawTarget;
use serde::Deserialize;
use crate::defaults::{
default_ignore_errors,
default_verbose,
};
use crate::handle_output;
use crate::schema::{
get_output_handler,
interpolate_template_string,
Shell,
TaskContext,
};
#[derive(Debug, Deserialize, Clone)]
pub struct LocalRun {
pub command: String,
#[serde(default)]
pub shell: Option<Shell>,
#[serde(default)]
pub test: Option<String>,
#[serde(default)]
pub work_dir: Option<String>,
#[serde(default)]
pub interactive: Option<bool>,
#[serde(default)]
pub ignore_errors: Option<bool>,
#[serde(default)]
pub save_output_as: Option<String>,
#[serde(default)]
pub verbose: Option<bool>,
}
impl LocalRun {
pub fn execute(&self, context: &TaskContext) -> anyhow::Result<()> {
assert!(!self.command.is_empty());
let command = interpolate_template_string(&self.command, context)?;
let interactive = self.interactive();
let ignore_errors = self.ignore_errors(context);
let capture_output = self.save_output_as.is_some();
let verbose = interactive || self.verbose(context);
if self.test(context).is_err() {
return Ok(());
}
let mut cmd = self
.shell
.as_ref()
.map(|shell| shell.proc())
.unwrap_or_else(|| context.shell().proc());
cmd.arg(&command);
if capture_output {
cmd.stdout(Stdio::piped());
if interactive {
context.multi.set_draw_target(ProgressDrawTarget::hidden());
cmd.stdin(Stdio::inherit()).stderr(Stdio::inherit());
} else {
cmd.stderr(get_output_handler(verbose));
}
} else if verbose {
if interactive {
context.multi.set_draw_target(ProgressDrawTarget::hidden());
cmd
.stdin(Stdio::inherit())
.stdout(Stdio::inherit())
.stderr(Stdio::inherit());
} else {
let stdout = get_output_handler(verbose);
let stderr = get_output_handler(verbose);
cmd.stdout(stdout).stderr(stderr);
}
}
if let Some(work_dir) = self.resolved_work_dir(context) {
cmd.current_dir(work_dir);
}
for (key, value) in context.env_vars.iter() {
cmd.env(key, value);
}
let mut cmd = cmd.spawn()?;
let stdout_handle = if capture_output {
let stdout = cmd.stdout.take().context("Failed to open stdout")?;
let multi = context.multi.clone();
Some(thread::spawn(move || -> anyhow::Result<String> {
let reader = BufReader::new(stdout);
let mut output = String::new();
for line in reader.lines() {
let line = line?;
if verbose {
let _ = multi.println(line.clone());
}
output.push_str(&line);
output.push('\n');
}
Ok(output.trim_end_matches(['\r', '\n']).to_string())
}))
} else {
None
};
if verbose && !interactive && !capture_output {
handle_output!(cmd.stdout, context);
handle_output!(cmd.stderr, context);
} else if verbose && !interactive && capture_output {
handle_output!(cmd.stderr, context);
}
let status = cmd.wait()?;
let captured_stdout = match stdout_handle {
Some(handle) => Some(
handle
.join()
.map_err(|_| anyhow::anyhow!("Failed to join stdout capture thread"))??,
),
None => None,
};
if !status.success() && !ignore_errors {
anyhow::bail!("Command failed - {}", command);
}
if status.success() {
if let (Some(output_name), Some(output_value)) = (&self.save_output_as, captured_stdout) {
context.insert_task_output(output_name.clone(), output_value)?;
}
}
Ok(())
}
pub fn is_parallel_safe(&self) -> bool {
!self.interactive()
}
fn test(&self, context: &TaskContext) -> anyhow::Result<()> {
let verbose = self.verbose(context);
let stdout = get_output_handler(verbose);
let stderr = get_output_handler(verbose);
if let Some(test) = &self.test {
let test = interpolate_template_string(test, context)?;
let mut cmd = self
.shell
.as_ref()
.map(|shell| shell.proc())
.unwrap_or_else(|| context.shell().proc());
cmd.arg(&test).stdout(stdout).stderr(stderr);
if let Some(work_dir) = self.resolved_work_dir(context) {
cmd.current_dir(work_dir);
}
let mut cmd = cmd.spawn()?;
if verbose {
handle_output!(cmd.stdout, context);
handle_output!(cmd.stderr, context);
}
let status = cmd.wait()?;
log::trace!("Test status: {:?}", status.success());
if !status.success() {
anyhow::bail!("Command test failed - {}", test);
}
}
Ok(())
}
fn interactive(&self) -> bool {
self.interactive.unwrap_or(false)
}
fn ignore_errors(&self, context: &TaskContext) -> bool {
self
.ignore_errors
.or(context.ignore_errors)
.unwrap_or(default_ignore_errors())
}
fn verbose(&self, context: &TaskContext) -> bool {
self.verbose.or(context.verbose).unwrap_or(default_verbose())
}
pub fn resolved_work_dir(&self, context: &TaskContext) -> Option<std::path::PathBuf> {
self
.work_dir
.as_ref()
.map(|work_dir| context.resolve_from_config(work_dir))
}
}
#[cfg(test)]
mod test {
use super::*;
#[test]
fn test_local_run_1() -> anyhow::Result<()> {
{
let yaml = "
command: echo 'Hello, World!'
ignore_errors: false
verbose: false
";
let local_run = serde_yaml::from_str::<LocalRun>(yaml)?;
assert_eq!(local_run.command, "echo 'Hello, World!'");
assert_eq!(local_run.work_dir, None);
assert_eq!(local_run.ignore_errors, Some(false));
assert_eq!(local_run.verbose, Some(false));
assert_eq!(local_run.save_output_as, None);
Ok(())
}
}
#[test]
fn test_local_run_2() -> anyhow::Result<()> {
{
let yaml = "
command: echo 'Hello, World!'
test: test $(uname) = 'Linux'
ignore_errors: false
verbose: false
";
let local_run = serde_yaml::from_str::<LocalRun>(yaml)?;
assert_eq!(local_run.command, "echo 'Hello, World!'");
assert_eq!(local_run.test, Some("test $(uname) = 'Linux'".to_string()));
assert_eq!(local_run.work_dir, None);
assert_eq!(local_run.ignore_errors, Some(false));
assert_eq!(local_run.verbose, Some(false));
assert_eq!(local_run.save_output_as, None);
Ok(())
}
}
#[test]
fn test_local_run_3() -> anyhow::Result<()> {
{
let yaml = "
command: echo 'Hello, World!'
test: test $(uname) = 'Linux'
shell: bash
ignore_errors: false
verbose: false
interactive: true
";
let local_run = serde_yaml::from_str::<LocalRun>(yaml)?;
assert_eq!(local_run.command, "echo 'Hello, World!'");
assert_eq!(local_run.test, Some("test $(uname) = 'Linux'".to_string()));
assert_eq!(local_run.shell, Some(Shell::String("bash".to_string())));
assert_eq!(local_run.work_dir, None);
assert_eq!(local_run.ignore_errors, Some(false));
assert_eq!(local_run.verbose, Some(false));
assert_eq!(local_run.interactive, Some(true));
assert_eq!(local_run.save_output_as, None);
Ok(())
}
}
}