Skip to main content

doge_runtime/stdlib/
chase.rs

1//! `chase` — run external programs. `chase.run(cmd, args, stdin)` spawns `cmd`
2//! with its argument list, optionally feeds it `stdin`, waits for it to finish,
3//! and gives back a Dict `{"code", "stdout", "stderr"}`. Every failure to launch
4//! the program — a missing binary, a permission problem — is a catchable IOError
5//! rather than a panic, and output that is not valid text is an IOError too (as in
6//! `fetch`/`howl`). Arguments are type-checked before anything is spawned, so a
7//! wrong type never leaves a stray process behind.
8
9use std::io::Write;
10use std::process::{Command, Stdio};
11
12use crate::error::{DogeError, DogeResult};
13use crate::ordered_map::OrderedMap;
14use crate::stdlib::str_arg;
15use crate::value::Value;
16
17/// The exit code reported when a child is terminated by a signal and so has no
18/// ordinary exit status of its own.
19const SIGNAL_EXIT_CODE: i64 = -1;
20
21/// `chase.run(cmd, args, stdin)` — run `cmd` with the Str `args`, feeding it the
22/// Str `stdin` (or nothing when `stdin` is `none`), and give back a Dict
23/// `{"code": Int, "stdout": Str, "stderr": Str}`. Failing to launch the program,
24/// or output that is not valid text, is a catchable IOError.
25pub fn chase_run(cmd: &Value, args: &Value, stdin: &Value) -> DogeResult {
26    let cmd = str_arg("chase", "run", cmd)?;
27    let args = arg_list(args)?;
28    let stdin = stdin_text(stdin)?;
29
30    let mut command = Command::new(cmd);
31    command.args(&args);
32    command.stdout(Stdio::piped());
33    command.stderr(Stdio::piped());
34    command.stdin(match stdin {
35        Some(_) => Stdio::piped(),
36        None => Stdio::null(),
37    });
38
39    let mut child = command
40        .spawn()
41        .map_err(|err| DogeError::io_error(format!("cannot run {cmd}: {err}")))?;
42
43    // Feed stdin on its own thread so a child that writes a lot of output before
44    // reading all of its input cannot deadlock against us (both pipes would block).
45    // A child that stops reading early (`true`, `head`) makes the write fail with a
46    // broken pipe; that is normal, exactly as a shell ignores SIGPIPE here, so the
47    // writer swallows any write error.
48    let writer = stdin.map(|text| {
49        let mut handle = child.stdin.take();
50        std::thread::spawn(move || {
51            if let Some(pipe) = handle.as_mut() {
52                let _ = pipe.write_all(text.as_bytes());
53            }
54        })
55    });
56
57    let output = child
58        .wait_with_output()
59        .map_err(|err| DogeError::io_error(format!("cannot run {cmd}: {err}")))?;
60    if let Some(writer) = writer {
61        let _ = writer.join();
62    }
63
64    let code = output.status.code().map_or(SIGNAL_EXIT_CODE, i64::from);
65    let stdout = decode(cmd, "stdout", output.stdout)?;
66    let stderr = decode(cmd, "stderr", output.stderr)?;
67
68    let mut result = OrderedMap::new();
69    result.insert("code".to_string(), Value::int(code));
70    result.insert("stdout".to_string(), Value::str(stdout));
71    result.insert("stderr".to_string(), Value::str(stderr));
72    Ok(Value::dict(result))
73}
74
75/// The `args` argument as a `Vec<String>`: a List whose every element is a Str.
76/// Anything else is a catchable type error.
77fn arg_list(args: &Value) -> DogeResult<Vec<String>> {
78    let items = match args {
79        Value::List(items) => items.borrow(),
80        _ => {
81            return Err(DogeError::type_error(format!(
82                "chase.run needs a List of Str for its args, got {}",
83                args.describe()
84            )))
85        }
86    };
87    let mut out = Vec::with_capacity(items.len());
88    for item in items.iter() {
89        match item {
90            Value::Str(s) => out.push(s.to_string()),
91            other => {
92                return Err(DogeError::type_error(format!(
93                    "chase.run needs a List of Str for its args, got a {} element",
94                    other.describe()
95                )))
96            }
97        }
98    }
99    Ok(out)
100}
101
102/// The `stdin` argument: `Some(text)` for a Str to feed, `None` for `none` (no
103/// input). Any other type is a catchable type error.
104fn stdin_text(stdin: &Value) -> DogeResult<Option<String>> {
105    match stdin {
106        Value::Str(s) => Ok(Some(s.to_string())),
107        Value::None => Ok(None),
108        other => Err(DogeError::type_error(format!(
109            "chase.run needs a Str or none for its stdin, got {}",
110            other.describe()
111        ))),
112    }
113}
114
115/// Captured output bytes as text, or a catchable IOError naming the stream when
116/// they are not valid UTF-8.
117fn decode(cmd: &str, stream: &str, bytes: Vec<u8>) -> DogeResult<String> {
118    String::from_utf8(bytes)
119        .map_err(|_| DogeError::io_error(format!("{cmd} wrote non-text bytes to {stream}")))
120}
121
122#[cfg(test)]
123mod tests {
124    use super::*;
125    use crate::error::ErrorKind;
126    use bigdecimal::ToPrimitive;
127
128    fn run(cmd: &str, args: &[&str], stdin: Value) -> DogeResult {
129        let args = Value::list(args.iter().map(Value::str).collect());
130        chase_run(&Value::str(cmd), &args, &stdin)
131    }
132
133    /// Assert a Str/Int field of the result dict against an expected value,
134    /// keeping the `RefCell` borrow local to this helper.
135    fn assert_str(dict: &Value, key: &str, expected: &str) {
136        match dict {
137            Value::Dict(entries) => match entries.borrow().get(key) {
138                Some(Value::Str(s)) => assert_eq!(&**s, expected, "{key}"),
139                other => panic!("expected a Str {key}, got {other:?}"),
140            },
141            _ => panic!("expected a dict"),
142        }
143    }
144
145    fn assert_code(dict: &Value, expected: i64) {
146        match dict {
147            Value::Dict(entries) => match entries.borrow().get("code") {
148                Some(Value::Int(n)) => assert_eq!(n.to_i64().unwrap(), expected, "code"),
149                other => panic!("expected an Int code, got {other:?}"),
150            },
151            _ => panic!("expected a dict"),
152        }
153    }
154
155    #[test]
156    fn captures_stdout_and_a_zero_code() {
157        let out = run("printf", &["much wow"], Value::None).unwrap();
158        assert_code(&out, 0);
159        assert_str(&out, "stdout", "much wow");
160        assert_str(&out, "stderr", "");
161    }
162
163    #[test]
164    fn passes_arguments_through() {
165        let out = run("printf", &["%s-%s", "such", "wow"], Value::None).unwrap();
166        assert_str(&out, "stdout", "such-wow");
167    }
168
169    #[test]
170    fn feeds_stdin_to_the_child() {
171        let out = run("cat", &[], Value::str("such stdin")).unwrap();
172        assert_str(&out, "stdout", "such stdin");
173    }
174
175    #[test]
176    fn a_child_that_ignores_stdin_does_not_hang_or_error() {
177        let out = run("true", &[], Value::str("wasted input")).unwrap();
178        assert_code(&out, 0);
179    }
180
181    #[test]
182    fn reports_a_nonzero_exit_code() {
183        let out = run("false", &[], Value::None).unwrap();
184        assert_code(&out, 1);
185    }
186
187    #[test]
188    fn a_missing_program_is_a_catchable_io_error() {
189        let err = run("doge-no-such-prog-xyz", &[], Value::None).unwrap_err();
190        assert_eq!(err.kind, ErrorKind::IOError);
191    }
192
193    #[test]
194    fn a_non_str_command_is_a_type_error() {
195        let err = chase_run(&Value::int(1), &Value::list(vec![]), &Value::None).unwrap_err();
196        assert_eq!(err.kind, ErrorKind::TypeError);
197    }
198
199    #[test]
200    fn non_list_args_is_a_type_error() {
201        let err = chase_run(&Value::str("echo"), &Value::int(1), &Value::None).unwrap_err();
202        assert_eq!(err.kind, ErrorKind::TypeError);
203    }
204
205    #[test]
206    fn a_non_str_args_element_is_a_type_error() {
207        let args = Value::list(vec![Value::int(1)]);
208        let err = chase_run(&Value::str("echo"), &args, &Value::None).unwrap_err();
209        assert_eq!(err.kind, ErrorKind::TypeError);
210    }
211
212    #[test]
213    fn a_non_str_non_none_stdin_is_a_type_error() {
214        let err = chase_run(&Value::str("cat"), &Value::list(vec![]), &Value::int(1)).unwrap_err();
215        assert_eq!(err.kind, ErrorKind::TypeError);
216    }
217}