1use super::arg_str;
34use crate::host::{with_host, IoTask, JsObj};
35use fusevm::Value;
36use indexmap::IndexMap;
37use std::process::{Child, Command, Stdio};
38use std::sync::atomic::{AtomicU64, Ordering};
39use std::sync::mpsc::Sender;
40use std::sync::{Arc, Mutex};
41
42pub const METHODS: &[&str] = &[
43 "execSync",
44 "spawnSync",
45 "execFileSync",
46 "exec",
47 "execFile",
48 "spawn",
49 "fork",
50];
51
52pub const CHILD_PROCESS_METHODS: &[&str] = &["kill", "send", "disconnect", "ref", "unref"];
55
56pub fn call(method: &str, args: &[Value]) -> Option<Result<Value, String>> {
57 Some(match method {
58 "execSync" => exec_sync(args),
59 "spawnSync" => spawn_sync(args),
60 "execFileSync" => exec_file_sync(args),
61 "exec" => exec(args),
62 "execFile" => exec_file(args),
63 "spawn" => spawn(args),
64 "fork" => fork(args),
65 _ => return None,
66 })
67}
68
69static NEXT_CHILD_ID: AtomicU64 = AtomicU64::new(1);
73
74struct ChildRec {
77 emitter: Value,
78 handle: Arc<Mutex<Option<Child>>>,
79}
80
81thread_local! {
82 static CHILDREN: std::cell::RefCell<std::collections::HashMap<u64, ChildRec>> =
83 std::cell::RefCell::new(std::collections::HashMap::new());
84}
85
86fn child_object(extra: IndexMap<String, Value>) -> Value {
89 super::net::new_emitter_object("ChildProcess", extra)
90}
91
92struct Run {
95 status: Option<i32>,
96 stdout: Vec<u8>,
97 stderr: Vec<u8>,
98 pid: u32,
99}
100
101#[derive(Default)]
103struct SpawnOpts {
104 input: Option<Vec<u8>>,
105 env: Option<Vec<(String, String)>>,
107 cwd: Option<String>,
108}
109
110fn spawn_opts(args: &[Value], idx: usize) -> SpawnOpts {
117 let Some(opts) = args.get(idx) else {
118 return SpawnOpts::default();
119 };
120 let read = |k: &str| crate::builtins::get_property(opts, k).ok();
121 let input = match read("input") {
122 Some(Value::Undef) | None => None,
123 Some(v) => Some(super::arg_str(&[v], 0).into_bytes()),
124 };
125 let cwd = match read("cwd") {
126 Some(Value::Undef) | None => None,
127 Some(v) => Some(with_host(|h| h.str_of(&v))),
128 };
129 let env = match read("env") {
130 Some(v) if with_host(|h| matches!(h.get(&v), Some(JsObj::Object(_)))) => {
131 let keys = with_host(|h| match h.get(&v) {
132 Some(JsObj::Object(m)) => m
133 .keys()
134 .filter(|k| !k.starts_with("@@"))
135 .cloned()
136 .collect::<Vec<_>>(),
137 _ => Vec::new(),
138 });
139 Some(
140 keys.into_iter()
141 .filter_map(|k| {
142 let val = crate::builtins::get_property(&v, &k).ok()?;
143 Some((k, with_host(|h| h.str_of(&val))))
144 })
145 .collect(),
146 )
147 }
148 _ => None,
149 };
150 SpawnOpts { input, env, cwd }
151}
152
153fn run(program: &str, args: &[String], opts: &SpawnOpts) -> std::io::Result<Run> {
156 let input = opts.input.as_deref();
157 let mut cmd = Command::new(program);
158 cmd.args(args).stdout(Stdio::piped()).stderr(Stdio::piped());
159 if let Some(dir) = &opts.cwd {
160 cmd.current_dir(dir);
161 }
162 if let Some(vars) = &opts.env {
163 cmd.env_clear();
164 for (k, v) in vars {
165 cmd.env(k, v);
166 }
167 }
168 cmd.stdin(if input.is_some() {
169 Stdio::piped()
170 } else {
171 Stdio::inherit()
172 });
173 let mut child = cmd.spawn()?;
174 let pid = child.id();
175 if let Some(bytes) = input {
176 if let Some(mut stdin) = child.stdin.take() {
177 use std::io::Write as _;
178 let _ = stdin.write_all(bytes);
179 }
181 }
182 let out = child.wait_with_output()?;
183 Ok(Run {
184 status: out.status.code(),
185 stdout: out.stdout,
186 stderr: out.stderr,
187 pid,
188 })
189}
190
191fn echo_stderr(args: &[Value], opts_idx: usize, bytes: &[u8]) {
200 if bytes.is_empty() {
201 return;
202 }
203 let explicit_stdio = args
204 .get(opts_idx)
205 .and_then(|o| crate::builtins::get_property(o, "stdio").ok())
206 .is_some_and(|v| !matches!(v, Value::Undef));
207 if explicit_stdio {
208 return;
209 }
210 let text = String::from_utf8_lossy(bytes).into_owned();
211 with_host(|h| h.write_out(&text, true));
212}
213
214fn command_failed(cmd: &str, r: &Run, enc: Option<&str>) -> String {
221 let tail = String::from_utf8_lossy(&r.stderr).into_owned();
222 let msg = format!("Command failed: {cmd}\n{tail}");
223 let stdout = output_value(&r.stdout, enc);
226 let stderr = output_value(&r.stderr, enc);
227 let e = crate::builtins::make_error_pub("Error", &msg);
230 let null = with_host(|h| h.null());
231 let status = r
232 .status
233 .map(|c| Value::Float(c as f64))
234 .unwrap_or_else(|| null.clone());
235 for (k, v) in [
236 ("status", status),
237 ("signal", null),
238 ("pid", Value::Float(r.pid as f64)),
239 ("stdout", stdout),
240 ("stderr", stderr),
241 ] {
242 let _ = crate::builtins::set_property_pub(&e, k, v);
243 }
244 with_host(|h| h.exc = Some(e));
245 format!("Error: {msg}")
246}
247
248fn exec_error(cmd: &str, r: &Run) -> Value {
252 let tail = String::from_utf8_lossy(&r.stderr).into_owned();
253 let e = crate::builtins::make_error_pub("Error", &format!("Command failed: {cmd}\n{tail}"));
254 let null = with_host(|h| h.null());
255 let cmd_v = with_host(|h| h.new_str(cmd.to_string()));
256 for (k, v) in [
257 ("killed", Value::Bool(false)),
258 ("code", Value::Float(r.status.unwrap_or(-1) as f64)),
259 ("signal", null),
260 ("cmd", cmd_v),
261 ] {
262 let _ = crate::builtins::set_property_pub(&e, k, v);
263 }
264 e
265}
266
267fn spawn_error(file: &str, argv: &[String], e: &std::io::Error) -> Value {
272 let code = super::fs::libuv_code(e);
273 let err = crate::builtins::make_error_pub("Error", &format!("spawn {file} {code}"));
274 let errno = -f64::from(e.raw_os_error().unwrap_or(5));
275 let (code_v, syscall, path, spawnargs) = with_host(|h| {
276 let items = argv.iter().map(|a| h.new_str(a.clone())).collect();
277 (
278 h.new_str(code.to_string()),
279 h.new_str(format!("spawn {file}")),
280 h.new_str(file.to_string()),
281 h.new_array(items),
282 )
283 });
284 for (k, v) in [
285 ("errno", Value::Float(errno)),
286 ("code", code_v),
287 ("syscall", syscall),
288 ("path", path),
289 ("spawnargs", spawnargs),
290 ] {
291 let _ = crate::builtins::set_property_pub(&err, k, v);
292 }
293 err
294}
295
296fn exec_sync(args: &[Value]) -> Result<Value, String> {
299 let cmd = arg_str(args, 0);
300 let enc = opts_encoding(args, 1);
301 let r = run("sh", &["-c".to_string(), cmd.clone()], &spawn_opts(args, 1))
302 .map_err(|e| format!("Error: {e}"))?;
303 echo_stderr(args, 1, &r.stderr);
304 if r.status != Some(0) {
305 return Err(command_failed(&cmd, &r, enc.as_deref()));
306 }
307 Ok(output_value(&r.stdout, enc.as_deref()))
308}
309
310fn spawn_sync(args: &[Value]) -> Result<Value, String> {
313 let cmd = arg_str(args, 0);
314 let cmd_args = arg_array(args, 1);
315 let enc = opts_encoding(args, 2);
316 match run(&cmd, &cmd_args, &spawn_opts(args, 2)) {
317 Ok(r) => {
318 let stdout = output_value(&r.stdout, enc.as_deref());
322 let stderr = output_value(&r.stderr, enc.as_deref());
323 Ok(with_host(|h| {
324 let mut m = IndexMap::new();
325 m.insert("pid".into(), Value::Float(r.pid as f64));
326 m.insert(
327 "status".into(),
328 r.status
329 .map(|c| Value::Float(c as f64))
330 .unwrap_or_else(|| h.null()),
331 );
332 m.insert("signal".into(), h.null());
335 m.insert("stdout".into(), stdout);
336 m.insert("stderr".into(), stderr);
337 h.new_object(m)
338 }))
339 }
340 Err(e) => Ok(with_host(|h| {
343 let mut m = IndexMap::new();
344 m.insert("pid".into(), Value::Float(0.0));
345 m.insert("status".into(), h.null());
346 m.insert("signal".into(), h.null());
347 m.insert("stdout".into(), h.null());
348 m.insert("stderr".into(), h.null());
349 m.insert("error".into(), h.new_str(format!("Error: spawn {cmd} {e}")));
350 h.new_object(m)
351 })),
352 }
353}
354
355fn exec_file_sync(args: &[Value]) -> Result<Value, String> {
358 let file = arg_str(args, 0);
359 let cmd_args = arg_array(args, 1);
360 let enc = opts_encoding(args, 2);
361 let r = run(&file, &cmd_args, &spawn_opts(args, 2))
362 .map_err(|e| format!("Error: spawn {file} {e}"))?;
363 echo_stderr(args, 2, &r.stderr);
364 if r.status != Some(0) {
365 return Err(command_failed(&file, &r, enc.as_deref()));
369 }
370 Ok(output_value(&r.stdout, enc.as_deref()))
371}
372
373fn exec(args: &[Value]) -> Result<Value, String> {
377 let cmd = arg_str(args, 0);
378 let Some(cb) = args.last().cloned() else {
380 return Ok(Value::Undef);
381 };
382 let (err, out, errout) = match run("sh", &["-c".to_string(), cmd.clone()], &spawn_opts(args, 1))
383 {
384 Ok(r) => {
385 let stdout = String::from_utf8_lossy(&r.stdout).into_owned();
386 let stderr = String::from_utf8_lossy(&r.stderr).into_owned();
387 let err = if r.status == Some(0) {
394 with_host(|h| h.null())
395 } else {
396 exec_error(&cmd, &r)
397 };
398 (err, stdout, stderr)
399 }
400 Err(e) => (
401 with_host(|h| crate::builtins::synth_error(h, &format!("Error: {e}"))),
402 String::new(),
403 String::new(),
404 ),
405 };
406 with_host(|h| {
407 let so = h.new_str(out);
408 let se = h.new_str(errout);
409 h.queue_micro(cb, vec![err, so, se]);
410 });
411 Ok(Value::Undef)
412}
413
414fn spawn(args: &[Value]) -> Result<Value, String> {
418 let cmd = arg_str(args, 0);
419 let cmd_args = arg_array(args, 1);
420 match run(&cmd, &cmd_args, &spawn_opts(args, 2)) {
421 Ok(r) => {
422 let stdout = super::buffer::from_bytes(&r.stdout);
425 let stderr = super::buffer::from_bytes(&r.stderr);
426 let null = with_host(|h| h.null());
427 let mut m = IndexMap::new();
428 m.insert("pid".into(), Value::Float(r.pid as f64));
429 m.insert(
430 "exitCode".into(),
431 r.status
432 .map(|c| Value::Float(c as f64))
433 .unwrap_or_else(|| null.clone()),
434 );
435 m.insert("signalCode".into(), null);
436 m.insert("killed".into(), Value::Bool(false));
437 m.insert("connected".into(), Value::Bool(false));
438 m.insert("stdout".into(), stdout);
439 m.insert("stderr".into(), stderr);
440 Ok(child_object(m))
441 }
442 Err(e) => Err(format!("Error: spawn {cmd} {e}")),
443 }
444}
445
446fn exec_file(args: &[Value]) -> Result<Value, String> {
451 let file = arg_str(args, 0);
452 let cmd_args = arg_array(args, 1);
453 let cb = args
455 .iter()
456 .rev()
457 .find(|v| with_host(|h| crate::host::is_callable(h, v)))
458 .cloned();
459
460 let full_cmd = std::iter::once(file.clone())
461 .chain(cmd_args.iter().cloned())
462 .collect::<Vec<_>>()
463 .join(" ");
464
465 match run(&file, &cmd_args, &spawn_opts(args, 2)) {
466 Ok(r) => {
467 let stdout_buf = super::buffer::from_bytes(&r.stdout);
468 let stderr_buf = super::buffer::from_bytes(&r.stderr);
469 let null = with_host(|h| h.null());
470 if let Some(cb) = cb {
471 let so = String::from_utf8_lossy(&r.stdout).into_owned();
472 let se = String::from_utf8_lossy(&r.stderr).into_owned();
473 let err = if r.status == Some(0) {
480 null.clone()
481 } else {
482 exec_error(&full_cmd, &r)
483 };
484 with_host(|h| {
485 let so = h.new_str(so);
486 let se = h.new_str(se);
487 h.queue_micro(cb, vec![err, so, se]);
488 });
489 }
490 let mut m = IndexMap::new();
491 m.insert("pid".into(), Value::Float(r.pid as f64));
492 m.insert(
493 "exitCode".into(),
494 r.status
495 .map(|c| Value::Float(c as f64))
496 .unwrap_or_else(|| null.clone()),
497 );
498 m.insert("signalCode".into(), null);
499 m.insert("killed".into(), Value::Bool(false));
500 m.insert("connected".into(), Value::Bool(false));
501 m.insert("stdout".into(), stdout_buf);
502 m.insert("stderr".into(), stderr_buf);
503 Ok(child_object(m))
504 }
505 Err(e) => {
509 let err = spawn_error(&file, &cmd_args, &e);
510 if let Some(cb) = cb {
511 let (empty1, empty2) = with_host(|h| (h.new_str(""), h.new_str("")));
512 with_host(|h| h.queue_micro(cb, vec![err, empty1, empty2]));
513 let null = with_host(|h| h.null());
514 let mut m = IndexMap::new();
515 m.insert("pid".into(), Value::Undef);
516 m.insert("exitCode".into(), null.clone());
517 m.insert("signalCode".into(), null.clone());
518 m.insert("killed".into(), Value::Bool(false));
519 m.insert("connected".into(), Value::Bool(false));
520 m.insert("stdout".into(), null.clone());
521 m.insert("stderr".into(), null);
522 return Ok(child_object(m));
523 }
524 Err(format!("Error: spawn {file} {e}"))
525 }
526 }
527}
528
529fn fork(args: &[Value]) -> Result<Value, String> {
540 let module = arg_str(args, 0);
541 let extra_args = arg_array(args, 1);
542 let exe = std::env::current_exe().map_err(|e| format!("Error: fork: {e}"))?;
543
544 let mut cmd = Command::new(exe);
545 cmd.arg(&module).args(&extra_args);
546 cmd.stdin(Stdio::inherit())
547 .stdout(Stdio::inherit())
548 .stderr(Stdio::inherit());
549 let child = cmd
550 .spawn()
551 .map_err(|e| format!("Error: fork {module} {e}"))?;
552 let pid = child.id();
553
554 let id = NEXT_CHILD_ID.fetch_add(1, Ordering::Relaxed);
555 let handle = Arc::new(Mutex::new(Some(child)));
556
557 let mut extra = IndexMap::new();
558 extra.insert("@@childid".into(), Value::Float(id as f64));
559 extra.insert("pid".into(), Value::Float(pid as f64));
560 extra.insert("connected".into(), Value::Bool(false));
561 extra.insert("killed".into(), Value::Bool(false));
562 extra.insert("exitCode".into(), with_host(|h| h.null()));
563 extra.insert("signalCode".into(), with_host(|h| h.null()));
564 let emitter = child_object(extra);
565 CHILDREN.with(|c| {
566 c.borrow_mut().insert(
567 id,
568 ChildRec {
569 emitter: emitter.clone(),
570 handle: handle.clone(),
571 },
572 );
573 });
574 with_host(|h| h.incr_handle());
575
576 let io_tx = with_host(|h| h.io_sender());
577 std::thread::spawn(move || wait_child(id, handle, io_tx));
578 Ok(emitter)
579}
580
581fn wait_child(id: u64, handle: Arc<Mutex<Option<Child>>>, io_tx: Sender<IoTask>) {
585 loop {
586 std::thread::sleep(std::time::Duration::from_millis(20));
587 let status = {
588 let mut g = match handle.lock() {
589 Ok(g) => g,
590 Err(_) => return,
591 };
592 match g.as_mut() {
593 Some(child) => match child.try_wait() {
594 Ok(Some(status)) => {
595 *g = None;
596 Some(status.code())
597 }
598 Ok(None) => None,
599 Err(_) => {
600 *g = None;
601 Some(None)
602 }
603 },
604 None => return,
606 }
607 };
608 if let Some(code) = status {
609 let _ = io_tx.send(Box::new(move || on_child_exit(id, code)));
610 return;
611 }
612 }
613}
614
615fn on_child_exit(id: u64, code: Option<i32>) -> Result<(), String> {
618 let emitter = CHILDREN.with(|c| c.borrow().get(&id).map(|r| r.emitter.clone()));
619 let Some(emitter) = emitter else {
620 return Ok(());
621 };
622 let (code_val, null1, null2) = with_host(|h| {
623 let cv = code
624 .map(|c| Value::Float(c as f64))
625 .unwrap_or_else(|| h.null());
626 (cv, h.null(), h.null())
627 });
628 set_prop(&emitter, "exitCode", code_val.clone());
629 set_prop(&emitter, "killed", Value::Bool(true));
630 let ev_exit = with_host(|h| h.new_str("exit"));
631 let ev_close = with_host(|h| h.new_str("close"));
632 super::events::instance_call(&emitter, "emit", vec![ev_exit, code_val.clone(), null1])?;
633 super::events::instance_call(&emitter, "emit", vec![ev_close, code_val, null2])?;
634 CHILDREN.with(|c| c.borrow_mut().remove(&id));
635 with_host(|h| h.decr_handle());
636 let _ = with_host(|h| h.io_sender()).send(Box::new(|| Ok(())));
637 Ok(())
638}
639
640fn set_prop(recv: &Value, key: &str, val: Value) {
641 with_host(|h| {
642 if let Some(JsObj::Object(p)) = h.get_mut(recv) {
643 p.insert(key.to_string(), val);
644 }
645 });
646}
647
648pub fn instance_call(recv: &Value, method: &str, args: Vec<Value>) -> Result<Value, String> {
655 if super::events::METHODS.contains(&method) {
656 return super::events::instance_call(recv, method, args);
657 }
658 match method {
659 "kill" => Ok(Value::Bool(kill_child(recv))),
660 "send" => Ok(Value::Bool(false)),
662 "disconnect" => {
663 set_prop(recv, "connected", Value::Bool(false));
664 Ok(Value::Undef)
665 }
666 "ref" | "unref" => Ok(recv.clone()),
667 _ => Err(crate::host::type_error(&format!(
668 "child.{method} is not a function"
669 ))),
670 }
671}
672
673fn kill_child(recv: &Value) -> bool {
677 let id = with_host(|h| match h.get(recv) {
678 Some(JsObj::Object(p)) => p.get("@@childid").map(|v| h.to_number(v) as u64),
679 _ => None,
680 });
681 let Some(id) = id else { return false };
682 let handle = CHILDREN.with(|c| c.borrow().get(&id).map(|r| r.handle.clone()));
683 let Some(handle) = handle else { return false };
684 if let Ok(mut g) = handle.lock() {
685 if let Some(child) = g.as_mut() {
686 let _ = child.kill();
687 return true;
688 }
689 }
690 false
691}
692
693fn output_value(bytes: &[u8], encoding: Option<&str>) -> Value {
697 match encoding {
698 Some(enc) if !enc.eq_ignore_ascii_case("buffer") => {
699 with_host(|h| h.new_str(String::from_utf8_lossy(bytes).into_owned()))
700 }
701 _ => super::buffer::from_bytes(bytes),
702 }
703}
704
705fn arg_array(args: &[Value], i: usize) -> Vec<String> {
708 with_host(|h| match args.get(i).and_then(|v| h.get(v)) {
709 Some(crate::host::JsObj::Array(items)) => items.iter().map(|v| h.str_of(v)).collect(),
710 _ => Vec::new(),
711 })
712}
713
714fn opts_encoding(args: &[Value], i: usize) -> Option<String> {
717 with_host(|h| match args.get(i).and_then(|v| h.get(v)) {
718 Some(crate::host::JsObj::Object(p)) => p
719 .get("encoding")
720 .map(|v| h.str_of(v))
721 .filter(|s| !s.is_empty() && s != "undefined" && s != "null"),
722 _ => None,
723 })
724}