1
use std::io::{
2
  BufRead as _,
3
  BufReader,
4
};
5
use std::process::Command as ProcessCommand;
6
use std::thread;
7

            
8
use anyhow::Context as _;
9
use serde::Deserialize;
10

            
11
use crate::defaults::{
12
  default_ignore_errors,
13
  default_shell,
14
  default_verbose,
15
};
16
use crate::handle_output;
17
use crate::schema::{
18
  get_output_handler,
19
  TaskContext,
20
};
21

            
22
#[derive(Debug, Deserialize)]
23
pub struct LocalRun {
24
  /// The command to run
25
  pub command: String,
26

            
27
  /// The shell to use to run the command
28
  #[serde(default = "default_shell")]
29
  pub shell: String,
30

            
31
  /// The test to run before running command
32
  /// If the test fails, the command will not run
33
  #[serde(default)]
34
  pub test: Option<String>,
35

            
36
  /// The working directory to run the command in
37
  #[serde(default)]
38
  pub work_dir: Option<String>,
39

            
40
  /// Ignore errors if the command fails
41
  #[serde(default)]
42
  pub ignore_errors: Option<bool>,
43

            
44
  /// Show verbose output
45
  #[serde(default)]
46
  pub verbose: Option<bool>,
47
}
48

            
49
impl LocalRun {
50
113
  pub fn execute(&self, context: &TaskContext) -> anyhow::Result<()> {
51
113
    assert!(!self.command.is_empty());
52
113
    assert!(!self.shell.is_empty());
53

            
54
113
    let ignore_errors = self.ignore_errors(context);
55
113
    let verbose = self.verbose(context);
56
113

            
57
113
    // Skip the command if the test fails
58
113
    if self.test(context).is_err() {
59
      return Ok(());
60
113
    }
61
113

            
62
113
    let stdout = get_output_handler(verbose);
63
113
    let stderr = get_output_handler(verbose);
64
113

            
65
113
    let mut cmd = ProcessCommand::new(&self.shell);
66
113
    cmd.arg("-c").arg(&self.command).stdout(stdout).stderr(stderr);
67

            
68
113
    if let Some(work_dir) = &self.work_dir.clone() {
69
      cmd.current_dir(work_dir);
70
113
    }
71

            
72
    // Inject environment variables
73
113
    for (key, value) in context.env_vars.iter() {
74
      cmd.env(key, value);
75
    }
76

            
77
113
    let mut cmd = cmd.spawn()?;
78
113
    if verbose {
79
93
      handle_output!(cmd.stdout, context);
80
93
      handle_output!(cmd.stderr, context);
81
20
    }
82

            
83
113
    let status = cmd.wait()?;
84
113
    if !status.success() && !ignore_errors {
85
      anyhow::bail!("Command failed - {}", self.command);
86
113
    }
87
113

            
88
113
    Ok(())
89
113
  }
90

            
91
113
  fn test(&self, context: &TaskContext) -> anyhow::Result<()> {
92
113
    let verbose = self.verbose(context);
93
113

            
94
113
    let stdout = get_output_handler(verbose);
95
113
    let stderr = get_output_handler(verbose);
96

            
97
113
    if let Some(test) = &self.test {
98
      let mut cmd = ProcessCommand::new(&self.shell);
99
      cmd.arg("-c").arg(test).stdout(stdout).stderr(stderr);
100

            
101
      let mut cmd = cmd.spawn()?;
102
      if verbose {
103
        handle_output!(cmd.stdout, context);
104
        handle_output!(cmd.stderr, context);
105
      }
106

            
107
      let status = cmd.wait()?;
108

            
109
      log::trace!("Test status: {:?}", status.success());
110
      if !status.success() {
111
        anyhow::bail!("Command test failed - {}", test);
112
      }
113
113
    }
114

            
115
113
    Ok(())
116
113
  }
117

            
118
113
  fn ignore_errors(&self, context: &TaskContext) -> bool {
119
113
    self
120
113
      .ignore_errors
121
113
      .or(context.ignore_errors)
122
113
      .unwrap_or(default_ignore_errors())
123
113
  }
124

            
125
226
  fn verbose(&self, context: &TaskContext) -> bool {
126
226
    self.verbose.or(context.verbose).unwrap_or(default_verbose())
127
226
  }
128
}
129

            
130
#[cfg(test)]
131
mod test {
132
  use super::*;
133

            
134
  #[test]
135
20
  fn test_local_run_1() -> anyhow::Result<()> {
136
20
    {
137
20
      let yaml = "
138
20
        command: echo 'Hello, World!'
139
20
        ignore_errors: false
140
20
        verbose: false
141
20
      ";
142
20
      let local_run = serde_yaml::from_str::<LocalRun>(yaml)?;
143

            
144
20
      assert_eq!(local_run.command, "echo 'Hello, World!'");
145
20
      assert_eq!(local_run.shell, "sh");
146
20
      assert_eq!(local_run.work_dir, None);
147
20
      assert_eq!(local_run.ignore_errors, Some(false));
148
20
      assert_eq!(local_run.verbose, Some(false));
149

            
150
20
      Ok(())
151
    }
152
20
  }
153

            
154
  #[test]
155
20
  fn test_local_run_2() -> anyhow::Result<()> {
156
20
    {
157
20
      let yaml = "
158
20
        command: echo 'Hello, World!'
159
20
        test: test $(uname) = 'Linux'
160
20
        ignore_errors: false
161
20
        verbose: false
162
20
      ";
163
20
      let local_run = serde_yaml::from_str::<LocalRun>(yaml)?;
164

            
165
20
      assert_eq!(local_run.command, "echo 'Hello, World!'");
166
20
      assert_eq!(local_run.test, Some("test $(uname) = 'Linux'".to_string()));
167
20
      assert_eq!(local_run.shell, "sh");
168
20
      assert_eq!(local_run.work_dir, None);
169
20
      assert_eq!(local_run.ignore_errors, Some(false));
170
20
      assert_eq!(local_run.verbose, Some(false));
171

            
172
20
      Ok(())
173
    }
174
20
  }
175
}