diff --git a/.gitignore b/.gitignore index f4f7dfed..3e801a5d 100644 --- a/.gitignore +++ b/.gitignore @@ -20,3 +20,5 @@ pom.xml.asc /tmp /reports *.dylib +*.log +org_babashka*.h diff --git a/doc/pods.md b/doc/pods.md new file mode 100644 index 00000000..67308f1d --- /dev/null +++ b/doc/pods.md @@ -0,0 +1,3 @@ +# Pods + +Pods are commmand line programs that babashka can communicate with via stdin and stdout. diff --git a/examples/pods/pod-babashka-filewatcher/.gitignore b/examples/pods/pod-babashka-filewatcher/.gitignore new file mode 100644 index 00000000..ea8c4bf7 --- /dev/null +++ b/examples/pods/pod-babashka-filewatcher/.gitignore @@ -0,0 +1 @@ +/target diff --git a/examples/pods/pod-babashka-filewatcher/Cargo.lock b/examples/pods/pod-babashka-filewatcher/Cargo.lock new file mode 100644 index 00000000..4798333a --- /dev/null +++ b/examples/pods/pod-babashka-filewatcher/Cargo.lock @@ -0,0 +1,292 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +[[package]] +name = "bitflags" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf1de2fe8c75bc145a2f577add951f8134889b4795d47466a54a5c846d691693" + +[[package]] +name = "cfg-if" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4785bdd1c96b2a846b2bd7cc02e86b6b3dbf14e7e53446c4f54c92a361040822" + +[[package]] +name = "filetime" +version = "0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f59efc38004c988e4201d11d263b8171f49a2e7ec0bdbb71773433f271504a5e" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "winapi 0.3.8", +] + +[[package]] +name = "fsevent" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ab7d1bd1bd33cc98b0889831b72da23c0aa4df9cec7e0702f46ecea04b35db6" +dependencies = [ + "bitflags", + "fsevent-sys", +] + +[[package]] +name = "fsevent-sys" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f41b048a94555da0f42f1d632e2e19510084fb8e303b0daa2816e733fb3644a0" +dependencies = [ + "libc", +] + +[[package]] +name = "fuchsia-zircon" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e9763c69ebaae630ba35f74888db465e49e259ba1bc0eda7d06f4a067615d82" +dependencies = [ + "bitflags", + "fuchsia-zircon-sys", +] + +[[package]] +name = "fuchsia-zircon-sys" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3dcaa9ae7725d12cdb85b3ad99a434db70b468c09ded17e012d86b5c1010f7a7" + +[[package]] +name = "inotify" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24e40d6fd5d64e2082e0c796495c8ef5ad667a96d03e5aaa0becfd9d47bcbfb8" +dependencies = [ + "bitflags", + "inotify-sys", + "libc", +] + +[[package]] +name = "inotify-sys" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e74a1aa87c59aeff6ef2cc2fa62d41bc43f54952f55652656b18a02fd5e356c0" +dependencies = [ + "libc", +] + +[[package]] +name = "iovec" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2b3ea6ff95e175473f8ffe6a7eb7c00d054240321b84c57051175fe3c1e075e" +dependencies = [ + "libc", +] + +[[package]] +name = "json" +version = "0.12.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "078e285eafdfb6c4b434e0d31e8cfcb5115b651496faca5749b88fafd4f23bfd" + +[[package]] +name = "kernel32-sys" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7507624b29483431c0ba2d82aece8ca6cdba9382bff4ddd0f7490560c056098d" +dependencies = [ + "winapi 0.2.8", + "winapi-build", +] + +[[package]] +name = "lazycell" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b294d6fa9ee409a054354afc4352b0b9ef7ca222c69b8812cbea9e7d2bf3783f" + +[[package]] +name = "libc" +version = "0.2.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "99e85c08494b21a9054e7fe1374a732aeadaff3980b6990b94bfd3a70f690005" + +[[package]] +name = "log" +version = "0.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "14b6052be84e6b71ab17edffc2eeabf5c2c3ae1fdb464aae35ac50c67a44e1f7" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "mio" +version = "0.6.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fce347092656428bc8eaf6201042cb551b8d67855af7374542a92a0fbfcac430" +dependencies = [ + "cfg-if", + "fuchsia-zircon", + "fuchsia-zircon-sys", + "iovec", + "kernel32-sys", + "libc", + "log", + "miow", + "net2", + "slab", + "winapi 0.2.8", +] + +[[package]] +name = "mio-extras" +version = "2.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52403fe290012ce777c4626790c8951324a2b9e3316b3143779c72b029742f19" +dependencies = [ + "lazycell", + "log", + "mio", + "slab", +] + +[[package]] +name = "miow" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c1f2f3b1cf331de6896aabf6e9d55dca90356cc9960cca7eaaf408a355ae919" +dependencies = [ + "kernel32-sys", + "net2", + "winapi 0.2.8", + "ws2_32-sys", +] + +[[package]] +name = "net2" +version = "0.2.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2ba7c918ac76704fb42afcbbb43891e72731f3dcca3bef2a19786297baf14af7" +dependencies = [ + "cfg-if", + "libc", + "winapi 0.3.8", +] + +[[package]] +name = "notify" +version = "4.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "80ae4a7688d1fab81c5bf19c64fc8db920be8d519ce6336ed4e7efe024724dbd" +dependencies = [ + "bitflags", + "filetime", + "fsevent", + "fsevent-sys", + "inotify", + "libc", + "mio", + "mio-extras", + "walkdir", + "winapi 0.3.8", +] + +[[package]] +name = "pod-babashka-filewatcher" +version = "0.1.0" +dependencies = [ + "json", + "log", + "notify", +] + +[[package]] +name = "redox_syscall" +version = "0.1.56" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2439c63f3f6139d1b57529d16bc3b8bb855230c8efcc5d3a896c8bea7c3b1e84" + +[[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "slab" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c111b5bd5695e56cffe5129854aa230b39c93a305372fdbb2668ca2394eea9f8" + +[[package]] +name = "walkdir" +version = "2.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "777182bc735b6424e1a57516d35ed72cb8019d85c8c9bf536dccb3445c1a2f7d" +dependencies = [ + "same-file", + "winapi 0.3.8", + "winapi-util", +] + +[[package]] +name = "winapi" +version = "0.2.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "167dc9d6949a9b857f3451275e911c3f44255842c1f7a76f33c55103a909087a" + +[[package]] +name = "winapi" +version = "0.3.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8093091eeb260906a183e6ae1abdba2ef5ef2257a21801128899c3fc699229c6" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-build" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d315eee3b34aca4797b2da6b13ed88266e6d612562a0c46390af8299fc699bc" + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-util" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70ec6ce85bb158151cae5e5c87f95a8e97d2c0c4b001223f33a334e3ce5de178" +dependencies = [ + "winapi 0.3.8", +] + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "ws2_32-sys" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d59cefebd0c892fa2dd6de581e937301d8552cb44489cdff035c6187cb63fa5e" +dependencies = [ + "winapi 0.2.8", + "winapi-build", +] diff --git a/examples/pods/pod-babashka-filewatcher/Cargo.toml b/examples/pods/pod-babashka-filewatcher/Cargo.toml new file mode 100644 index 00000000..ee787dac --- /dev/null +++ b/examples/pods/pod-babashka-filewatcher/Cargo.toml @@ -0,0 +1,12 @@ +[package] +name = "pod-babashka-filewatcher" +version = "0.1.0" +authors = ["Michiel Borkent "] +edition = "2018" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +notify = "4.0.12" +json = "0.12.4" +log = "0.4" diff --git a/examples/pods/pod-babashka-filewatcher/README.md b/examples/pods/pod-babashka-filewatcher/README.md new file mode 100644 index 00000000..39eb2f18 --- /dev/null +++ b/examples/pods/pod-babashka-filewatcher/README.md @@ -0,0 +1,18 @@ +# pod-babashka-filewatcher + +## Compile + +``` +$ cargo build --release +``` + +## Run + +``` +(babashka.pods/load-pod "examples/pods/pod-babashka-filewatcher/target/release/pod-babashka-filewatcher") +(def chan (pod.babashka.filewatcher/watch "/tmp")) +(require '[clojure.core.async :as async]) +(loop [] (prn (async/ ["changed" "/tmp"] +;;=> ["changed" "/tmp"] +``` diff --git a/examples/pods/pod-babashka-filewatcher/src/bencode.rs b/examples/pods/pod-babashka-filewatcher/src/bencode.rs new file mode 100644 index 00000000..aea08fef --- /dev/null +++ b/examples/pods/pod-babashka-filewatcher/src/bencode.rs @@ -0,0 +1,320 @@ +// from https://github.com/jasilven/redbush/blob/master/src/nrepl/bencode.rs + +use std::collections::HashMap; +use std::convert::TryInto; +use std::fmt::{self, Display}; +use std::hash::Hash; +use std::hash::Hasher; +use std::io::BufRead; +use std::iter::Iterator; +use std::str::FromStr; +use std::string::ToString; + +type Result = std::result::Result; + +#[derive(Debug)] +pub enum BencodeError { + Error(String), + Io(std::io::Error), + Eof(), + Parse(std::num::ParseIntError), +} + +impl Display for BencodeError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + BencodeError::Error(s) => write!(f, "Bencode Error: {} ", s), + BencodeError::Io(e) => write!(f, "Bencode Io: {}", e), + BencodeError::Parse(e) => write!(f, "Bencode Parse: {}", e), + BencodeError::Eof() => write!(f, "Bencode Eof"), + } + } +} + +impl From for BencodeError { + fn from(err: std::io::Error) -> BencodeError { + BencodeError::Io(err) + } +} + +impl From for BencodeError { + fn from(err: std::num::ParseIntError) -> BencodeError { + BencodeError::Parse(err) + } +} + +#[derive(Clone, Debug, Eq)] +pub struct HMap(pub HashMap); + +#[derive(Clone, Debug, PartialEq, Eq, Hash)] +pub enum Value { + Map(HMap), + List(Vec), + Str(String), + Int(i32), +} + +impl From<&str> for Value { + fn from(s: &str) -> Self { + Value::Str(s.to_string()) + } +} + +impl From> for Value { + fn from(m: HashMap) -> Self { + Value::Map(HMap::new(m)) + } +} + +impl From> for Value { + fn from(map: HashMap<&str, &str>) -> Self { + let mut m = HashMap::new(); + for (k, v) in map { + m.insert(Value::Str(k.to_string()), Value::Str(v.to_string())); + } + let hm = HMap::new(m); + Value::Map(hm) + } +} + +impl TryInto> for Value { + type Error = BencodeError; + + fn try_into(self) -> std::result::Result, Self::Error> { + match self { + Value::Map(hm) => { + let mut map = HashMap::::new(); + for key in hm.0.keys() { + // safe to unwrap here + map.insert(format!("{}", &key), format!("{}", &hm.get(key).unwrap())); + } + Ok(map) + } + _ => Err(BencodeError::Error("Expected HashMap Value".into())), + } + } +} + +impl HMap { + pub fn new(map: HashMap) -> Self { + HMap(map) + } + + pub fn get(&self, key: &Value) -> Option<&Value> { + self.0.get(key) + } +} + +impl Hash for HMap { + fn hash(&self, state: &mut H) { + let mut keys: Vec = self.0.keys().map(|k| format!("{:?}", k)).collect(); + let mut vals: Vec = self.0.values().map(|v| format!("{:?}", v)).collect(); + keys.sort(); + vals.sort(); + keys.hash(state); + vals.hash(state); + } +} + +impl PartialEq for HMap { + fn eq(&self, other: &HMap) -> bool { + self.0.eq(&other.0) + } +} + +impl Display for Value { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Value::Map(hm) => { + let mut result = String::from("{"); + for (key, val) in hm.0.iter() { + result.push_str(&format!("{} {} ", &key, &val)); + } + let mut result = result.trim_end().to_string(); + result.push('}'); + write!(f, "{}", result) + } + Value::List(v) => { + let mut result = String::from("["); + for item in v { + result.push_str(&item.to_string()); + result.push_str(", "); + } + let mut result = result + .trim_end_matches(|c| c == ',' || c == ' ') + .to_string(); + result.push(']'); + write!(f, "{}", result) + } + Value::Str(s) => write!(f, "{}", s), + Value::Int(i) => write!(f, "{}", i), + } + } +} + +impl Value { + pub fn to_bencode(&self) -> String { + match self { + Value::Map(hm) => { + let mut result = String::from("d"); + for (key, val) in hm.0.iter() { + result.push_str(&format!("{}{}", key.to_bencode(), val.to_bencode())); + } + result.push('e'); + result + } + Value::List(v) => { + let mut result = String::from("l"); + for item in v { + result.push_str(&item.to_bencode()); + } + result.push('e'); + result + } + Value::Str(s) => format!("{}:{}", s.len(), s), + Value::Int(i) => format!("i{}e", i), + } + } +} + +pub fn parse_bencode(reader: &mut dyn BufRead) -> Result> { + log::debug!("Parsing bencode from reader"); + + let mut buf = vec![]; + buf.resize(1, 0); + match reader.read_exact(&mut buf[0..1]) { + Ok(()) => match buf[0] { + b'i' => match reader.read_until(b'e', &mut buf) { + Ok(cnt) => { + let s = String::from_utf8_lossy(&buf[1..cnt]); + let n = i32::from_str(&s)?; + Ok(Some(Value::Int(n))) + } + Err(e) => Err(e.into()), + }, + b'd' => { + let mut map = HashMap::new(); + loop { + match parse_bencode(reader) { + Ok(None) => return Ok(Some(Value::Map(HMap(map)))), + Ok(Some(v)) => map.insert(v, parse_bencode(reader)?.unwrap()), + Err(e) => return Err(e), + }; + } + } + b'l' => { + let mut list = Vec::::new(); + loop { + match parse_bencode(reader) { + Ok(None) => return Ok(Some(Value::List(list))), + Ok(Some(v)) => list.push(v), + Err(e) => return Err(e), + } + } + } + b'e' => Ok(None), + b'0' => { + reader.read_until(b':', &mut buf)?; + Ok(Some(Value::Str("".to_string()))) + } + _ => match reader.read_until(b':', &mut buf) { + Ok(_) => { + buf.resize(buf.len() - 1, 0); + let mut s = String::from(""); + buf.iter().for_each(|i| s.push(*i as char)); + let cnt = usize::from_str(&s)?; + buf.resize(cnt, 0); + reader.read_exact(&mut buf[0..cnt])?; + Ok(Some(Value::Str( + String::from_utf8_lossy(&buf[..]).to_string(), + ))) + } + Err(e) => Err(BencodeError::Io(e)), + }, + }, + Err(e) => match e.kind() { + std::io::ErrorKind::UnexpectedEof => (Err(BencodeError::Eof())), + _ => Err(BencodeError::Io(e)), + }, + } +} + +#[cfg(test)] +mod tests { + use super::*; + use std::io::BufReader; + + #[test] + fn test_parse_bencode_num() { + let left = vec![ + Value::Int(1), + Value::Int(10), + Value::Int(100_000), + Value::Int(-1), + Value::Int(-999), + ]; + let right = vec!["i1e", "i10e", "i100000e", "i-1e", "i-999e"]; + + for i in 0..left.len() { + let mut bufread = BufReader::new(right[i].as_bytes()); + assert_eq!(left[i], parse_bencode(&mut bufread).unwrap().unwrap()); + assert_eq!(left[i].to_bencode(), right[i]); + } + } + + #[test] + fn test_parse_bencode_str() { + let left = vec![ + Value::Str("foo".to_string()), + Value::Str("1234567890\n".to_string()), + Value::Str("".to_string()), + ]; + let right = vec!["3:foo", "11:1234567890\n", "0:"]; + for i in 0..left.len() { + let mut bufread = BufReader::new(right[i].as_bytes()); + assert_eq!(left[i], parse_bencode(&mut bufread).unwrap().unwrap()); + assert_eq!(left[i].to_bencode(), right[i]); + } + } + + #[test] + fn test_parse_bencode_list() { + let left = vec![ + (Value::List(vec![Value::Int(1), Value::Int(2), Value::Int(3)])), + (Value::List(vec![ + Value::Int(1), + Value::Str("foo".to_string()), + Value::Int(3), + ])), + (Value::List(vec![Value::Str("".to_string())])), + ]; + let right = vec!["li1ei2ei3ee", "li1e3:fooi3ee", "l0:e"]; + for i in 0..left.len() { + let mut bufread = BufReader::new(right[i].as_bytes()); + assert_eq!(left[i], parse_bencode(&mut bufread).unwrap().unwrap()); + assert_eq!(left[i].to_bencode(), right[i]); + } + } + + #[test] + fn test_parse_bencode_map() { + let mut m1 = HashMap::new(); + m1.insert(Value::Str("bar".to_string()), Value::Str("baz".to_string())); + let m1_c = m1.clone(); + let left1 = Value::Map(HMap::new(m1)); + + let mut m2 = HashMap::new(); + m2.insert(Value::Str("foo".to_string()), Value::Map(HMap::new(m1_c))); + let left2 = Value::Map(HMap::new(m2)); + + let sright1 = "d3:bar3:baze".to_string(); + let mut right1 = BufReader::new(sright1.as_bytes()); + assert_eq!(left1, parse_bencode(&mut right1).unwrap().unwrap()); + assert_eq!(left1.to_bencode(), sright1); + + let sright2 = "d3:food3:bar3:bazee".to_string(); + let mut right2 = BufReader::new(sright2.as_bytes()); + assert_eq!(left2, parse_bencode(&mut right2).unwrap().unwrap()); + assert_eq!(left2.to_bencode(), sright2); + } +} diff --git a/examples/pods/pod-babashka-filewatcher/src/main.rs b/examples/pods/pod-babashka-filewatcher/src/main.rs new file mode 100644 index 00000000..74b0bd70 --- /dev/null +++ b/examples/pods/pod-babashka-filewatcher/src/main.rs @@ -0,0 +1,137 @@ +// see https://github.com/jasilven/redbush/blob/master/src/nrepl/mod.rs + +mod bencode; +use bencode::{Value}; +use bencode as bc; + +use notify::{Watcher, RecursiveMode, watcher}; +use std::sync::mpsc::channel; +use std::time::Duration; + +use std::collections::HashMap; +use std::io; +use std::io::{Write, BufReader}; + +use json; + +fn get_string(val: &bc::Value, key: &str) -> Option { + match val { + bc::Value::Map(hm) => { + match hm.get(&Value::from(key)) { + Some(Value::Str(s)) => + Some(String::from(s)), + _ => None + } + }, + _ => None + } +} + +fn insert(mut m: HashMap, k: &str, v: &str) -> HashMap { + m.insert(Value::from(k), Value::from(v)); + m +} + +fn describe() { + let namespace = HashMap::new(); + let mut namespace = insert(namespace, "name", "pod.babashka.filewatcher"); + let mut vars = Vec::new(); + let var_map = HashMap::new(); + let var_map = insert(var_map, "name", "watch"); + let var_map = insert(var_map, "async", "true"); + vars.push(Value::from(var_map)); + namespace.insert(Value::from("vars"),Value::List(vars)); + let describe_map = HashMap::new(); + let mut describe_map = insert(describe_map, "format", "json"); + let namespaces = vec![Value::from(namespace)]; + let namespaces = Value::List(namespaces); + describe_map.insert(Value::from("namespaces"), namespaces); + let describe_map = Value::from(describe_map); + let bencode = describe_map.to_bencode(); + let stdout = io::stdout(); + let mut handle = stdout.lock(); + handle.write_all(bencode.as_bytes()).unwrap(); + handle.flush().unwrap(); +} + +fn path_changed(id: &str, path: &str) { + let reply = HashMap::new(); + let reply = insert(reply, "id", id); + let value = vec!["changed", path]; + let value = json::stringify(value); + let mut reply = insert(reply, "value", &value); + let status = vec![Value::from("status")]; + reply.insert(Value::from("status"),Value::List(status)); + let bencode = Value::from(reply).to_bencode(); + let stdout = io::stdout(); + let mut handle = stdout.lock(); + handle.write_all(bencode.as_bytes()).unwrap(); + handle.flush().unwrap(); +} + +fn watch(id: &str, path: &str) { + let (tx, rx) = channel(); + let mut watcher = watcher(tx, Duration::from_secs(2)).unwrap(); + watcher.watch(path, RecursiveMode::Recursive).unwrap(); + loop { + match rx.recv() { + Ok(_) => { + path_changed(id, path); + }, + Err(e) => panic!("watch error: {:?}", e), + } + } + +} + +fn handle_incoming(val: bc::Value) { + let op = get_string(&val, "op").unwrap(); + match &op[..] { + "describe" => { + describe() + + }, + "invoke" => { + let var = get_string(&val, "var").unwrap(); + match &var[..] { + "pod.babashka.filewatcher/watch" => { + let args = get_string(&val, "args").unwrap(); + let args = json::parse(&args).unwrap(); + let path = &args[0]; + let path = path.as_str().unwrap(); + let id = get_string(&val, "id").unwrap(); + watch(&id, path); + }, + _ => panic!(var) + }; + }, + _ => panic!(op) + } +} + + +fn main() { + + loop { + let mut reader = BufReader::new(io::stdin()); + let val = bc::parse_bencode(&mut reader); + match val { + Ok(res) => { + match res { + Some(val) => { + handle_incoming(val) + } + None => { + + } + } + + } + Err(bc::BencodeError::Eof()) => { + return + }, + Err(v) => panic!("{}", v) + } + + } +} diff --git a/examples/pods/pod-babashka-hsqldb/.gitignore b/examples/pods/pod-babashka-hsqldb/.gitignore new file mode 100644 index 00000000..faad8a87 --- /dev/null +++ b/examples/pods/pod-babashka-hsqldb/.gitignore @@ -0,0 +1,20 @@ +/target +/classes +/checkouts +profiles.clj +pom.xml +pom.xml.asc +*.jar +*.class +/.lein-* +/.nrepl-port +.hgignore +.hg/ +/bb +.clj-kondo/.cache +!java/src/babashka/impl/LockFix.class +!test-resources/babashka/src_for_classpath_test/foo.jar +!test-resources/pom.xml +.cpcache +!reflection.json +hsqldb-babashka-plugin diff --git a/examples/pods/pod-babashka-hsqldb/README.md b/examples/pods/pod-babashka-hsqldb/README.md new file mode 100644 index 00000000..86c1f3a2 --- /dev/null +++ b/examples/pods/pod-babashka-hsqldb/README.md @@ -0,0 +1,12 @@ +# pod-babashka-hsqldb + +## Compile + +Run `./compile` + +## Run + +``` clojure +(babashka.pods/load-pod "examples/pods/pod-babashka-hsqldb/pod-babashka-hsqldb") +(pod.hsqldb/execute! "jdbc:hsqldb:mem:testdb;sql.syntax_mys=true" ["create table foo ( foo int );"])' +``` diff --git a/examples/pods/pod-babashka-hsqldb/compile b/examples/pods/pod-babashka-hsqldb/compile new file mode 100755 index 00000000..fce66613 --- /dev/null +++ b/examples/pods/pod-babashka-hsqldb/compile @@ -0,0 +1,33 @@ +#!/usr/bin/env bash + +set -xeo pipefail + +if [ -z "$GRAALVM_HOME" ]; then + echo "Please set GRAALVM_HOME" + exit 1 +fi + +"$GRAALVM_HOME/bin/gu" install native-image || true +export PATH=$GRAALVM_HOME/bin:$PATH + +JAR=target/pod-babashka-hsqldb-0.0.1-SNAPSHOT-standalone.jar + +lein do clean, uberjar + +$GRAALVM_HOME/bin/native-image \ + -jar $JAR \ + -H:Name=pod-babashka-hsqldb \ + -H:+ReportExceptionStackTraces \ + -J-Dclojure.spec.skip-macros=true \ + -J-Dclojure.compiler.direct-linking=true \ + "-H:IncludeResources=SCI_VERSION" \ + -H:ReflectionConfigurationFiles=reflection.json \ + --initialize-at-run-time=java.lang.Math\$RandomNumberGeneratorHolder \ + --initialize-at-build-time \ + -H:Log=registerResource: \ + --verbose \ + --no-fallback \ + --no-server \ + --report-unsupported-elements-at-runtime \ + "-H:IncludeResources=org/hsqldb/.*\.properties" "-H:IncludeResources=org/hsqldb/.*\.sql" \ + "-J-Xmx4500m" diff --git a/examples/pods/pod-babashka-hsqldb/pod-babashka-hsqldb b/examples/pods/pod-babashka-hsqldb/pod-babashka-hsqldb new file mode 100755 index 00000000..1a51018f Binary files /dev/null and b/examples/pods/pod-babashka-hsqldb/pod-babashka-hsqldb differ diff --git a/examples/pods/pod-babashka-hsqldb/project.clj b/examples/pods/pod-babashka-hsqldb/project.clj new file mode 100644 index 00000000..adf72ecf --- /dev/null +++ b/examples/pods/pod-babashka-hsqldb/project.clj @@ -0,0 +1,22 @@ +(defproject org.babashka/pod-babashka-hsqldb "0.0.1-SNAPSHOT" + :description "babashka" + :url "https://github.com/borkdude/babashka" + :scm {:name "git" + :url "https://github.com/borkdude/babashka"} + :license {:name "Eclipse Public License 1.0" + :url "http://opensource.org/licenses/eclipse-1.0.php"} + :source-paths ["src"] + :resource-paths ["resources"] + :dependencies [[org.clojure/clojure "1.10.2-alpha1"] + [org.hsqldb/hsqldb "2.4.0"] + [seancorfield/next.jdbc "1.0.424"] + [nrepl/bencode "1.1.0"]] + :profiles {:uberjar {:global-vars {*assert* false} + :jvm-opts ["-Dclojure.compiler.direct-linking=true" + "-Dclojure.spec.skip-macros=true"] + :aot :all + :main pod.babashka.hsqldb}} + :deploy-repositories [["clojars" {:url "https://clojars.org/repo" + :username :env/clojars_user + :password :env/clojars_pass + :sign-releases false}]]) diff --git a/examples/pods/pod-babashka-hsqldb/reflection.json b/examples/pods/pod-babashka-hsqldb/reflection.json new file mode 100644 index 00000000..69c0247f --- /dev/null +++ b/examples/pods/pod-babashka-hsqldb/reflection.json @@ -0,0 +1,15 @@ +[ { + "name" : "org.hsqldb.jdbcDriver" +}, + { + "methods" : [{"name":"", "parameterTypes":[ "org.hsqldb.Database"]} ], + "name" : "org.hsqldb.dbinfo.DatabaseInformationFull" + }, + { + "methods" : [ { + "name" : "getBundle", + "parameterTypes" : [ "java.lang.String", "java.util.Locale", "java.lang.ClassLoader" ] + } ], + "name" : "java.util.ResourceBundle" + } +] diff --git a/examples/pods/pod-babashka-hsqldb/src/pod/babashka/hsqldb.clj b/examples/pods/pod-babashka-hsqldb/src/pod/babashka/hsqldb.clj new file mode 100644 index 00000000..6981c2c3 --- /dev/null +++ b/examples/pods/pod-babashka-hsqldb/src/pod/babashka/hsqldb.clj @@ -0,0 +1,49 @@ +(ns pod.babashka.hsqldb + (:refer-clojure :exclude [read read-string]) + (:require [bencode.core :as bencode] + [clojure.edn :as edn] + [next.jdbc :as jdbc]) + (:import [java.io PushbackInputStream]) + (:gen-class)) + +(def stdin (PushbackInputStream. System/in)) + +(def lookup + {'pod.hsqldb/execute! jdbc/execute!}) + +(defn write [v] + (bencode/write-bencode System/out v) + (.flush System/out)) + +(defn read-string [^"[B" v] + (String. v)) + +(defn read [] + (bencode/read-bencode stdin)) + +(defn -main [& _args] + (loop [] + (let [message (try (read) + (catch java.io.EOFException _ + ::EOF))] + (when-not (identical? ::EOF message) + (let [op (get message "op") + op (read-string op) + op (keyword op)] + (case op + :describe (do (write {"format" "edn" + "namespaces" [{"name" "pod.hsqldb" + "vars" [{"name" "execute!"}]}]}) + (recur)) + :invoke (let [var (-> (get message "var") + read-string + symbol) + id (-> (get message "id") + read-string) + args (get message "args") + args (read-string args) + args (edn/read-string args)] + (write {"value" (pr-str (apply (lookup var) args)) + "id" id + "status" ["done"]}) + (recur)))))))) diff --git a/examples/pods/pod-babashka-hsqldb/test.clj b/examples/pods/pod-babashka-hsqldb/test.clj new file mode 100644 index 00000000..5f66ffee --- /dev/null +++ b/examples/pods/pod-babashka-hsqldb/test.clj @@ -0,0 +1,45 @@ +(ns test + (:refer-clojure :exclude [read]) + (:require [bencode.core :as bencode] + [clojure.edn :as edn]) + #_(:import java.lang.ProcessBuilder$Redirect)) + +(defn write [stream v] + (bencode/write-bencode stream v) + (.flush stream)) + +(defn read [stream] + (bencode/read-bencode stream)) + +(defn bytes->string [^"[B" bytes] + (String. bytes)) + +(defn query [stream q] + (write stream {"op" "invoke" + "var" "hsqldb.jdbc/execute!" + "args" (pr-str ["jdbc:hsqldb:mem:testdb;sql.syntax_mys=true" q])})) + +(let [pb (ProcessBuilder. #_["lein" "run" "-m" "org.babashka.hsqldb"] + ["./hsqldb-babashka-plugin"]) + _ (.redirectErrorStream pb true) + ;; _ (.redirectOutput pb ProcessBuilder$Redirect/INHERIT) + p (.start pb) + stdin (.getOutputStream p) + stdout (.getInputStream p) + stdout (java.io.PushbackInputStream. stdout)] + (write stdin {"op" "describe"}) + (let [reply (read stdout)] + (println "format:" (String. (get reply "format"))) + (println "vars:" (mapv bytes->string (get reply "vars")))) ;;=> edn + + (query stdin ["create table foo ( foo int );"]) + (let [reply (read stdout)] + (println "reply:" (edn/read-string (String. (get reply "value"))))) ;;=> [{:next.jdbc/update-count 0}] + + (query stdin ["insert into foo values ( 1, 2, 3);"]) + (let [reply (read stdout)] + (println "reply:" (edn/read-string (String. (get reply "value"))))) ;;=> [{:next.jdbc/update-count 3}] + + (query stdin ["select * from foo;"]) + (let [reply (read stdout)] + (println "reply:" (edn/read-string (String. (get reply "value")))))) ;=> [{:FOO/FOO 1} {:FOO/FOO 2} {:FOO/FOO 3}] diff --git a/examples/pods/pod-lispyclouds-sqlite/README.md b/examples/pods/pod-lispyclouds-sqlite/README.md new file mode 100644 index 00000000..6a798ce2 --- /dev/null +++ b/examples/pods/pod-lispyclouds-sqlite/README.md @@ -0,0 +1,20 @@ +# pod-lispyclouds-sqlite + +To run this: + +- Install python3 and sqlite3 +- Create a virtualenv: `python3 -m venv ~/.virtualenvs/babashka` +- Switch to it: `source ~/.virtualenvs/babashka/bin/activate` +- Run: `pip install bcoding` to install the bencode lib +- Create a new db: `sqlite3 /tmp/babashka.db "create table foo (foo int);"` + +Then run as pod: + +``` clojure +(babashka.pods/load-pod ["examples/pods/pod-lispyclouds-sqlite/pod-lispyclouds-sqlite.py"]) +(require '[pod.lispy-clouds.sqlite :as sqlite]) +(sqlite/execute! "create table if not exists foo ( int foo )") +(sqlite/execute! "delete from foo") +(sqlite/execute! "insert into foo values (1), (2)") +(sqlite/execute! "select * from foo") ;;=> ([1] [2]) +``` diff --git a/examples/pods/pod-lispyclouds-sqlite/pod-lispyclouds-sqlite.py b/examples/pods/pod-lispyclouds-sqlite/pod-lispyclouds-sqlite.py new file mode 100755 index 00000000..19359793 --- /dev/null +++ b/examples/pods/pod-lispyclouds-sqlite/pod-lispyclouds-sqlite.py @@ -0,0 +1,62 @@ +#!/usr/bin/env python3 + +import json +import sqlite3 +import sys + +from bcoding import bencode, bdecode + + +def read(): + return dict(bdecode(sys.stdin.buffer)) + + +def write(obj): + sys.stdout.buffer.write(bencode(obj)) + sys.stdout.flush() + +def debug(*msg): + with open("/tmp/debug.log", "a") as f: + f.write(str(msg) + "\n") + +def main(): + while True: + msg = read() + debug("msg", msg) + + op = msg["op"] + + if op == "describe": + write( + { + "format": "json", + "namespaces": [{"name": "pod.lispy-clouds.sqlite", + "vars": [{"name": "execute!"}]}]} + ) + elif op == "invoke": + var = msg["var"] + id = msg["id"] + args = json.loads(msg["args"]) + debug(args) + conn = sqlite3.connect("/tmp/babashka.db") + c = conn.cursor() + + result = None + + if var == "pod.lispy-clouds.sqlite/execute!": + try: + result = c.execute(*args) + except Exception as e: + debug(e) + + value = json.dumps(result.fetchall()) + debug("value", value) + + write({"value": value, "id": id, "status": ["done"]}) + + conn.commit() + conn.close() + + +if __name__ == "__main__": + main() diff --git a/project.clj b/project.clj index 60558c17..cf1e6a36 100644 --- a/project.clj +++ b/project.clj @@ -10,6 +10,7 @@ :source-paths ["src" "sci/src" "babashka.curl/src"] ;; for debugging Reflector.java code: ;; :java-source-paths ["sci/reflector/src-java"] + :java-source-paths ["src-java"] :resource-paths ["resources" "sci/resources"] :dependencies [[org.clojure/clojure "1.10.2-alpha1"] [org.clojure/tools.reader "1.3.2"] diff --git a/sci b/sci index 438ec157..806cf61e 160000 --- a/sci +++ b/sci @@ -1 +1 @@ -Subproject commit 438ec15798f319f232d789b74b04ac25f15d540b +Subproject commit 806cf61eeff31869727e205a8f1a7021a0ea5b49 diff --git a/src/babashka/impl/cheshire.clj b/src/babashka/impl/cheshire.clj index 32f518e8..3ee35d34 100644 --- a/src/babashka/impl/cheshire.clj +++ b/src/babashka/impl/cheshire.clj @@ -15,8 +15,10 @@ 'generate-smile (copy-var json/generate-smile tns) 'decode (copy-var json/decode tns) 'parse-string (copy-var json/parse-string tns) + 'parse-string-strict (copy-var json/parse-string-strict tns) 'parse-smile (copy-var json/parse-smile tns) 'parse-stream (copy-var json/parse-stream tns) + 'parse-stream-strict (copy-var json/parse-stream-strict tns) 'parsed-seq (copy-var json/parsed-seq tns) 'parsed-smile-seq (copy-var json/parsed-smile-seq tns) 'decode-smile (copy-var json/decode-smile tns)}) diff --git a/src/babashka/impl/pods.clj b/src/babashka/impl/pods.clj new file mode 100644 index 00000000..65fb2325 --- /dev/null +++ b/src/babashka/impl/pods.clj @@ -0,0 +1,153 @@ +(ns babashka.impl.pods + {:no-doc true} + (:refer-clojure :exclude [read]) + (:require [babashka.impl.bencode.core :as bencode] + [cheshire.core :as cheshire] + [clojure.core.async :as async] + [clojure.edn :as edn] + [sci.core :as sci])) + +(set! *warn-on-reflection* true) + +(defn add-shutdown-hook! [^Runnable f] + (-> (Runtime/getRuntime) + (.addShutdownHook (Thread. f)))) + +(defn write [^java.io.OutputStream stream v] + (locking stream + (bencode/write-bencode stream v) + (.flush stream))) + +(defn read [stream] + (bencode/read-bencode stream)) + +(defn bytes->string [^"[B" bytes] + (String. bytes)) + +(defn get-string [m k] + (-> (get m k) + bytes->string)) + +(defn processor [_ctx pod] + (let [stdout (:stdout pod) + format (:format pod) + chans (:chans pod) + read-fn (case format + :edn edn/read-string + :json #(cheshire/parse-string-strict % true))] + (try + (loop [] + (let [reply (read stdout) + id (get reply "id") + id (bytes->string id) + value* (find reply "value") + value (some-> value* + second + bytes->string + read-fn) + status (get reply "status") + status (set (map (comp keyword bytes->string) status)) + done? (contains? status :done) + error? (contains? status :error) + value (if error? + (let [message (or (some-> (get reply "ex-message") + bytes->string) + "") + data (or (some-> (get reply "ex-data") + bytes->string + read-fn) + {})] + (ex-info message data)) + value) + chan (get @chans id) + out (some-> (get reply "out") + bytes->string) + err (some-> (get reply "err") + bytes->string)] + (when (or value* error?) (async/put! chan value)) + (when (or done? error?) (async/close! chan)) + (when out (binding [*out* @sci/out] + (println out))) + (when err (binding [*out* @sci/err] + (println err)))) + (recur)) + (catch Exception e + (binding [*out* @sci/err] + (prn e)))))) + +(defn invoke [pod pod-var args async?] + (let [stream (:stdin pod) + format (:format pod) + chans (:chans pod) + write-fn (case format + :edn pr-str + :json cheshire/generate-string) + id (str (java.util.UUID/randomUUID)) + chan (async/chan) + _ (swap! chans assoc id chan) + _ (write stream {"id" id + "op" "invoke" + "var" (str pod-var) + "args" (write-fn args)})] + (if async? chan ;; TODO: https://blog.jakubholy.net/2019/core-async-error-handling/ + (let [v (async/ (get reply "format") bytes->string keyword) + pod {:process p + :pod-spec pod-spec + :stdin stdin + :stdout stdout + :chans (atom {}) + :format format} + pod-namespaces (get reply "namespaces") + vars-fn (fn [ns-name-str vars] + (reduce + (fn [m var] + (let [name (get-string var "name") + async? (some-> (get var "async") + bytes->string + #(Boolean/parseBoolean %)) + name-sym (symbol name) + sym (symbol ns-name-str name)] + (assoc m name-sym (fn [& args] + (let [res (invoke pod sym args async?)] + res))))) + {} + vars)) + env (:env ctx)] + (swap! env + (fn [env] + (let [namespaces (:namespaces env) + namespaces + (reduce (fn [namespaces namespace] + (let [name-str (-> namespace (get "name") bytes->string) + name-sym (symbol name-str) + vars (get namespace "vars") + vars (vars-fn name-str vars)] + (assoc namespaces name-sym vars))) + namespaces + pod-namespaces)] + (assoc env :namespaces namespaces)))) + (sci/future (processor ctx pod)) + ;; TODO: we could return the entire describe map here + nil))) + +(def pods-namespace + {'load-pod (with-meta load-pod + {:sci.impl/op :needs-ctx})}) diff --git a/src/babashka/main.clj b/src/babashka/main.clj index 92beaedc..87f2fc66 100644 --- a/src/babashka/main.clj +++ b/src/babashka/main.clj @@ -15,6 +15,7 @@ [babashka.impl.curl :refer [curl-namespace]] [babashka.impl.features :as features] [babashka.impl.nrepl-server :as nrepl-server] + [babashka.impl.pods :as pods] [babashka.impl.repl :as repl] [babashka.impl.socket-repl :as socket-repl] [babashka.impl.test :as t] @@ -349,6 +350,7 @@ If neither -e, -f, or --socket-repl are specified, then the first argument that 'babashka.classpath {'add-classpath add-classpath*} 'clojure.pprint pprint-namespace 'babashka.curl curl-namespace + 'babashka.pods pods/pods-namespace 'bencode.core bencode-namespace} features/xml? (assoc 'clojure.data.xml @(resolve 'babashka.impl.xml/xml-namespace)) features/yaml? (assoc 'clj-yaml.core @(resolve 'babashka.impl.yaml/yaml-namespace)) diff --git a/test-resources/pod.clj b/test-resources/pod.clj new file mode 100644 index 00000000..31f3647e --- /dev/null +++ b/test-resources/pod.clj @@ -0,0 +1,146 @@ +(ns pod + (:refer-clojure :exclude [read read-string]) + (:require [babashka.pods :as pods] + [bencode.core :as bencode] + [cheshire.core :as cheshire] + [clojure.core.async :as async] + [clojure.edn :as edn] + [clojure.java.io :as io]) + (:import [java.io PushbackInputStream]) + (:gen-class)) + +(def debug? false) + +(defn debug [& args] + (when debug? + (binding [*out* (io/writer "/tmp/log.txt" :append true)] + (apply println args)))) + +(def stdin (PushbackInputStream. System/in)) + +(defn write [v] + (bencode/write-bencode System/out v) + (.flush System/out)) + +(defn read-string [^"[B" v] + (String. v)) + +(defn read [] + (bencode/read-bencode stdin)) + +(defn run-pod [cli-args] + (let [format (if (contains? cli-args "--json") + :json + :edn) + write-fn (if (identical? :json format) + cheshire/generate-string + pr-str) + read-fn (if (identical? :json format) + #(cheshire/parse-string % true) + edn/read-string)] + (loop [] + (let [message (try (read) + (catch java.io.EOFException _ + ::EOF))] + (when-not (identical? ::EOF message) + (let [op (get message "op") + op (read-string op) + op (keyword op)] + (case op + ;; TODO: + ;; group by namespace + :describe (do (write {"format" (if (= format :json) + "json" + "edn") + "namespaces" + [{"name" "pod.test-pod" + "vars" [{"name" "add-sync"} + {"name" "range-stream" + "async" "true"} + {"name" "assoc"} + {"name" "error"} + {"name" "print"} + {"name" "print-err"}]}]}) + (recur)) + :invoke (let [var (-> (get message "var") + read-string + symbol) + _ (debug "var" var) + id (-> (get message "id") + read-string) + args (get message "args") + args (read-string args) + args (read-fn args)] + (case var + pod.test-pod/add-sync (write + {"value" (write-fn (apply + args)) + "id" id + "status" ["done"]}) + pod.test-pod/range-stream + (let [rng (apply range args)] + (doseq [v rng] + (write + {"value" (write-fn v) + "id" id}) + (Thread/sleep 100)) + (write + {"status" ["done"] + "id" id})) + pod.test-pod/assoc + (write + {"value" (write-fn (apply assoc args)) + "status" ["done"] + "id" id}) + pod.test-pod/error + (write + {"ex-data" (write-fn {:args args}) + "ex-message" (str "Illegal arguments") + "status" ["done" "error"] + "id" id}) + pod.test-pod/print + (do (write + {"out" (pr-str args) + "id" id}) + (write + {"status" ["done"] + "id" id})) + pod.test-pod/print-err + (do (write + {"err" (pr-str args) + "id" id}) + (write + {"status" ["done"] + "id" id}))) + (recur))))))))) + +(let [cli-args (set *command-line-args*)] + (if (contains? cli-args "--run-as-pod") + (do (debug "running pod with cli args" cli-args) + (run-pod cli-args)) + (let [native? (contains? cli-args "--native")] + (pods/load-pod (if native? + (into ["./bb" "test-resources/pod.clj" "--run-as-pod"] cli-args) + (into ["lein" "bb" "test-resources/pod.clj" "--run-as-pod"] cli-args))) + (require '[pod.test-pod]) + (if (contains? cli-args "--json") + (do + (debug "Running JSON test") + (prn ((resolve 'pod.test-pod/assoc) {:a 1} :b 2))) + (do + (debug "Running synchronous add test") + (prn ((resolve 'pod.test-pod/add-sync) 1 2 3)) + (debug "Running async stream test") + (let [chan ((resolve 'pod.test-pod/range-stream) 1 10)] + (loop [] + (when-let [x (async/ ["-f" "test-resources/pod.clj"] + native? + (conj "--native"))) + err (str sw)] + (is (= "6\n1\n2\n3\n4\n5\n6\n7\n8\n9\n\"Illegal arguments / {:args (1 2 3)}\"\n(\"hello\" \"print\" \"this\" \"debugging\" \"message\")\n" res)) + (when-not tu/native? + (is (= "(\"hello\" \"print\" \"this\" \"error\")\n" err))) + (is (= {:a 1 :b 2} + (edn/read-string + (apply tu/bb nil (cond-> ["-f" "test-resources/pod.clj" "--json"] + native? + (conj "--native")))))))) diff --git a/test/babashka/test_utils.clj b/test/babashka/test_utils.clj index b5bfd387..b4dcefd9 100644 --- a/test/babashka/test_utils.clj +++ b/test/babashka/test_utils.clj @@ -7,24 +7,25 @@ (set! *warn-on-reflection* true) -(defn bb-jvm [input & args] +(defn bb-jvm [input-or-opts & args] (reset! main/cp-state nil) (let [os (java.io.StringWriter.) - es (java.io.StringWriter.) - is (when input - (java.io.StringReader. input)) + es (if-let [err (:err input-or-opts)] + err (java.io.StringWriter.)) + is (when (string? input-or-opts) + (java.io.StringReader. input-or-opts)) bindings-map (cond-> {sci/out os sci/err es} is (assoc sci/in is))] (try - (when input (vars/bindRoot sci/in is)) + (when (string? input-or-opts) (vars/bindRoot sci/in is)) (vars/bindRoot sci/out os) (vars/bindRoot sci/err es) (sci/with-bindings bindings-map (let [res (binding [*out* os *err* es] - (if input - (with-in-str input (apply main/main args)) + (if (string? input-or-opts) + (with-in-str input-or-opts (apply main/main args)) (apply main/main args)))] (if (zero? res) (str os) @@ -32,7 +33,7 @@ {:stdout (str os) :stderr (str es)}))))) (finally - (when input (vars/bindRoot sci/in *in*)) + (when (string? input-or-opts) (vars/bindRoot sci/in *in*)) (vars/bindRoot sci/out *out*) (vars/bindRoot sci/err *err*)))))