1use std::io::Write;
17use std::process::{Command, Output, Stdio};
18use std::sync::{Arc, Mutex};
19
20use cljrs_gc::GcPtr;
21use cljrs_runtime::env::env::GlobalEnv;
22use cljrs_value::{Arity, Keyword, MapValue, NativeFn, Value, ValueError, ValueResult};
23
24pub const RUN_NAME: &str = "clojure.rust.process/run";
27
28pub fn register(globals: &Arc<GlobalEnv>, ns: &str) {
29 let nf = NativeFn::new(RUN_NAME, Arity::Variadic { min: 1 }, builtin_run);
30 globals.intern(ns, Arc::from("run"), Value::NativeFunction(GcPtr::new(nf)));
31}
32
33#[derive(Debug, Clone, PartialEq)]
37pub struct Request {
38 pub argv: Vec<String>,
39 pub stdin: Option<String>,
40 pub dir: Option<String>,
41 pub env: Option<Vec<(String, String)>>,
42 pub out_bytes: bool,
43}
44
45fn kw(name: &str) -> Value {
48 Value::keyword(Keyword::simple(name))
49}
50
51fn arg_error(detail: &str) -> ValueError {
52 ValueError::Other(format!("clojure.rust.process/run: {detail}"))
53}
54
55fn env_name(k: &Value) -> Option<String> {
57 let name = match k {
58 Value::Str(s) => s.get().clone(),
59 Value::Keyword(k) => k.get().name.to_string(),
60 _ => return None,
61 };
62 let valid = !name.is_empty() && !name.contains('=') && !name.contains('\0');
63 valid.then_some(name)
64}
65
66fn opt_string(opts: &MapValue, key: &str) -> ValueResult<Option<String>> {
67 match opts.get(&kw(key)) {
68 None | Some(Value::Nil) => Ok(None),
69 Some(Value::Str(s)) => Ok(Some(s.get().clone())),
70 Some(_) => Err(arg_error(&format!("the :{key} option must be a string"))),
71 }
72}
73
74fn collect_argv(v: &Value) -> ValueResult<Vec<String>> {
75 let bad = || arg_error("the command must be a non-empty vector of strings");
76 let argv: Vec<String> = match v {
77 Value::Vector(v) => v
78 .get()
79 .iter()
80 .map(|e| match e {
81 Value::Str(s) => Ok(s.get().clone()),
82 _ => Err(arg_error("every command element must be a string")),
83 })
84 .collect::<ValueResult<_>>()?,
85 _ => return Err(bad()),
86 };
87 if argv.is_empty() {
88 Err(bad())
89 } else {
90 Ok(argv)
91 }
92}
93
94fn collect_env(opts: &MapValue) -> ValueResult<Option<Vec<(String, String)>>> {
95 match opts.get(&kw("env")) {
96 None | Some(Value::Nil) => Ok(None),
97 Some(Value::Map(m)) => m
98 .iter()
99 .map(|(k, v)| match (env_name(k), v) {
100 (Some(name), Value::Str(s)) => {
103 let v = s.get().clone();
104 if v.contains('\0') {
105 Err(arg_error("an :env value must not contain a NUL character"))
106 } else {
107 Ok((name, v))
108 }
109 }
110 _ => Err(arg_error(
111 "every :env key must be a valid variable name (string/keyword) and every value a string",
112 )),
113 })
114 .collect::<ValueResult<Vec<_>>>()
115 .map(Some),
116 Some(_) => Err(arg_error("the :env option must be a map of name to string value")),
117 }
118}
119
120pub fn collect(args: &[Value]) -> ValueResult<Request> {
122 if args.len() > 2 {
123 return Err(arg_error("expects (run argv) or (run argv opts)"));
124 }
125 let argv = collect_argv(&args[0])?;
126 let opts = match args.get(1) {
127 None | Some(Value::Nil) => {
128 return Ok(Request {
129 argv,
130 stdin: None,
131 dir: None,
132 env: None,
133 out_bytes: false,
134 });
135 }
136 Some(Value::Map(m)) => m,
137 Some(_) => return Err(arg_error("the options argument must be a map")),
138 };
139 Ok(Request {
140 argv,
141 stdin: opt_string(opts, "in")?,
142 dir: opt_string(opts, "dir")?,
143 env: collect_env(opts)?,
144 out_bytes: matches!(opts.get(&kw("out-bytes")), Some(Value::Bool(true))),
145 })
146}
147
148pub fn command(req: &Request) -> Command {
152 let mut cmd = Command::new(&req.argv[0]);
153 cmd.args(&req.argv[1..])
154 .stdin(if req.stdin.is_some() {
155 Stdio::piped()
156 } else {
157 Stdio::null()
158 })
159 .stdout(Stdio::piped())
160 .stderr(Stdio::piped());
161 if let Some(dir) = &req.dir {
162 cmd.current_dir(dir);
163 }
164 if let Some(env) = &req.env {
165 cmd.env_clear()
166 .envs(env.iter().map(|(k, v)| (k.as_str(), v.as_str())));
167 }
168 cmd
169}
170
171#[cfg(unix)]
174pub fn exit_code(status: std::process::ExitStatus) -> i64 {
175 use std::os::unix::process::ExitStatusExt;
176 match (status.code(), status.signal()) {
177 (Some(code), _) => code as i64,
178 (None, Some(sig)) => 128 + sig as i64,
179 (None, None) => -1,
180 }
181}
182
183#[cfg(not(unix))]
184pub fn exit_code(status: std::process::ExitStatus) -> i64 {
185 status.code().map(|c| c as i64).unwrap_or(-1)
186}
187
188pub fn result_value(output: &Output, out_bytes: bool) -> Value {
190 let out = if out_bytes {
191 let bytes: Vec<i8> = output.stdout.iter().map(|b| *b as i8).collect();
192 Value::ByteArray(GcPtr::new(Mutex::new(bytes)))
193 } else {
194 Value::string(String::from_utf8_lossy(&output.stdout).into_owned())
195 };
196 Value::Map(MapValue::from_pairs(vec![
197 (kw("exit"), Value::Long(exit_code(output.status))),
198 (kw("out"), out),
199 (
200 kw("err"),
201 Value::string(String::from_utf8_lossy(&output.stderr).into_owned()),
202 ),
203 ]))
204}
205
206pub fn execute(mut cmd: Command, program: &str, stdin: Option<String>) -> ValueResult<Output> {
215 let mut child = cmd.spawn().map_err(|e| {
216 ValueError::Other(format!(
217 "clojure.rust.process/run: cannot run program '{program}' ({e})"
218 ))
219 })?;
220 let feeder = match (stdin, child.stdin.take()) {
221 (Some(bytes), Some(mut pipe)) => Some(std::thread::spawn(move || {
222 let _ = pipe.write_all(bytes.as_bytes());
223 })),
224 _ => None,
225 };
226 let output = child.wait_with_output().map_err(|e| {
227 ValueError::Other(format!(
228 "clojure.rust.process/run: waiting for '{program}' failed ({e})"
229 ))
230 })?;
231 if let Some(handle) = feeder {
232 let _ = handle.join();
233 }
234 Ok(output)
235}
236
237fn builtin_run(args: &[Value]) -> ValueResult<Value> {
240 let req = collect(args)?;
241 let output = execute(command(&req), &req.argv[0], req.stdin.clone())?;
242 Ok(result_value(&output, req.out_bytes))
243}