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] = super::events::METHODS;
93
94static NEXT_WORKER_ID: AtomicU64 = AtomicU64::new(1);
96
97#[derive(Default, Clone)]
100struct Settings {
101 exec: Option<String>,
103 args: Option<Vec<String>>,
105 exec_argv: Option<Vec<String>>,
108 silent: bool,
110}
111
112thread_local! {
113 static WORKERS: RefCell<HashMap<u64, Value>> = RefCell::new(HashMap::new());
116 static CLUSTER_EMITTER: RefCell<Option<Value>> = const { RefCell::new(None) };
120 static SELF_WORKER: RefCell<Option<Value>> = const { RefCell::new(None) };
122 static SETTINGS: RefCell<Settings> = RefCell::new(Settings::default());
124}
125
126fn worker_id_from_env() -> Option<u64> {
131 std::env::var("CLUSTER_WORKER")
132 .ok()
133 .or_else(|| std::env::var("NODE_UNIQUE_ID").ok())
134 .and_then(|s| s.trim().parse::<u64>().ok())
135}
136
137fn is_primary() -> bool {
139 worker_id_from_env().is_none()
140}
141
142pub fn call(method: &str, args: &[Value]) -> Option<Result<Value, String>> {
145 if EMITTER_METHODS.contains(&method) {
147 let em = cluster_emitter();
148 return Some(super::events::instance_call(&em, method, args.to_vec()));
149 }
150 Some(match method {
151 "fork" => fork(args),
152 "setupPrimary" | "setupMaster" => setup_primary(args),
153 "disconnect" => disconnect(args),
154 _ => return None,
155 })
156}
157
158pub fn constant(name: &str) -> Option<Value> {
160 Some(match name {
161 "isPrimary" | "isMaster" => Value::Bool(is_primary()),
162 "isWorker" => Value::Bool(!is_primary()),
163 "workers" => workers_object(),
164 "worker" => {
165 if is_primary() {
166 with_host(|h| h.null())
167 } else {
168 self_worker()
169 }
170 }
171 "settings" => settings_object(),
172 "SCHED_NONE" => Value::Float(1.0),
175 "SCHED_RR" => Value::Float(2.0),
176 "schedulingPolicy" => Value::Float(1.0),
177 _ => return None,
178 })
179}
180
181fn fork(args: &[Value]) -> Result<Value, String> {
186 if !is_primary() {
187 return Err("Error: cluster.fork() can only be called from the primary process".into());
188 }
189
190 let s = SETTINGS.with(|s| s.borrow().clone());
191 let exec = s
192 .exec
193 .clone()
194 .or_else(|| std::env::args().nth(1))
195 .unwrap_or_default();
196 if exec.is_empty() {
197 return Err(
198 "Error: cluster.fork() requires a main script (process.argv[1]); none was found".into(),
199 );
200 }
201 let fwd_args: Vec<String> = s
202 .args
203 .clone()
204 .unwrap_or_else(|| std::env::args().skip(2).collect());
205 let exe = std::env::current_exe().map_err(|e| format!("Error: cluster.fork(): {e}"))?;
206
207 let overrides = args.first().map(env_overrides).unwrap_or_default();
209
210 let id = NEXT_WORKER_ID.fetch_add(1, Ordering::SeqCst);
211
212 let mut cmd = Command::new(exe);
213 cmd.arg(&exec);
214 cmd.args(&fwd_args);
215 cmd.env("CLUSTER_WORKER", id.to_string());
216 cmd.env("NODE_UNIQUE_ID", id.to_string());
217 for (k, v) in overrides {
218 cmd.env(k, v);
219 }
220 if s.silent {
221 cmd.stdout(Stdio::null()).stderr(Stdio::null());
222 } else {
223 cmd.stdout(Stdio::inherit()).stderr(Stdio::inherit());
224 }
225
226 let child = cmd
227 .spawn()
228 .map_err(|e| format!("Error: cluster.fork(): {e}"))?;
229 let pid = child.id();
230
231 let worker = new_worker(id, pid);
233 WORKERS.with(|w| {
234 w.borrow_mut().insert(id, worker.clone());
235 });
236 with_host(|h| h.incr_handle());
237
238 let _ = emit_on(&cluster_emitter(), "fork", vec![worker.clone()]);
240
241 let io_online = with_host(|h| h.io_sender());
244 let _ = io_online.send(Box::new(move || dispatch_online(id)));
245
246 let io_exit: std::sync::mpsc::Sender<IoTask> = with_host(|h| h.io_sender());
249 std::thread::spawn(move || {
250 let mut child = child;
251 let code = child.wait().ok().and_then(|st| st.code()).unwrap_or(0);
252 let _ = io_exit.send(Box::new(move || dispatch_exit(id, code)));
253 });
254
255 Ok(worker)
256}
257
258fn env_overrides(v: &Value) -> Vec<(String, String)> {
261 with_host(|h| match h.get(v) {
262 Some(JsObj::Object(p)) => p
263 .iter()
264 .filter(|(k, _)| !k.starts_with("@@"))
265 .map(|(k, val)| (k.clone(), h.str_of(val)))
266 .collect(),
267 _ => Vec::new(),
268 })
269}
270
271fn setup_primary(args: &[Value]) -> Result<Value, String> {
276 if let Some(opts) = args.first() {
277 let exec = str_prop(opts, "exec");
278 let arr = arr_prop(opts, "args");
279 let ea = arr_prop(opts, "execArgv");
280 let silent = bool_prop(opts, "silent");
281 SETTINGS.with(|s| {
282 let mut s = s.borrow_mut();
283 if exec.is_some() {
284 s.exec = exec;
285 }
286 if arr.is_some() {
287 s.args = arr;
288 }
289 if ea.is_some() {
290 s.exec_argv = ea;
291 }
292 if let Some(b) = silent {
293 s.silent = b;
294 }
295 });
296 }
297 Ok(Value::Undef)
298}
299
300fn settings_object() -> Value {
303 let s = SETTINGS.with(|s| s.borrow().clone());
304 let exec = s
305 .exec
306 .clone()
307 .unwrap_or_else(|| std::env::args().nth(1).unwrap_or_default());
308 let args_vec = s
309 .args
310 .clone()
311 .unwrap_or_else(|| std::env::args().skip(2).collect());
312 let exec_argv = s.exec_argv.clone().unwrap_or_default();
313 with_host(|h| {
314 let arg_items: Vec<Value> = args_vec.into_iter().map(|a| h.new_str(a)).collect();
315 let args_arr = h.new_array(arg_items);
316 let ea_items: Vec<Value> = exec_argv.into_iter().map(|a| h.new_str(a)).collect();
317 let ea_arr = h.new_array(ea_items);
318 let exec_v = h.new_str(exec);
319 let mut m = IndexMap::new();
320 m.insert("exec".into(), exec_v);
321 m.insert("args".into(), args_arr);
322 m.insert("execArgv".into(), ea_arr);
323 m.insert("silent".into(), Value::Bool(s.silent));
324 h.new_object(m)
325 })
326}
327
328fn disconnect(args: &[Value]) -> Result<Value, String> {
335 let workers: Vec<Value> = WORKERS.with(|w| w.borrow().values().cloned().collect());
336 for wk in workers {
337 mark_disconnected(&wk);
338 }
339 if let Some(cb) = args.first() {
340 if with_host(|h| h.type_of(cb)) == "function" {
341 crate::host::invoke(cb, vec![], None)?;
342 }
343 }
344 Ok(Value::Undef)
345}
346
347fn new_worker(id: u64, pid: u32) -> Value {
352 let proc_obj = with_host(|h| {
353 let mut p = IndexMap::new();
354 p.insert("pid".into(), Value::Float(pid as f64));
355 p.insert("connected".into(), Value::Bool(true));
356 h.new_object(p)
357 });
358 let mut extra = IndexMap::new();
359 extra.insert("id".into(), Value::Float(id as f64));
360 extra.insert("process".into(), proc_obj);
361 extra.insert("@@cwid".into(), Value::Float(id as f64));
362 extra.insert("@@pid".into(), Value::Float(pid as f64));
363 extra.insert("@@connected".into(), Value::Bool(true));
364 super::net::new_emitter_object("ClusterWorker", extra)
365}
366
367fn self_worker() -> Value {
369 if let Some(v) = SELF_WORKER.with(|c| c.borrow().clone()) {
370 return v;
371 }
372 let id = worker_id_from_env().unwrap_or(0);
373 let w = new_worker(id, std::process::id());
374 SELF_WORKER.with(|c| *c.borrow_mut() = Some(w.clone()));
375 w
376}
377
378fn workers_object() -> Value {
380 let entries: Vec<(String, Value)> = WORKERS.with(|w| {
381 w.borrow()
382 .iter()
383 .map(|(id, wk)| (id.to_string(), wk.clone()))
384 .collect()
385 });
386 with_host(|h| {
387 let mut m = IndexMap::new();
388 for (k, v) in entries {
389 m.insert(k, v);
390 }
391 h.new_object(m)
392 })
393}
394
395pub fn instance_call(recv: &Value, method: &str, args: Vec<Value>) -> Result<Value, String> {
396 if EMITTER_METHODS.contains(&method) {
397 return super::events::instance_call(recv, method, args);
398 }
399 match method {
400 "send" => Ok(Value::Bool(false)),
403 "kill" | "destroy" => {
404 let sig = signal_number(args.first());
405 if let Some(pid) = pid_of(recv) {
406 unsafe {
408 libc::kill(pid as libc::pid_t, sig);
409 }
410 }
411 mark_disconnected(recv);
412 Ok(Value::Undef)
413 }
414 "disconnect" => {
417 mark_disconnected(recv);
418 Ok(recv.clone())
419 }
420 "isConnected" => Ok(Value::Bool(bool_prop(recv, "@@connected").unwrap_or(false))),
421 "isDead" => {
422 let id = pid_or_id(recv, "@@cwid");
423 let alive = id
424 .map(|i| WORKERS.with(|w| w.borrow().contains_key(&i)))
425 .unwrap_or(false);
426 Ok(Value::Bool(!alive))
427 }
428 _ => Err(crate::host::type_error(&format!(
429 "worker.{method} is not a function"
430 ))),
431 }
432}
433
434fn mark_disconnected(worker: &Value) {
437 with_host(|h| {
438 if let Some(JsObj::Object(p)) = h.get_mut(worker) {
439 p.insert("@@connected".into(), Value::Bool(false));
440 }
441 });
442 let proc = with_host(|h| match h.get(worker) {
443 Some(JsObj::Object(p)) => p.get("process").cloned(),
444 _ => None,
445 });
446 if let Some(proc) = proc {
447 with_host(|h| {
448 if let Some(JsObj::Object(p)) = h.get_mut(&proc) {
449 p.insert("connected".into(), Value::Bool(false));
450 }
451 });
452 }
453 let _ = emit_on(worker, "disconnect", vec![]);
454 let _ = emit_on(&cluster_emitter(), "disconnect", vec![worker.clone()]);
455}
456
457fn dispatch_online(id: u64) -> Result<(), String> {
461 let Some(worker) = WORKERS.with(|w| w.borrow().get(&id).cloned()) else {
462 return Ok(());
463 };
464 emit_on(&worker, "online", vec![])?;
465 emit_on(&cluster_emitter(), "online", vec![worker])
466}
467
468fn dispatch_exit(id: u64, code: i32) -> Result<(), String> {
471 let Some(worker) = WORKERS.with(|w| w.borrow().get(&id).cloned()) else {
472 return Ok(());
473 };
474 let null_sig = with_host(|h| h.null());
475 emit_on(
476 &worker,
477 "exit",
478 vec![Value::Float(code as f64), null_sig.clone()],
479 )?;
480 emit_on(
481 &cluster_emitter(),
482 "exit",
483 vec![worker, Value::Float(code as f64), null_sig],
484 )?;
485 WORKERS.with(|w| {
486 w.borrow_mut().remove(&id);
487 });
488 with_host(|h| h.decr_handle());
489 Ok(())
490}
491
492fn cluster_emitter() -> Value {
496 if let Some(v) = CLUSTER_EMITTER.with(|c| c.borrow().clone()) {
497 return v;
498 }
499 let e = super::events::new_emitter();
500 CLUSTER_EMITTER.with(|c| *c.borrow_mut() = Some(e.clone()));
501 e
502}
503
504fn emit_on(emitter: &Value, name: &str, mut args: Vec<Value>) -> Result<(), String> {
507 let mut a = vec![with_host(|h| h.new_str(name))];
508 a.append(&mut args);
509 super::events::instance_call(emitter, "emit", a).map(|_| ())
510}
511
512fn pid_of(worker: &Value) -> Option<u32> {
514 pid_or_id(worker, "@@pid").map(|n| n as u32)
515}
516
517fn pid_or_id(worker: &Value, key: &str) -> Option<u64> {
519 with_host(|h| match h.get(worker) {
520 Some(JsObj::Object(p)) => p.get(key).map(|v| h.to_number(v) as u64),
521 _ => None,
522 })
523}
524
525fn signal_number(arg: Option<&Value>) -> libc::c_int {
529 let Some(v) = arg else { return libc::SIGTERM };
530 let n = with_host(|h| h.to_number(v));
531 if n.is_finite() && n != 0.0 {
532 return n as libc::c_int;
533 }
534 super::process::signal_number(&arg_str(std::slice::from_ref(v), 0)).unwrap_or(libc::SIGTERM)
535}
536
537fn str_prop(obj: &Value, key: &str) -> Option<String> {
539 with_host(|h| match h.get(obj) {
540 Some(JsObj::Object(p)) => p
541 .get(key)
542 .map(|v| h.str_of(v))
543 .filter(|s| !s.is_empty() && s != "undefined"),
544 _ => None,
545 })
546}
547
548fn bool_prop(obj: &Value, key: &str) -> Option<bool> {
550 with_host(|h| match h.get(obj) {
551 Some(JsObj::Object(p)) => p.get(key).map(|v| h.truthy(v)),
552 _ => None,
553 })
554}
555
556fn arr_prop(obj: &Value, key: &str) -> Option<Vec<String>> {
558 with_host(|h| match h.get(obj) {
559 Some(JsObj::Object(p)) => match p.get(key).and_then(|v| h.get(v)) {
560 Some(JsObj::Array(items)) => Some(items.iter().map(|v| h.str_of(v)).collect()),
561 _ => None,
562 },
563 _ => None,
564 })
565}