1use super::arg_str;
46use crate::host::{with_host, IoTask, JsObj};
47use fusevm::Value;
48use indexmap::IndexMap;
49use std::cell::RefCell;
50use std::collections::HashMap;
51use std::process::{Command, Stdio};
52use std::sync::atomic::{AtomicU64, Ordering};
53
54pub const METHODS: &[&str] = &[
58 "fork",
59 "setupPrimary",
60 "setupMaster",
61 "disconnect",
62 "on",
64 "addListener",
65 "prependListener",
66 "once",
67 "prependOnceListener",
68 "emit",
69 "removeListener",
70 "off",
71 "removeAllListeners",
72 "listenerCount",
73 "listeners",
74 "eventNames",
75 "setMaxListeners",
76 "getMaxListeners",
77];
78
79pub const WORKER_METHODS: &[&str] = &[
82 "send",
83 "kill",
84 "destroy",
85 "disconnect",
86 "isConnected",
87 "isDead",
88];
89
90const EMITTER_METHODS: &[&str] = &[
92 "on",
93 "addListener",
94 "prependListener",
95 "once",
96 "prependOnceListener",
97 "emit",
98 "removeListener",
99 "off",
100 "removeAllListeners",
101 "listenerCount",
102 "listeners",
103 "eventNames",
104 "setMaxListeners",
105 "getMaxListeners",
106];
107
108static NEXT_WORKER_ID: AtomicU64 = AtomicU64::new(1);
110
111#[derive(Default, Clone)]
114struct Settings {
115 exec: Option<String>,
117 args: Option<Vec<String>>,
119 exec_argv: Option<Vec<String>>,
122 silent: bool,
124}
125
126thread_local! {
127 static WORKERS: RefCell<HashMap<u64, Value>> = RefCell::new(HashMap::new());
130 static CLUSTER_EMITTER: RefCell<Option<Value>> = const { RefCell::new(None) };
134 static SELF_WORKER: RefCell<Option<Value>> = const { RefCell::new(None) };
136 static SETTINGS: RefCell<Settings> = RefCell::new(Settings::default());
138}
139
140fn worker_id_from_env() -> Option<u64> {
145 std::env::var("CLUSTER_WORKER")
146 .ok()
147 .or_else(|| std::env::var("NODE_UNIQUE_ID").ok())
148 .and_then(|s| s.trim().parse::<u64>().ok())
149}
150
151fn is_primary() -> bool {
153 worker_id_from_env().is_none()
154}
155
156pub fn call(method: &str, args: &[Value]) -> Option<Result<Value, String>> {
159 if EMITTER_METHODS.contains(&method) {
161 let em = cluster_emitter();
162 return Some(super::events::instance_call(&em, method, args.to_vec()));
163 }
164 Some(match method {
165 "fork" => fork(args),
166 "setupPrimary" | "setupMaster" => setup_primary(args),
167 "disconnect" => disconnect(args),
168 _ => return None,
169 })
170}
171
172pub fn constant(name: &str) -> Option<Value> {
174 Some(match name {
175 "isPrimary" | "isMaster" => Value::Bool(is_primary()),
176 "isWorker" => Value::Bool(!is_primary()),
177 "workers" => workers_object(),
178 "worker" => {
179 if is_primary() {
180 with_host(|h| h.null())
181 } else {
182 self_worker()
183 }
184 }
185 "settings" => settings_object(),
186 "SCHED_NONE" => Value::Float(1.0),
189 "SCHED_RR" => Value::Float(2.0),
190 "schedulingPolicy" => Value::Float(1.0),
191 _ => return None,
192 })
193}
194
195fn fork(args: &[Value]) -> Result<Value, String> {
200 if !is_primary() {
201 return Err("Error: cluster.fork() can only be called from the primary process".into());
202 }
203
204 let s = SETTINGS.with(|s| s.borrow().clone());
205 let exec = s
206 .exec
207 .clone()
208 .or_else(|| std::env::args().nth(1))
209 .unwrap_or_default();
210 if exec.is_empty() {
211 return Err(
212 "Error: cluster.fork() requires a main script (process.argv[1]); none was found".into(),
213 );
214 }
215 let fwd_args: Vec<String> = s
216 .args
217 .clone()
218 .unwrap_or_else(|| std::env::args().skip(2).collect());
219 let exe = std::env::current_exe().map_err(|e| format!("Error: cluster.fork(): {e}"))?;
220
221 let overrides = args.first().map(env_overrides).unwrap_or_default();
223
224 let id = NEXT_WORKER_ID.fetch_add(1, Ordering::SeqCst);
225
226 let mut cmd = Command::new(exe);
227 cmd.arg(&exec);
228 cmd.args(&fwd_args);
229 cmd.env("CLUSTER_WORKER", id.to_string());
230 cmd.env("NODE_UNIQUE_ID", id.to_string());
231 for (k, v) in overrides {
232 cmd.env(k, v);
233 }
234 if s.silent {
235 cmd.stdout(Stdio::null()).stderr(Stdio::null());
236 } else {
237 cmd.stdout(Stdio::inherit()).stderr(Stdio::inherit());
238 }
239
240 let child = cmd
241 .spawn()
242 .map_err(|e| format!("Error: cluster.fork(): {e}"))?;
243 let pid = child.id();
244
245 let worker = new_worker(id, pid);
247 WORKERS.with(|w| {
248 w.borrow_mut().insert(id, worker.clone());
249 });
250 with_host(|h| h.incr_handle());
251
252 let _ = emit_on(&cluster_emitter(), "fork", vec![worker.clone()]);
254
255 let io_online = with_host(|h| h.io_sender());
258 let _ = io_online.send(Box::new(move || dispatch_online(id)));
259
260 let io_exit: std::sync::mpsc::Sender<IoTask> = with_host(|h| h.io_sender());
263 std::thread::spawn(move || {
264 let mut child = child;
265 let code = child.wait().ok().and_then(|st| st.code()).unwrap_or(0);
266 let _ = io_exit.send(Box::new(move || dispatch_exit(id, code)));
267 });
268
269 Ok(worker)
270}
271
272fn env_overrides(v: &Value) -> Vec<(String, String)> {
275 with_host(|h| match h.get(v) {
276 Some(JsObj::Object(p)) => p
277 .iter()
278 .filter(|(k, _)| !k.starts_with("@@"))
279 .map(|(k, val)| (k.clone(), h.str_of(val)))
280 .collect(),
281 _ => Vec::new(),
282 })
283}
284
285fn setup_primary(args: &[Value]) -> Result<Value, String> {
290 if let Some(opts) = args.first() {
291 let exec = str_prop(opts, "exec");
292 let arr = arr_prop(opts, "args");
293 let ea = arr_prop(opts, "execArgv");
294 let silent = bool_prop(opts, "silent");
295 SETTINGS.with(|s| {
296 let mut s = s.borrow_mut();
297 if exec.is_some() {
298 s.exec = exec;
299 }
300 if arr.is_some() {
301 s.args = arr;
302 }
303 if ea.is_some() {
304 s.exec_argv = ea;
305 }
306 if let Some(b) = silent {
307 s.silent = b;
308 }
309 });
310 }
311 Ok(Value::Undef)
312}
313
314fn settings_object() -> Value {
317 let s = SETTINGS.with(|s| s.borrow().clone());
318 let exec = s
319 .exec
320 .clone()
321 .unwrap_or_else(|| std::env::args().nth(1).unwrap_or_default());
322 let args_vec = s
323 .args
324 .clone()
325 .unwrap_or_else(|| std::env::args().skip(2).collect());
326 let exec_argv = s.exec_argv.clone().unwrap_or_default();
327 with_host(|h| {
328 let arg_items: Vec<Value> = args_vec.into_iter().map(|a| h.new_str(a)).collect();
329 let args_arr = h.new_array(arg_items);
330 let ea_items: Vec<Value> = exec_argv.into_iter().map(|a| h.new_str(a)).collect();
331 let ea_arr = h.new_array(ea_items);
332 let exec_v = h.new_str(exec);
333 let mut m = IndexMap::new();
334 m.insert("exec".into(), exec_v);
335 m.insert("args".into(), args_arr);
336 m.insert("execArgv".into(), ea_arr);
337 m.insert("silent".into(), Value::Bool(s.silent));
338 h.new_object(m)
339 })
340}
341
342fn disconnect(args: &[Value]) -> Result<Value, String> {
349 let workers: Vec<Value> = WORKERS.with(|w| w.borrow().values().cloned().collect());
350 for wk in workers {
351 mark_disconnected(&wk);
352 }
353 if let Some(cb) = args.first() {
354 if with_host(|h| h.type_of(cb)) == "function" {
355 crate::host::invoke(cb, vec![], None)?;
356 }
357 }
358 Ok(Value::Undef)
359}
360
361fn new_worker(id: u64, pid: u32) -> Value {
366 let proc_obj = with_host(|h| {
367 let mut p = IndexMap::new();
368 p.insert("pid".into(), Value::Float(pid as f64));
369 p.insert("connected".into(), Value::Bool(true));
370 h.new_object(p)
371 });
372 let mut extra = IndexMap::new();
373 extra.insert("id".into(), Value::Float(id as f64));
374 extra.insert("process".into(), proc_obj);
375 extra.insert("@@cwid".into(), Value::Float(id as f64));
376 extra.insert("@@pid".into(), Value::Float(pid as f64));
377 extra.insert("@@connected".into(), Value::Bool(true));
378 super::net::new_emitter_object("ClusterWorker", extra)
379}
380
381fn self_worker() -> Value {
383 if let Some(v) = SELF_WORKER.with(|c| c.borrow().clone()) {
384 return v;
385 }
386 let id = worker_id_from_env().unwrap_or(0);
387 let w = new_worker(id, std::process::id());
388 SELF_WORKER.with(|c| *c.borrow_mut() = Some(w.clone()));
389 w
390}
391
392fn workers_object() -> Value {
394 let entries: Vec<(String, Value)> = WORKERS.with(|w| {
395 w.borrow()
396 .iter()
397 .map(|(id, wk)| (id.to_string(), wk.clone()))
398 .collect()
399 });
400 with_host(|h| {
401 let mut m = IndexMap::new();
402 for (k, v) in entries {
403 m.insert(k, v);
404 }
405 h.new_object(m)
406 })
407}
408
409pub fn instance_call(recv: &Value, method: &str, args: Vec<Value>) -> Result<Value, String> {
410 if EMITTER_METHODS.contains(&method) {
411 return super::events::instance_call(recv, method, args);
412 }
413 match method {
414 "send" => Ok(Value::Bool(false)),
417 "kill" | "destroy" => {
418 let sig = signal_number(args.first());
419 if let Some(pid) = pid_of(recv) {
420 unsafe {
422 libc::kill(pid as libc::pid_t, sig);
423 }
424 }
425 mark_disconnected(recv);
426 Ok(Value::Undef)
427 }
428 "disconnect" => {
431 mark_disconnected(recv);
432 Ok(recv.clone())
433 }
434 "isConnected" => Ok(Value::Bool(bool_prop(recv, "@@connected").unwrap_or(false))),
435 "isDead" => {
436 let id = pid_or_id(recv, "@@cwid");
437 let alive = id
438 .map(|i| WORKERS.with(|w| w.borrow().contains_key(&i)))
439 .unwrap_or(false);
440 Ok(Value::Bool(!alive))
441 }
442 _ => Err(crate::host::type_error(&format!(
443 "worker.{method} is not a function"
444 ))),
445 }
446}
447
448fn mark_disconnected(worker: &Value) {
451 with_host(|h| {
452 if let Some(JsObj::Object(p)) = h.get_mut(worker) {
453 p.insert("@@connected".into(), Value::Bool(false));
454 }
455 });
456 let proc = with_host(|h| match h.get(worker) {
457 Some(JsObj::Object(p)) => p.get("process").cloned(),
458 _ => None,
459 });
460 if let Some(proc) = proc {
461 with_host(|h| {
462 if let Some(JsObj::Object(p)) = h.get_mut(&proc) {
463 p.insert("connected".into(), Value::Bool(false));
464 }
465 });
466 }
467 let _ = emit_on(worker, "disconnect", vec![]);
468 let _ = emit_on(&cluster_emitter(), "disconnect", vec![worker.clone()]);
469}
470
471fn dispatch_online(id: u64) -> Result<(), String> {
475 let Some(worker) = WORKERS.with(|w| w.borrow().get(&id).cloned()) else {
476 return Ok(());
477 };
478 emit_on(&worker, "online", vec![])?;
479 emit_on(&cluster_emitter(), "online", vec![worker])
480}
481
482fn dispatch_exit(id: u64, code: i32) -> Result<(), String> {
485 let Some(worker) = WORKERS.with(|w| w.borrow().get(&id).cloned()) else {
486 return Ok(());
487 };
488 let null_sig = with_host(|h| h.null());
489 emit_on(
490 &worker,
491 "exit",
492 vec![Value::Float(code as f64), null_sig.clone()],
493 )?;
494 emit_on(
495 &cluster_emitter(),
496 "exit",
497 vec![worker, Value::Float(code as f64), null_sig],
498 )?;
499 WORKERS.with(|w| {
500 w.borrow_mut().remove(&id);
501 });
502 with_host(|h| h.decr_handle());
503 Ok(())
504}
505
506fn cluster_emitter() -> Value {
510 if let Some(v) = CLUSTER_EMITTER.with(|c| c.borrow().clone()) {
511 return v;
512 }
513 let e = super::events::new_emitter();
514 CLUSTER_EMITTER.with(|c| *c.borrow_mut() = Some(e.clone()));
515 e
516}
517
518fn emit_on(emitter: &Value, name: &str, mut args: Vec<Value>) -> Result<(), String> {
521 let mut a = vec![with_host(|h| h.new_str(name))];
522 a.append(&mut args);
523 super::events::instance_call(emitter, "emit", a).map(|_| ())
524}
525
526fn pid_of(worker: &Value) -> Option<u32> {
528 pid_or_id(worker, "@@pid").map(|n| n as u32)
529}
530
531fn pid_or_id(worker: &Value, key: &str) -> Option<u64> {
533 with_host(|h| match h.get(worker) {
534 Some(JsObj::Object(p)) => p.get(key).map(|v| h.to_number(v) as u64),
535 _ => None,
536 })
537}
538
539fn signal_number(arg: Option<&Value>) -> libc::c_int {
542 let Some(v) = arg else { return libc::SIGTERM };
543 let n = with_host(|h| h.to_number(v));
544 if n.is_finite() && n != 0.0 {
545 return n as libc::c_int;
546 }
547 match arg_str(std::slice::from_ref(v), 0).to_uppercase().as_str() {
548 "SIGKILL" => libc::SIGKILL,
549 "SIGINT" => libc::SIGINT,
550 "SIGHUP" => libc::SIGHUP,
551 "SIGQUIT" => libc::SIGQUIT,
552 "SIGUSR1" => libc::SIGUSR1,
553 "SIGUSR2" => libc::SIGUSR2,
554 _ => libc::SIGTERM,
555 }
556}
557
558fn str_prop(obj: &Value, key: &str) -> Option<String> {
560 with_host(|h| match h.get(obj) {
561 Some(JsObj::Object(p)) => p
562 .get(key)
563 .map(|v| h.str_of(v))
564 .filter(|s| !s.is_empty() && s != "undefined"),
565 _ => None,
566 })
567}
568
569fn bool_prop(obj: &Value, key: &str) -> Option<bool> {
571 with_host(|h| match h.get(obj) {
572 Some(JsObj::Object(p)) => p.get(key).map(|v| h.truthy(v)),
573 _ => None,
574 })
575}
576
577fn arr_prop(obj: &Value, key: &str) -> Option<Vec<String>> {
579 with_host(|h| match h.get(obj) {
580 Some(JsObj::Object(p)) => match p.get(key).and_then(|v| h.get(v)) {
581 Some(JsObj::Array(items)) => Some(items.iter().map(|v| h.str_of(v)).collect()),
582 _ => None,
583 },
584 _ => None,
585 })
586}