1
use anyhow::Context as _;
2
use schemars::JsonSchema;
3
use serde::Deserialize;
4
use std::io::{
5
  BufRead as _,
6
  BufReader,
7
};
8
use std::thread;
9

            
10
use super::{
11
  Shell,
12
  TaskContext,
13
};
14
use crate::defaults::default_verbose;
15
use crate::handle_output;
16
use crate::schema::get_output_handler;
17

            
18
/// This struct represents a precondition that must be met before a task can be
19
/// executed.
20
#[derive(Debug, Default, Deserialize, JsonSchema)]
21
pub struct Precondition {
22
  /// The command to run
23
  pub command: String,
24

            
25
  /// The message to display if the command fails
26
  #[serde(default)]
27
  pub message: Option<String>,
28

            
29
  /// The shell to use to run the command
30
  #[serde(default)]
31
  pub shell: Option<Shell>,
32

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

            
37
  /// Show verbose output
38
  #[serde(default)]
39
  pub verbose: Option<bool>,
40
}
41

            
42
impl Precondition {
43
  pub fn execute(&self, context: &TaskContext) -> anyhow::Result<()> {
44
    assert!(!self.command.is_empty());
45

            
46
    let verbose = self.verbose(context);
47

            
48
    let stdout = get_output_handler(verbose);
49
    let stderr = get_output_handler(verbose);
50

            
51
    let mut cmd = self
52
      .shell
53
      .as_ref()
54
      .map(|shell| shell.proc())
55
      .unwrap_or_else(|| context.shell().proc());
56

            
57
    cmd.arg(self.command.clone()).stdout(stdout).stderr(stderr);
58

            
59
    if let Some(work_dir) = self.resolved_work_dir(context) {
60
      cmd.current_dir(work_dir);
61
    }
62

            
63
    // Inject environment variables
64
    for (key, value) in context.env_vars.iter() {
65
      cmd.env(key, value);
66
    }
67

            
68
    let mut cmd = cmd.spawn()?;
69

            
70
    if verbose {
71
      handle_output!(cmd.stdout, context);
72
      handle_output!(cmd.stderr, context);
73
    }
74

            
75
    let status = cmd.wait()?;
76
    if !status.success() {
77
      if let Some(message) = &self.message {
78
        anyhow::bail!("Precondition failed - {}", message);
79
      } else {
80
        anyhow::bail!("Precondition failed - {}", self.command);
81
      }
82
    }
83

            
84
    Ok(())
85
  }
86

            
87
  fn verbose(&self, context: &TaskContext) -> bool {
88
    self.verbose.or(context.verbose).unwrap_or(default_verbose())
89
  }
90

            
91
  fn resolved_work_dir(&self, context: &TaskContext) -> Option<std::path::PathBuf> {
92
    self
93
      .work_dir
94
      .as_ref()
95
      .map(|work_dir| context.resolve_from_config(work_dir))
96
  }
97
}
98

            
99
#[cfg(test)]
100
mod test {
101
  use super::*;
102

            
103
  #[test]
104
2
  fn test_precondition_1() -> anyhow::Result<()> {
105
    {
106
2
      let yaml = "
107
2
        command: 'echo \"Hello, World!\"'
108
2
        message: 'This is a message'
109
2
      ";
110
2
      let precondition = serde_yaml::from_str::<Precondition>(yaml)?;
111

            
112
2
      assert_eq!(precondition.command, "echo \"Hello, World!\"");
113
2
      assert_eq!(precondition.message, Some("This is a message".into()));
114
2
      assert_eq!(precondition.work_dir, None);
115
2
      assert_eq!(precondition.verbose, None);
116

            
117
2
      Ok(())
118
    }
119
2
  }
120

            
121
  #[test]
122
2
  fn test_precondition_2() -> anyhow::Result<()> {
123
    {
124
2
      let yaml = "
125
2
        command: 'echo \"Hello, World!\"'
126
2
      ";
127
2
      let precondition = serde_yaml::from_str::<Precondition>(yaml)?;
128

            
129
2
      assert_eq!(precondition.command, "echo \"Hello, World!\"");
130
2
      assert_eq!(precondition.message, None);
131
2
      assert_eq!(precondition.work_dir, None);
132
2
      assert_eq!(precondition.verbose, None);
133

            
134
2
      Ok(())
135
    }
136
2
  }
137

            
138
  #[test]
139
2
  fn test_precondition_3() -> anyhow::Result<()> {
140
    {
141
2
      let yaml = "
142
2
        command: 'echo \"Hello, World!\"'
143
2
        message: null
144
2
      ";
145
2
      let precondition = serde_yaml::from_str::<Precondition>(yaml)?;
146

            
147
2
      assert_eq!(precondition.command, "echo \"Hello, World!\"");
148
2
      assert_eq!(precondition.message, None);
149
2
      assert_eq!(precondition.work_dir, None);
150
2
      assert_eq!(precondition.verbose, None);
151

            
152
2
      Ok(())
153
    }
154
2
  }
155

            
156
  #[test]
157
2
  fn test_precondition_4() -> anyhow::Result<()> {
158
    {
159
2
      let yaml = "
160
2
        command: 'echo \"Hello, World!\"'
161
2
        work_dir: /tmp
162
2
      ";
163
2
      let precondition = serde_yaml::from_str::<Precondition>(yaml)?;
164

            
165
2
      assert_eq!(precondition.command, "echo \"Hello, World!\"");
166
2
      assert_eq!(precondition.message, None);
167
2
      assert_eq!(precondition.work_dir, Some("/tmp".into()));
168
2
      assert_eq!(precondition.verbose, None);
169

            
170
2
      Ok(())
171
    }
172
2
  }
173

            
174
  #[test]
175
2
  fn test_precondition_5() -> anyhow::Result<()> {
176
    {
177
2
      let yaml = "
178
2
        command: 'echo \"Hello, World!\"'
179
2
        verbose: true
180
2
      ";
181
2
      let precondition = serde_yaml::from_str::<Precondition>(yaml)?;
182

            
183
2
      assert_eq!(precondition.command, "echo \"Hello, World!\"");
184
2
      assert_eq!(precondition.message, None);
185
2
      assert_eq!(precondition.work_dir, None);
186
2
      assert_eq!(precondition.verbose, Some(true));
187

            
188
2
      Ok(())
189
    }
190
2
  }
191

            
192
  #[test]
193
2
  fn test_precondition_6() -> anyhow::Result<()> {
194
    {
195
2
      let yaml = "
196
2
        command: ls -la
197
2
        message: Listing directory contents
198
2
        shell: bash
199
2
        work_dir: /tmp
200
2
        verbose: false
201
2
      ";
202
2
      let precondition = serde_yaml::from_str::<Precondition>(yaml)?;
203

            
204
2
      assert_eq!(precondition.command, "ls -la");
205
2
      assert_eq!(
206
        precondition.message,
207
2
        Some("Listing directory contents".to_string())
208
      );
209
2
      if let Some(shell) = precondition.shell {
210
2
        assert_eq!(shell.cmd(), "bash".to_string());
211
      } else {
212
        panic!("Expected shell to be Some");
213
      }
214
2
      assert_eq!(precondition.work_dir, Some("/tmp".into()));
215
2
      assert_eq!(precondition.verbose, Some(false));
216

            
217
2
      Ok(())
218
    }
219
2
  }
220
}