use super::arg_str;
use crate::host::{with_host, IoTask, JsObj};
use fusevm::Value;
use indexmap::IndexMap;
use std::process::{Child, Command, Stdio};
use std::sync::atomic::{AtomicU64, Ordering};
use std::sync::mpsc::Sender;
use std::sync::{Arc, Mutex};
pub const METHODS: &[&str] = &[
"execSync",
"spawnSync",
"execFileSync",
"exec",
"execFile",
"spawn",
"fork",
];
pub const CHILD_PROCESS_METHODS: &[&str] = &["kill", "send", "disconnect", "ref", "unref"];
pub fn call(method: &str, args: &[Value]) -> Option<Result<Value, String>> {
Some(match method {
"execSync" => exec_sync(args),
"spawnSync" => spawn_sync(args),
"execFileSync" => exec_file_sync(args),
"exec" => exec(args),
"execFile" => exec_file(args),
"spawn" => spawn(args),
"fork" => fork(args),
_ => return None,
})
}
static NEXT_CHILD_ID: AtomicU64 = AtomicU64::new(1);
struct ChildRec {
emitter: Value,
handle: Arc<Mutex<Option<Child>>>,
}
thread_local! {
static CHILDREN: std::cell::RefCell<std::collections::HashMap<u64, ChildRec>> =
std::cell::RefCell::new(std::collections::HashMap::new());
}
fn child_object(extra: IndexMap<String, Value>) -> Value {
super::net::new_emitter_object("ChildProcess", extra)
}
struct Run {
status: Option<i32>,
stdout: Vec<u8>,
stderr: Vec<u8>,
pid: u32,
}
#[derive(Default)]
struct SpawnOpts {
input: Option<Vec<u8>>,
env: Option<Vec<(String, String)>>,
cwd: Option<String>,
}
fn spawn_opts(args: &[Value], idx: usize) -> SpawnOpts {
let Some(opts) = args.get(idx) else {
return SpawnOpts::default();
};
let read = |k: &str| crate::builtins::get_property(opts, k).ok();
let input = match read("input") {
Some(Value::Undef) | None => None,
Some(v) => Some(super::arg_str(&[v], 0).into_bytes()),
};
let cwd = match read("cwd") {
Some(Value::Undef) | None => None,
Some(v) => Some(with_host(|h| h.str_of(&v))),
};
let env = match read("env") {
Some(v) if with_host(|h| matches!(h.get(&v), Some(JsObj::Object(_)))) => {
let keys = with_host(|h| match h.get(&v) {
Some(JsObj::Object(m)) => m
.keys()
.filter(|k| !k.starts_with("@@"))
.cloned()
.collect::<Vec<_>>(),
_ => Vec::new(),
});
Some(
keys.into_iter()
.filter_map(|k| {
let val = crate::builtins::get_property(&v, &k).ok()?;
Some((k, with_host(|h| h.str_of(&val))))
})
.collect(),
)
}
_ => None,
};
SpawnOpts { input, env, cwd }
}
fn run(program: &str, args: &[String], opts: &SpawnOpts) -> std::io::Result<Run> {
let input = opts.input.as_deref();
let mut cmd = Command::new(program);
cmd.args(args).stdout(Stdio::piped()).stderr(Stdio::piped());
if let Some(dir) = &opts.cwd {
cmd.current_dir(dir);
}
if let Some(vars) = &opts.env {
cmd.env_clear();
for (k, v) in vars {
cmd.env(k, v);
}
}
cmd.stdin(if input.is_some() {
Stdio::piped()
} else {
Stdio::inherit()
});
let mut child = cmd.spawn()?;
let pid = child.id();
if let Some(bytes) = input {
if let Some(mut stdin) = child.stdin.take() {
use std::io::Write as _;
let _ = stdin.write_all(bytes);
}
}
let out = child.wait_with_output()?;
Ok(Run {
status: out.status.code(),
stdout: out.stdout,
stderr: out.stderr,
pid,
})
}
fn echo_stderr(args: &[Value], opts_idx: usize, bytes: &[u8]) {
if bytes.is_empty() {
return;
}
let explicit_stdio = args
.get(opts_idx)
.and_then(|o| crate::builtins::get_property(o, "stdio").ok())
.is_some_and(|v| !matches!(v, Value::Undef));
if explicit_stdio {
return;
}
let text = String::from_utf8_lossy(bytes).into_owned();
with_host(|h| h.write_out(&text, true));
}
fn command_failed(cmd: &str, r: &Run, enc: Option<&str>) -> String {
let tail = String::from_utf8_lossy(&r.stderr).into_owned();
let msg = format!("Command failed: {cmd}\n{tail}");
let stdout = output_value(&r.stdout, enc);
let stderr = output_value(&r.stderr, enc);
let e = crate::builtins::make_error_pub("Error", &msg);
let null = with_host(|h| h.null());
let status = r
.status
.map(|c| Value::Float(c as f64))
.unwrap_or_else(|| null.clone());
for (k, v) in [
("status", status),
("signal", null),
("pid", Value::Float(r.pid as f64)),
("stdout", stdout),
("stderr", stderr),
] {
let _ = crate::builtins::set_property_pub(&e, k, v);
}
with_host(|h| h.exc = Some(e));
format!("Error: {msg}")
}
fn exec_error(cmd: &str, r: &Run) -> Value {
let tail = String::from_utf8_lossy(&r.stderr).into_owned();
let e = crate::builtins::make_error_pub("Error", &format!("Command failed: {cmd}\n{tail}"));
let null = with_host(|h| h.null());
let cmd_v = with_host(|h| h.new_str(cmd.to_string()));
for (k, v) in [
("killed", Value::Bool(false)),
("code", Value::Float(r.status.unwrap_or(-1) as f64)),
("signal", null),
("cmd", cmd_v),
] {
let _ = crate::builtins::set_property_pub(&e, k, v);
}
e
}
fn spawn_error(file: &str, argv: &[String], e: &std::io::Error) -> Value {
let code = super::fs::libuv_code(e);
let err = crate::builtins::make_error_pub("Error", &format!("spawn {file} {code}"));
let errno = -f64::from(e.raw_os_error().unwrap_or(5));
let (code_v, syscall, path, spawnargs) = with_host(|h| {
let items = argv.iter().map(|a| h.new_str(a.clone())).collect();
(
h.new_str(code.to_string()),
h.new_str(format!("spawn {file}")),
h.new_str(file.to_string()),
h.new_array(items),
)
});
for (k, v) in [
("errno", Value::Float(errno)),
("code", code_v),
("syscall", syscall),
("path", path),
("spawnargs", spawnargs),
] {
let _ = crate::builtins::set_property_pub(&err, k, v);
}
err
}
fn exec_sync(args: &[Value]) -> Result<Value, String> {
let cmd = arg_str(args, 0);
let enc = opts_encoding(args, 1);
let r = run("sh", &["-c".to_string(), cmd.clone()], &spawn_opts(args, 1))
.map_err(|e| format!("Error: {e}"))?;
echo_stderr(args, 1, &r.stderr);
if r.status != Some(0) {
return Err(command_failed(&cmd, &r, enc.as_deref()));
}
Ok(output_value(&r.stdout, enc.as_deref()))
}
fn spawn_sync(args: &[Value]) -> Result<Value, String> {
let cmd = arg_str(args, 0);
let cmd_args = arg_array(args, 1);
let enc = opts_encoding(args, 2);
match run(&cmd, &cmd_args, &spawn_opts(args, 2)) {
Ok(r) => {
let stdout = output_value(&r.stdout, enc.as_deref());
let stderr = output_value(&r.stderr, enc.as_deref());
Ok(with_host(|h| {
let mut m = IndexMap::new();
m.insert("pid".into(), Value::Float(r.pid as f64));
m.insert(
"status".into(),
r.status
.map(|c| Value::Float(c as f64))
.unwrap_or_else(|| h.null()),
);
m.insert("signal".into(), h.null());
m.insert("stdout".into(), stdout);
m.insert("stderr".into(), stderr);
h.new_object(m)
}))
}
Err(e) => Ok(with_host(|h| {
let mut m = IndexMap::new();
m.insert("pid".into(), Value::Float(0.0));
m.insert("status".into(), h.null());
m.insert("signal".into(), h.null());
m.insert("stdout".into(), h.null());
m.insert("stderr".into(), h.null());
m.insert("error".into(), h.new_str(format!("Error: spawn {cmd} {e}")));
h.new_object(m)
})),
}
}
fn exec_file_sync(args: &[Value]) -> Result<Value, String> {
let file = arg_str(args, 0);
let cmd_args = arg_array(args, 1);
let enc = opts_encoding(args, 2);
let r = run(&file, &cmd_args, &spawn_opts(args, 2))
.map_err(|e| format!("Error: spawn {file} {e}"))?;
echo_stderr(args, 2, &r.stderr);
if r.status != Some(0) {
return Err(command_failed(&file, &r, enc.as_deref()));
}
Ok(output_value(&r.stdout, enc.as_deref()))
}
fn exec(args: &[Value]) -> Result<Value, String> {
let cmd = arg_str(args, 0);
let Some(cb) = args.last().cloned() else {
return Ok(Value::Undef);
};
let (err, out, errout) = match run("sh", &["-c".to_string(), cmd.clone()], &spawn_opts(args, 1))
{
Ok(r) => {
let stdout = String::from_utf8_lossy(&r.stdout).into_owned();
let stderr = String::from_utf8_lossy(&r.stderr).into_owned();
let err = if r.status == Some(0) {
with_host(|h| h.null())
} else {
exec_error(&cmd, &r)
};
(err, stdout, stderr)
}
Err(e) => (
with_host(|h| crate::builtins::synth_error(h, &format!("Error: {e}"))),
String::new(),
String::new(),
),
};
with_host(|h| {
let so = h.new_str(out);
let se = h.new_str(errout);
h.queue_micro(cb, vec![err, so, se]);
});
Ok(Value::Undef)
}
fn spawn(args: &[Value]) -> Result<Value, String> {
let cmd = arg_str(args, 0);
let cmd_args = arg_array(args, 1);
match run(&cmd, &cmd_args, &spawn_opts(args, 2)) {
Ok(r) => {
let stdout = super::buffer::from_bytes(&r.stdout);
let stderr = super::buffer::from_bytes(&r.stderr);
let null = with_host(|h| h.null());
let mut m = IndexMap::new();
m.insert("pid".into(), Value::Float(r.pid as f64));
m.insert(
"exitCode".into(),
r.status
.map(|c| Value::Float(c as f64))
.unwrap_or_else(|| null.clone()),
);
m.insert("signalCode".into(), null);
m.insert("killed".into(), Value::Bool(false));
m.insert("connected".into(), Value::Bool(false));
m.insert("stdout".into(), stdout);
m.insert("stderr".into(), stderr);
Ok(child_object(m))
}
Err(e) => Err(format!("Error: spawn {cmd} {e}")),
}
}
fn exec_file(args: &[Value]) -> Result<Value, String> {
let file = arg_str(args, 0);
let cmd_args = arg_array(args, 1);
let cb = args
.iter()
.rev()
.find(|v| with_host(|h| crate::host::is_callable(h, v)))
.cloned();
let full_cmd = std::iter::once(file.clone())
.chain(cmd_args.iter().cloned())
.collect::<Vec<_>>()
.join(" ");
match run(&file, &cmd_args, &spawn_opts(args, 2)) {
Ok(r) => {
let stdout_buf = super::buffer::from_bytes(&r.stdout);
let stderr_buf = super::buffer::from_bytes(&r.stderr);
let null = with_host(|h| h.null());
if let Some(cb) = cb {
let so = String::from_utf8_lossy(&r.stdout).into_owned();
let se = String::from_utf8_lossy(&r.stderr).into_owned();
let err = if r.status == Some(0) {
null.clone()
} else {
exec_error(&full_cmd, &r)
};
with_host(|h| {
let so = h.new_str(so);
let se = h.new_str(se);
h.queue_micro(cb, vec![err, so, se]);
});
}
let mut m = IndexMap::new();
m.insert("pid".into(), Value::Float(r.pid as f64));
m.insert(
"exitCode".into(),
r.status
.map(|c| Value::Float(c as f64))
.unwrap_or_else(|| null.clone()),
);
m.insert("signalCode".into(), null);
m.insert("killed".into(), Value::Bool(false));
m.insert("connected".into(), Value::Bool(false));
m.insert("stdout".into(), stdout_buf);
m.insert("stderr".into(), stderr_buf);
Ok(child_object(m))
}
Err(e) => {
let err = spawn_error(&file, &cmd_args, &e);
if let Some(cb) = cb {
let (empty1, empty2) = with_host(|h| (h.new_str(""), h.new_str("")));
with_host(|h| h.queue_micro(cb, vec![err, empty1, empty2]));
let null = with_host(|h| h.null());
let mut m = IndexMap::new();
m.insert("pid".into(), Value::Undef);
m.insert("exitCode".into(), null.clone());
m.insert("signalCode".into(), null.clone());
m.insert("killed".into(), Value::Bool(false));
m.insert("connected".into(), Value::Bool(false));
m.insert("stdout".into(), null.clone());
m.insert("stderr".into(), null);
return Ok(child_object(m));
}
Err(format!("Error: spawn {file} {e}"))
}
}
}
fn fork(args: &[Value]) -> Result<Value, String> {
let module = arg_str(args, 0);
let extra_args = arg_array(args, 1);
let exe = std::env::current_exe().map_err(|e| format!("Error: fork: {e}"))?;
let mut cmd = Command::new(exe);
cmd.arg(&module).args(&extra_args);
cmd.stdin(Stdio::inherit())
.stdout(Stdio::inherit())
.stderr(Stdio::inherit());
let child = cmd
.spawn()
.map_err(|e| format!("Error: fork {module} {e}"))?;
let pid = child.id();
let id = NEXT_CHILD_ID.fetch_add(1, Ordering::Relaxed);
let handle = Arc::new(Mutex::new(Some(child)));
let mut extra = IndexMap::new();
extra.insert("@@childid".into(), Value::Float(id as f64));
extra.insert("pid".into(), Value::Float(pid as f64));
extra.insert("connected".into(), Value::Bool(false));
extra.insert("killed".into(), Value::Bool(false));
extra.insert("exitCode".into(), with_host(|h| h.null()));
extra.insert("signalCode".into(), with_host(|h| h.null()));
let emitter = child_object(extra);
CHILDREN.with(|c| {
c.borrow_mut().insert(
id,
ChildRec {
emitter: emitter.clone(),
handle: handle.clone(),
},
);
});
with_host(|h| h.incr_handle());
let io_tx = with_host(|h| h.io_sender());
std::thread::spawn(move || wait_child(id, handle, io_tx));
Ok(emitter)
}
fn wait_child(id: u64, handle: Arc<Mutex<Option<Child>>>, io_tx: Sender<IoTask>) {
loop {
std::thread::sleep(std::time::Duration::from_millis(20));
let status = {
let mut g = match handle.lock() {
Ok(g) => g,
Err(_) => return,
};
match g.as_mut() {
Some(child) => match child.try_wait() {
Ok(Some(status)) => {
*g = None;
Some(status.code())
}
Ok(None) => None,
Err(_) => {
*g = None;
Some(None)
}
},
None => return,
}
};
if let Some(code) = status {
let _ = io_tx.send(Box::new(move || on_child_exit(id, code)));
return;
}
}
}
fn on_child_exit(id: u64, code: Option<i32>) -> Result<(), String> {
let emitter = CHILDREN.with(|c| c.borrow().get(&id).map(|r| r.emitter.clone()));
let Some(emitter) = emitter else {
return Ok(());
};
let (code_val, null1, null2) = with_host(|h| {
let cv = code
.map(|c| Value::Float(c as f64))
.unwrap_or_else(|| h.null());
(cv, h.null(), h.null())
});
set_prop(&emitter, "exitCode", code_val.clone());
set_prop(&emitter, "killed", Value::Bool(true));
let ev_exit = with_host(|h| h.new_str("exit"));
let ev_close = with_host(|h| h.new_str("close"));
super::events::instance_call(&emitter, "emit", vec![ev_exit, code_val.clone(), null1])?;
super::events::instance_call(&emitter, "emit", vec![ev_close, code_val, null2])?;
CHILDREN.with(|c| c.borrow_mut().remove(&id));
with_host(|h| h.decr_handle());
let _ = with_host(|h| h.io_sender()).send(Box::new(|| Ok(())));
Ok(())
}
fn set_prop(recv: &Value, key: &str, val: Value) {
with_host(|h| {
if let Some(JsObj::Object(p)) = h.get_mut(recv) {
p.insert(key.to_string(), val);
}
});
}
pub fn instance_call(recv: &Value, method: &str, args: Vec<Value>) -> Result<Value, String> {
if super::events::METHODS.contains(&method) {
return super::events::instance_call(recv, method, args);
}
match method {
"kill" => Ok(Value::Bool(kill_child(recv))),
"send" => Ok(Value::Bool(false)),
"disconnect" => {
set_prop(recv, "connected", Value::Bool(false));
Ok(Value::Undef)
}
"ref" | "unref" => Ok(recv.clone()),
_ => Err(crate::host::type_error(&format!(
"child.{method} is not a function"
))),
}
}
fn kill_child(recv: &Value) -> bool {
let id = with_host(|h| match h.get(recv) {
Some(JsObj::Object(p)) => p.get("@@childid").map(|v| h.to_number(v) as u64),
_ => None,
});
let Some(id) = id else { return false };
let handle = CHILDREN.with(|c| c.borrow().get(&id).map(|r| r.handle.clone()));
let Some(handle) = handle else { return false };
if let Ok(mut g) = handle.lock() {
if let Some(child) = g.as_mut() {
let _ = child.kill();
return true;
}
}
false
}
fn output_value(bytes: &[u8], encoding: Option<&str>) -> Value {
match encoding {
Some(enc) if !enc.eq_ignore_ascii_case("buffer") => {
with_host(|h| h.new_str(String::from_utf8_lossy(bytes).into_owned()))
}
_ => super::buffer::from_bytes(bytes),
}
}
fn arg_array(args: &[Value], i: usize) -> Vec<String> {
with_host(|h| match args.get(i).and_then(|v| h.get(v)) {
Some(crate::host::JsObj::Array(items)) => items.iter().map(|v| h.str_of(v)).collect(),
_ => Vec::new(),
})
}
fn opts_encoding(args: &[Value], i: usize) -> Option<String> {
with_host(|h| match args.get(i).and_then(|v| h.get(v)) {
Some(crate::host::JsObj::Object(p)) => p
.get("encoding")
.map(|v| h.str_of(v))
.filter(|s| !s.is_empty() && s != "undefined" && s != "null"),
_ => None,
})
}