Skip to main content

bun_runtime/
node_cluster.rs

1// @trace REQ-ENG-006 [api:node:cluster]
2//
3// Node.js cluster module. Bao supports cluster.fork() by spawning child processes
4// via child_process.spawn ("bao run <script>") with --cluster-worker env var.
5// Primary process: isPrimary=true, manages workers via fork().
6// Worker process: isWorker=true, communicates with primary via IPC (env-based).
7//
8// IPC: uses BAO_CLUSTER_WORKER_ID / BAO_CLUSTER_PRIMARY_PID env vars.
9// Workers communicate with primary via stdout/stderr pipe + process.send() over stdin.
10
11use ::std::cell::{Cell, RefCell};
12use ::std::time::Instant;
13
14use mozjs::jsapi::*;
15use mozjs::jsval::{
16    BooleanValue, Int32Value, JSVal, NullValue, ObjectValue, StringValue, UndefinedValue,
17};
18use mozjs::rooted;
19use mozjs::rust::wrappers2 as w2;
20
21use crate::require::cache_builtin;
22
23// ─── Cluster event pump (BCE: parent-loop stall eradication) ────────────────
24//
25// Root cause chain this replaces: the CLUSTER_JS shim drove BOTH IPC polls
26// (primary pollWorkers / worker recv) with `setInterval(..., 10)` timers that
27// are never cleared or unref'd. Because bao's timer registry has no unref
28// concept, those intervals pinned BOTH event loops forever:
29//   worker: script completes → 10ms IPC interval keeps the worker alive →
30//           worker never exits; primary: pollTimer keeps the primary alive
31//           while `cluster.workers` is non-empty → exit never observed →
32//           both processes spin until externally killed (the p2 full-script
33//           fork stall: >200s, zero stdout flush — stdout is block-buffered
34//           and neither process ever exits).
35//
36// Node semantics restored here: the worker's IPC channel NEVER keeps the
37// worker alive (a worker whose event loop drains exits; the primary's
38// ChildProcess handle keeps the PRIMARY alive while a worker runs). The pump
39// below is driven from `timers::drain_and_check` / `drain_one_pass` — the
40// same integration point as web_api::ws_pump_all — at a 10ms cadence, with
41// no JS timer anywhere:
42//   * Primary pump fn (pins=true): polls each worker's IPC + exit status,
43//     dispatches online/message/exit, returns `Object.keys(cluster.workers)
44//     .length > 0` → the loop-liveness contribution.
45//   * Worker pump fn (pins=false): polls the primary→worker channel; never
46//     contributes liveness (returns false; the registry ignores it anyway).
47//
48// The pump functions live in CLUSTER_JS (all event-dispatch logic stays in
49// the shim); Rust only registers, throttles, calls, and tracks liveness.
50
51struct ClusterPumpEntry {
52    /// GcStore key ("cluster-pump" namespace) of the JS pump function.
53    key: String,
54    /// true = a `true` return keeps the eval loop alive (primary); false =
55    /// never pins (worker — Node IPC-channel semantics).
56    pins: bool,
57    /// Last pump return value (liveness for pins entries). Starts true so a
58    /// pins pump registered mid-loop cannot race an exit decision.
59    last_alive: bool,
60}
61
62thread_local! {
63    static CLUSTER_PUMPS: RefCell<Vec<ClusterPumpEntry>> = const { RefCell::new(Vec::new()) };
64    static CLUSTER_PUMP_KEY: Cell<u64> = const { Cell::new(1) };
65    /// Throttle: the pump runs at most every 10ms (matches the old interval
66    /// cadence; drain_and_check ticks at ~1ms).
67    static CLUSTER_PUMP_LAST_TICK: Cell<Option<Instant>> = const { Cell::new(None) };
68}
69
70/// Native `__cluster_pump_register(fn, pins)` — CLUSTER_JS registers its pump
71/// function. `pins` distinguishes the primary (loop-keeping) pump from the
72/// worker (non-pinning) pump.
73#[allow(unsafe_op_in_unsafe_fn)]
74unsafe extern "C" fn cluster_pump_register(
75    cx: *mut JSContext,
76    argc: u32,
77    vp: *mut JSVal,
78) -> bool {
79    let args = CallArgs::from_vp(vp, argc);
80    if argc == 0 || !(*args.get(0).ptr).is_object() {
81        args.rval().set(BooleanValue(false));
82        return true;
83    }
84    let pins = if argc > 1 {
85        let v = *args.get(1).ptr;
86        v.is_boolean() && v.to_boolean()
87    } else {
88        false
89    };
90    let fn_obj = (*args.get(0).ptr).to_object();
91    let key = format!("p{}", CLUSTER_PUMP_KEY.with(|c| {
92        let v = c.get();
93        c.set(v + 1);
94        v
95    }));
96    crate::gc_store::gc_store_insert_ns(cx, "cluster-pump", &key, fn_obj);
97    CLUSTER_PUMPS.with(|p| {
98        p.borrow_mut().push(ClusterPumpEntry {
99            key,
100            pins,
101            last_alive: pins,
102        });
103    });
104    args.rval().set(BooleanValue(true));
105    true
106}
107
108/// Drive every registered cluster pump function on the JS thread. Called from
109/// `timers::drain_and_check` / `drain_one_pass`. Pump entries persist for the
110/// realm's lifetime (a primary pump that goes idle must still be present for
111/// the next fork); only their `last_alive` liveness contribution follows the
112/// return value.
113pub fn cluster_pump_all(raw_cx: *mut JSContext) {
114    let due = CLUSTER_PUMP_LAST_TICK.with(|t| match t.get() {
115        Some(last) => last.elapsed().as_millis() >= 10,
116        None => true,
117    });
118    if !due {
119        return;
120    }
121    CLUSTER_PUMP_LAST_TICK.with(|t| t.set(Some(Instant::now())));
122
123    // Snapshot keys to call (no borrow held across JS reentry — a pump fn may
124    // itself touch cluster state / re-register).
125    let snapshot: Vec<(String, bool)> = CLUSTER_PUMPS
126        .with(|p| p.borrow().iter().map(|e| (e.key.clone(), e.pins)).collect());
127    if snapshot.is_empty() {
128        return;
129    }
130
131    for (key, _pins) in snapshot {
132        let Some(pump_fn) = crate::gc_store::gc_store_get_ns(raw_cx, "cluster-pump", &key) else {
133            // Root vanished (realm teardown) — drop the entry.
134            CLUSTER_PUMPS.with(|p| p.borrow_mut().retain(|e| e.key != key));
135            continue;
136        };
137        let alive = unsafe { call_cluster_pump(raw_cx, pump_fn) };
138        CLUSTER_PUMPS.with(|p| {
139            let mut pumps = p.borrow_mut();
140            if let Some(entry) = pumps.iter_mut().find(|e| e.key == key) {
141                // Pumps persist across idle periods (a primary pump that
142                // reports no workers must still be alive for a later fork —
143                // removing it on `false` would strand all future online/exit
144                // events). Only `last_alive` (the liveness contribution)
145                // follows the return value.
146                entry.last_alive = alive;
147            }
148        });
149    }
150}
151
152/// Invoke one pump function; returns its boolean result (false on any JS
153/// error — a throwing pump must not pin the loop forever).
154///
155/// # Safety
156/// `raw_cx` must be the live JSContext on this thread; `pump_fn` a live
157/// function object rooted by GcStore.
158unsafe fn call_cluster_pump(raw_cx: *mut JSContext, pump_fn: *mut JSObject) -> bool {
159    let cx_ref = &mut mozjs::context::JSContext::from_ptr(
160        ::std::ptr::NonNull::new_unchecked(raw_cx),
161    );
162    let global = CurrentGlobalOrNull(raw_cx);
163    if global.is_null() {
164        return false;
165    }
166    rooted!(&in(cx_ref) let global_r = global);
167    rooted!(&in(cx_ref) let fval = ObjectValue(pump_fn));
168    let args = HandleValueArray {
169        length_: 0,
170        elements_: ::std::ptr::null(),
171    };
172    let mut rval = UndefinedValue();
173    let ok = JS_CallFunctionValue(
174        raw_cx,
175        global_r.handle().into(),
176        fval.handle().into(),
177        &args,
178        MutableHandle::<Value> {
179            _phantom_0: ::std::marker::PhantomData,
180            ptr: &mut rval,
181        },
182    );
183    if !ok {
184        JS_ClearPendingException(raw_cx);
185        return false;
186    }
187    rval.is_boolean() && rval.to_boolean()
188}
189
190/// Event-loop liveness contribution (wired into `timers::drain_and_check`'s
191/// return): a pins pump whose last run reported live workers keeps the loop
192/// alive. Worker pumps never contribute.
193pub fn cluster_loop_alive() -> bool {
194    CLUSTER_PUMPS.with(|p| p.borrow().iter().any(|e| e.pins && e.last_alive))
195}
196
197/// Pure worker-id predicate (no env access; testable without global state).
198///
199/// Strict form (BCE hardening for the "isPrimary occasionally flips false"
200/// class): fork() only ever issues ids 1, 2, 3… (see the `_nextId` counter in
201/// cluster_fork), so a well-formed worker env is a parseable integer ≥ 1.
202/// Anything else — var present but EMPTY (e.g. `BAO_CLUSTER_WORKER_ID= bao`),
203/// "0", or garbage — was never issued by our fork and must classify as
204/// primary. The previous `is_some()` predicate flipped primary→worker on any
205/// stray/empty env entry.
206fn is_worker_env(worker_id: Option<&str>) -> bool {
207    match worker_id {
208        Some(s) => s.parse::<u32>().map(|n| n >= 1).unwrap_or(false),
209        None => false,
210    }
211}
212
213/// Process-birth snapshot of the worker classification input (#64 root fix).
214///
215/// The freeze used to happen lazily at the FIRST `node_cluster::install` —
216/// which made the classification hostage to whatever wrote `std::env`
217/// between process birth and that first install:
218///
219///   * `process.env.X = v` in JS bridges to `std::env::set_var` (bun_api env
220///     setter), and multi-realm hosts (browser PagePool, embedder harnesses,
221///     cargo-test binaries) create realms lazily — user JS can run in an
222///     early realm (or a plain Rust `set_var` in a host) BEFORE the first
223///     realm that installs cluster, freezing a polluted value process-wide;
224///   * under parallel test execution the "which realm installs first" order
225///     is scheduler-dependent — the classic non-deterministic isPrimary
226///     flip-to-false.
227///
228/// The snapshot is now taken at PROCESS BIRTH by an `.init_array`
229/// constructor (Linux ELF: the dynamic linker runs it before `main`, before
230/// any realm, JS engine, or env bridge exists). This is the Node semantic
231/// made literal: worker-ness is a property of how the process was exec'd,
232/// never of later env writes.
233static EXEC_TIME_WORKER_ID: ::std::sync::OnceLock<Option<String>> = ::std::sync::OnceLock::new();
234
235/// Snapshot `BAO_CLUSTER_WORKER_ID` from the exec-time environment (pre-main).
236#[cfg(target_os = "linux")]
237extern "C" fn snapshot_exec_worker_id() {
238    let _ = EXEC_TIME_WORKER_ID.set(::std::env::var("BAO_CLUSTER_WORKER_ID").ok());
239}
240
241/// `.init_array` entry — the dynamic linker invokes the pointed-to function
242/// before `main` (the same mechanism glibc/libstd use for their own startup
243/// hooks; `environ` is already populated at this point).
244#[cfg(target_os = "linux")]
245#[used]
246#[unsafe(link_section = ".init_array")]
247static CAPTURE_EXEC_WORKER_ID: extern "C" fn() = snapshot_exec_worker_id;
248
249/// Check if this process is a cluster worker (started with --cluster-worker env).
250fn is_cluster_worker() -> bool {
251    // Process-birth snapshot (Linux ctor). The direct std::env read is only
252    // a non-Linux fallback where no pre-main hook exists — identical value
253    // in a fresh process; the lazy-realm race class only exists in
254    // long-lived multi-realm hosts, which are Linux (PagePool/browser).
255    let raw = EXEC_TIME_WORKER_ID
256        .get()
257        .cloned()
258        .unwrap_or_else(|| ::std::env::var("BAO_CLUSTER_WORKER_ID").ok());
259    is_worker_env(raw.as_deref())
260}
261
262// ─── Module install ────────────────────────────────────────────────────────
263
264pub fn install(cx: &mut mozjs::context::JSContext) {
265    rooted!(&in(cx) let obj = unsafe { w2::JS_NewPlainObject(cx) });
266    if obj.get().is_null() {
267        return;
268    }
269
270    let is_worker = is_cluster_worker();
271    let is_primary = !is_worker;
272
273    unsafe {
274        let raw_cx = cx.raw_cx();
275
276        // isPrimary
277        rooted!(&in(cx) let is_primary_val = BooleanValue(is_primary));
278        let _ = JS_DefineProperty(
279            raw_cx,
280            obj.handle().into(),
281            c"isPrimary".as_ptr(),
282            is_primary_val.handle().into(),
283            JSPROP_ENUMERATE as u32,
284        );
285
286        // isMaster (deprecated alias)
287        rooted!(&in(cx) let is_master_val = BooleanValue(is_primary));
288        let _ = JS_DefineProperty(
289            raw_cx,
290            obj.handle().into(),
291            c"isMaster".as_ptr(),
292            is_master_val.handle().into(),
293            JSPROP_ENUMERATE as u32,
294        );
295
296        // isWorker
297        rooted!(&in(cx) let is_worker_val = BooleanValue(is_worker));
298        let _ = JS_DefineProperty(
299            raw_cx,
300            obj.handle().into(),
301            c"isWorker".as_ptr(),
302            is_worker_val.handle().into(),
303            JSPROP_ENUMERATE as u32,
304        );
305
306        // workers = empty object
307        rooted!(&in(cx) let workers_obj = w2::JS_NewPlainObject(cx));
308        if !workers_obj.get().is_null() {
309            rooted!(&in(cx) let workers_val = ObjectValue(workers_obj.get()));
310            let _ = JS_DefineProperty(
311                raw_cx,
312                obj.handle().into(),
313                c"workers".as_ptr(),
314                workers_val.handle().into(),
315                JSPROP_ENUMERATE as u32,
316            );
317        }
318
319        // settings = empty object
320        rooted!(&in(cx) let settings_obj = w2::JS_NewPlainObject(cx));
321        if !settings_obj.get().is_null() {
322            rooted!(&in(cx) let settings_val = ObjectValue(settings_obj.get()));
323            let _ = JS_DefineProperty(
324                raw_cx,
325                obj.handle().into(),
326                c"settings".as_ptr(),
327                settings_val.handle().into(),
328                JSPROP_ENUMERATE as u32,
329            );
330        }
331
332        // worker — current worker object (if worker), or undefined (if primary)
333        if is_worker {
334            rooted!(&in(cx) let worker_obj = make_worker_object(cx, raw_cx));
335            if !worker_obj.get().is_null() {
336                rooted!(&in(cx) let worker_val = ObjectValue(worker_obj.get()));
337                let _ = JS_DefineProperty(
338                    raw_cx,
339                    obj.handle().into(),
340                    c"worker".as_ptr(),
341                    worker_val.handle().into(),
342                    JSPROP_ENUMERATE as u32,
343                );
344            }
345        } else {
346            rooted!(&in(cx) let worker_val = UndefinedValue());
347            let _ = JS_DefineProperty(
348                raw_cx,
349                obj.handle().into(),
350                c"worker".as_ptr(),
351                worker_val.handle().into(),
352                JSPROP_ENUMERATE as u32,
353            );
354        }
355
356        // fork() — spawns a worker process
357        let fork_fn = JS_NewFunction(raw_cx, Some(cluster_fork), 0, 0, c"fork".as_ptr());
358        if !fork_fn.is_null() {
359            let fn_obj = JS_GetFunctionObject(fork_fn);
360            if !fn_obj.is_null() {
361                rooted!(&in(cx) let val = ObjectValue(fn_obj));
362                let _ = JS_DefineProperty(
363                    raw_cx,
364                    obj.handle().into(),
365                    c"fork".as_ptr(),
366                    val.handle().into(),
367                    JSPROP_ENUMERATE as u32,
368                );
369            }
370        }
371
372        // disconnect()
373        let disconnect_fn = JS_NewFunction(
374            raw_cx,
375            Some(cluster_disconnect),
376            0,
377            0,
378            c"disconnect".as_ptr(),
379        );
380        if !disconnect_fn.is_null() {
381            let fn_obj = JS_GetFunctionObject(disconnect_fn);
382            if !fn_obj.is_null() {
383                rooted!(&in(cx) let val = ObjectValue(fn_obj));
384                let _ = JS_DefineProperty(
385                    raw_cx,
386                    obj.handle().into(),
387                    c"disconnect".as_ptr(),
388                    val.handle().into(),
389                    JSPROP_ENUMERATE as u32,
390                );
391            }
392        }
393
394        // setupPrimary() / setupMaster()
395        let setup_fn = JS_NewFunction(
396            raw_cx,
397            Some(cluster_setup_primary),
398            1,
399            0,
400            c"setupPrimary".as_ptr(),
401        );
402        if !setup_fn.is_null() {
403            let fn_obj = JS_GetFunctionObject(setup_fn);
404            if !fn_obj.is_null() {
405                rooted!(&in(cx) let val = ObjectValue(fn_obj));
406                let _ = JS_DefineProperty(
407                    raw_cx,
408                    obj.handle().into(),
409                    c"setupPrimary".as_ptr(),
410                    val.handle().into(),
411                    JSPROP_ENUMERATE as u32,
412                );
413            }
414        }
415        let setup_master_fn = JS_NewFunction(
416            raw_cx,
417            Some(cluster_setup_primary),
418            1,
419            0,
420            c"setupMaster".as_ptr(),
421        );
422        if !setup_master_fn.is_null() {
423            let fn_obj = JS_GetFunctionObject(setup_master_fn);
424            if !fn_obj.is_null() {
425                rooted!(&in(cx) let val = ObjectValue(fn_obj));
426                let _ = JS_DefineProperty(
427                    raw_cx,
428                    obj.handle().into(),
429                    c"setupMaster".as_ptr(),
430                    val.handle().into(),
431                    JSPROP_ENUMERATE as u32,
432                );
433            }
434        }
435
436        // schedulingPolicy = SCHED_RR (2) for round-robin connection distribution
437        rooted!(&in(cx) let sched = Int32Value(2));
438        let _ = JS_DefineProperty(
439            raw_cx,
440            obj.handle().into(),
441            c"schedulingPolicy".as_ptr(),
442            sched.handle().into(),
443            JSPROP_ENUMERATE as u32,
444        );
445
446        // SCHED_NONE = 1, SCHED_RR = 2
447        rooted!(&in(cx) let sched_none = Int32Value(1));
448        let _ = JS_DefineProperty(
449            raw_cx,
450            obj.handle().into(),
451            c"SCHED_NONE".as_ptr(),
452            sched_none.handle().into(),
453            JSPROP_ENUMERATE as u32,
454        );
455        rooted!(&in(cx) let sched_rr = Int32Value(2));
456        let _ = JS_DefineProperty(
457            raw_cx,
458            obj.handle().into(),
459            c"SCHED_RR".as_ptr(),
460            sched_rr.handle().into(),
461            JSPROP_ENUMERATE as u32,
462        );
463
464        // Worker-boot + kill natives (see cluster_worker_boot / _kill docs).
465        let boot_fn = JS_NewFunction(
466            raw_cx,
467            Some(cluster_worker_boot),
468            1,
469            0,
470            c"__cluster_worker_boot".as_ptr(),
471        );
472        if !boot_fn.is_null() {
473            let fn_obj = JS_GetFunctionObject(boot_fn);
474            if !fn_obj.is_null() {
475                rooted!(&in(cx) let val = ObjectValue(fn_obj));
476                let _ = JS_DefineProperty(
477                    raw_cx,
478                    obj.handle().into(),
479                    c"__cluster_worker_boot".as_ptr(),
480                    val.handle().into(),
481                    0,
482                );
483            }
484        }
485        let kill_fn = JS_NewFunction(
486            raw_cx,
487            Some(cluster_worker_kill),
488            2,
489            0,
490            c"__cluster_worker_kill".as_ptr(),
491        );
492        if !kill_fn.is_null() {
493            let fn_obj = JS_GetFunctionObject(kill_fn);
494            if !fn_obj.is_null() {
495                rooted!(&in(cx) let val = ObjectValue(fn_obj));
496                let _ = JS_DefineProperty(
497                    raw_cx,
498                    obj.handle().into(),
499                    c"__cluster_worker_kill".as_ptr(),
500                    val.handle().into(),
501                    0,
502                );
503            }
504        }
505        let ipc_send_fn = JS_NewFunction(
506            raw_cx,
507            Some(cluster_ipc_send),
508            2,
509            0,
510            c"__cluster_ipc_send".as_ptr(),
511        );
512        if !ipc_send_fn.is_null() {
513            let fn_obj = JS_GetFunctionObject(ipc_send_fn);
514            if !fn_obj.is_null() {
515                rooted!(&in(cx) let val = ObjectValue(fn_obj));
516                let _ = JS_DefineProperty(
517                    raw_cx,
518                    obj.handle().into(),
519                    c"__cluster_ipc_send".as_ptr(),
520                    val.handle().into(),
521                    0,
522                );
523            }
524        }
525        // Event-pump registration: the CLUSTER_JS shim registers its poll
526        // functions here (primary pins=true, worker pins=false) — driven by
527        // cluster_pump_all from the drain hook instead of loop-pinning
528        // setInterval timers (see the module-level BCE note).
529        let pump_register_fn = JS_NewFunction(
530            raw_cx,
531            Some(cluster_pump_register),
532            2,
533            0,
534            c"__cluster_pump_register".as_ptr(),
535        );
536        if !pump_register_fn.is_null() {
537            let fn_obj = JS_GetFunctionObject(pump_register_fn);
538            if !fn_obj.is_null() {
539                rooted!(&in(cx) let val = ObjectValue(fn_obj));
540                let _ = JS_DefineProperty(
541                    raw_cx,
542                    obj.handle().into(),
543                    c"__cluster_pump_register".as_ptr(),
544                    val.handle().into(),
545                    0,
546                );
547            }
548        }
549    }
550
551    cache_builtin(cx, "cluster", obj.get());
552
553    // Run the JS shim that sets up EventEmitter-based Worker class and process.send bridge.
554    unsafe {
555        let c_filename = bun_core::ZBox::from_bytes("node:cluster".as_bytes());
556        let opts = mozjs::glue::NewCompileOptions(cx.raw_cx(), c_filename.as_ptr(), 1);
557        if !opts.is_null() {
558            let mut src = mozjs::rust::transform_str_to_source_text(CLUSTER_JS);
559            let mut rval = UndefinedValue();
560            let rval_handle = MutableHandle::<Value> {
561                _phantom_0: ::std::marker::PhantomData,
562                ptr: &mut rval,
563            };
564            let _ = mozjs_sys::jsapi::JS::Evaluate2(cx.raw_cx(), opts, &mut src, rval_handle);
565            libc::free(opts as *mut _);
566        }
567    }
568}
569
570/// Build a JS Worker object representing a child worker process.
571unsafe fn make_worker_object(
572    cx: &mut mozjs::context::JSContext,
573    _raw_cx: *mut JSContext,
574) -> *mut JSObject {
575    unsafe {
576        let worker_obj = w2::JS_NewPlainObject(cx);
577        if worker_obj.is_null() {
578            return ::std::ptr::null_mut();
579        }
580        rooted!(&in(cx) let worker_r = worker_obj);
581        let worker_h = worker_r.handle().into();
582
583        // id — from env var
584        let worker_id: i32 = ::std::env::var("BAO_CLUSTER_WORKER_ID")
585            .ok()
586            .and_then(|s| s.parse().ok())
587            .unwrap_or(0);
588        rooted!(&in(cx) let id_val = Int32Value(worker_id));
589        JS_DefineProperty(
590            cx.raw_cx(),
591            worker_h,
592            c"id".as_ptr(),
593            id_val.handle().into(),
594            JSPROP_ENUMERATE as u32,
595        );
596
597        // process — null (would need to reference the actual ChildProcess, set from JS shim)
598        rooted!(&in(cx) let null_v = NullValue());
599        JS_DefineProperty(
600            cx.raw_cx(),
601            worker_h,
602            c"process".as_ptr(),
603            null_v.handle().into(),
604            JSPROP_ENUMERATE as u32,
605        );
606
607        // isConnected = true
608        rooted!(&in(cx) let connected_v = BooleanValue(true));
609        JS_DefineProperty(
610            cx.raw_cx(),
611            worker_h,
612            c"isConnected".as_ptr(),
613            connected_v.handle().into(),
614            JSPROP_ENUMERATE as u32,
615        );
616
617        // isDead = false
618        rooted!(&in(cx) let dead_v = BooleanValue(false));
619        JS_DefineProperty(
620            cx.raw_cx(),
621            worker_h,
622            c"isDead".as_ptr(),
623            dead_v.handle().into(),
624            JSPROP_ENUMERATE as u32,
625        );
626
627        // exitedAfterDisconnect = false
628        rooted!(&in(cx) let ead_v = BooleanValue(false));
629        JS_DefineProperty(
630            cx.raw_cx(),
631            worker_h,
632            c"exitedAfterDisconnect".as_ptr(),
633            ead_v.handle().into(),
634            JSPROP_ENUMERATE as u32,
635        );
636
637        // _events placeholder (for JS shim to enhance with EventEmitter)
638        rooted!(&in(cx) let events_obj = w2::JS_NewPlainObject(cx));
639        if !events_obj.get().is_null() {
640            rooted!(&in(cx) let events_val = ObjectValue(events_obj.get()));
641            JS_DefineProperty(
642                cx.raw_cx(),
643                worker_h,
644                c"_events".as_ptr(),
645                events_val.handle().into(),
646                0,
647            );
648        }
649
650        worker_r.get()
651    }
652}
653
654/// cluster.fork(env?) — spawn a worker process asynchronously.
655///
656/// BCE (v-surface P0-4) root causes fixed here:
657///   1. envp entries were built WITHOUT NUL terminators — execve requires
658///      NUL-terminated C strings, so the child exec'd with garbage env and
659///      never ran its worker branch. spawn_cluster_worker now appends the
660///      NULs (CString).
661///   2. bun_spawn::sync::spawn BLOCKS until the child exits — fork() could
662///      never deliver online/exit/message events. Now the async
663///      spawn_process path (same as child_process.spawn) with exit tracking
664///      via CP_ASYNC_STATES + a poll thread.
665///   3. fork(env) — the env object argument was parsed nowhere; now merged
666///      into the child env (Node semantics: fork env overrides matching
667///      keys, the rest is inherited).
668///   4. The IPC contract exists for real now: the child gets the IPC socket
669///      at fd 3 (PosixStdio::Ipc) + BAO_CLUSTER_IPC_FD=3, and the worker boot
670///      path (__cluster_worker_boot) wraps it into CP_IPC_CHANNELS keyed by
671///      the worker's own pid, powering process.send / process.on('message').
672///
673/// The JS shim (CLUSTER_JS) wraps the returned object in a Worker with
674/// EventEmitter methods and pumps online/message/exit events.
675#[allow(unsafe_op_in_unsafe_fn)]
676unsafe extern "C" fn cluster_fork(
677    cx: *mut JSContext,
678    argc: u32,
679    vp: *mut mozjs::jsval::JSVal,
680) -> bool {
681    let args = CallArgs::from_vp(vp, argc);
682
683    if let ::std::result::Result::Err(e) = crate::permission_bridge::check_run() {
684        let c_msg = bun_core::ZBox::from_bytes(e.as_bytes());
685        JS_ReportErrorUTF8(cx, c"%s".as_ptr(), c_msg.as_ptr());
686        return false;
687    }
688
689    // Get the script path — use process.argv[1] (the script being run).
690    let mut wrapped_cx =
691        mozjs::context::JSContext::from_ptr(::std::ptr::NonNull::new_unchecked(cx));
692    let cx_ref = &mut wrapped_cx;
693
694    let script_path = {
695        rooted!(&in(cx_ref) let global = CurrentGlobalOrNull(cx));
696        let mut process_val = UndefinedValue();
697        JS_GetProperty(
698            cx,
699            global.handle().into(),
700            c"process".as_ptr(),
701            MutableHandle::<Value> {
702                _phantom_0: ::std::marker::PhantomData,
703                ptr: &mut process_val,
704            },
705        );
706        if process_val.is_object() {
707            let process_obj = process_val.to_object();
708            rooted!(&in(cx_ref) let process_r = process_obj);
709            let mut argv_val = UndefinedValue();
710            JS_GetProperty(
711                cx,
712                process_r.handle().into(),
713                c"argv".as_ptr(),
714                MutableHandle::<Value> {
715                    _phantom_0: ::std::marker::PhantomData,
716                    ptr: &mut argv_val,
717                },
718            );
719            if argv_val.is_object() {
720                let argv_obj = argv_val.to_object();
721                rooted!(&in(cx_ref) let argv_r = argv_obj);
722                // bao's process.argv = [exec, "run", <script>] when invoked via
723                // the `run` subcommand (Node puts the script at argv[1]; bao
724                // keeps the subcommand). The worker must re-run the SCRIPT.
725                let mut first = UndefinedValue();
726                JS_GetElement(
727                    cx,
728                    argv_r.handle().into(),
729                    1,
730                    MutableHandle::<Value> {
731                        _phantom_0: ::std::marker::PhantomData,
732                        ptr: &mut first,
733                    },
734                );
735                let mut second = UndefinedValue();
736                JS_GetElement(
737                    cx,
738                    argv_r.handle().into(),
739                    2,
740                    MutableHandle::<Value> {
741                        _phantom_0: ::std::marker::PhantomData,
742                        ptr: &mut second,
743                    },
744                );
745                if first.is_string()
746                    && crate::js_to_rust_string(cx, first) == "run"
747                    && second.is_string()
748                {
749                    crate::js_to_rust_string(cx, second)
750                } else if first.is_string() {
751                    crate::js_to_rust_string(cx, first)
752                } else {
753                    String::new()
754                }
755            } else {
756                String::new()
757            }
758        } else {
759            String::new()
760        }
761    };
762
763    if script_path.is_empty() {
764        JS_ReportErrorUTF8(
765            cx,
766            c"cluster.fork(): cannot determine script path (process.argv[1] is empty)".as_ptr(),
767        );
768        args.rval().set(UndefinedValue());
769        return false;
770    }
771
772    // Determine the next worker ID from cluster.settings._nextId.
773    let worker_id: i32 = {
774        if let Some(cluster_mod) = crate::require::get_builtin(cx_ref.raw_cx(), "cluster") {
775            if !cluster_mod.is_null() {
776                rooted!(&in(cx_ref) let cm_r = cluster_mod);
777                let mut settings_val = UndefinedValue();
778                JS_GetProperty(
779                    cx,
780                    cm_r.handle().into(),
781                    c"settings".as_ptr(),
782                    MutableHandle::<Value> {
783                        _phantom_0: ::std::marker::PhantomData,
784                        ptr: &mut settings_val,
785                    },
786                );
787                if settings_val.is_object() {
788                    let settings_obj = settings_val.to_object();
789                    rooted!(&in(cx_ref) let settings_r = settings_obj);
790                    let mut next_id_val = UndefinedValue();
791                    JS_GetProperty(
792                        cx,
793                        settings_r.handle().into(),
794                        c"_nextId".as_ptr(),
795                        MutableHandle::<Value> {
796                            _phantom_0: ::std::marker::PhantomData,
797                            ptr: &mut next_id_val,
798                        },
799                    );
800                    if next_id_val.is_int32() {
801                        let id = next_id_val.to_int32();
802                        let new_id = id + 1;
803                        rooted!(&in(cx_ref) let new_id_v = Int32Value(new_id));
804                        JS_SetProperty(
805                            cx,
806                            settings_r.handle().into(),
807                            c"_nextId".as_ptr(),
808                            new_id_v.handle().into(),
809                        );
810                        id
811                    } else {
812                        rooted!(&in(cx_ref) let init_v = Int32Value(2));
813                        JS_SetProperty(
814                            cx,
815                            settings_r.handle().into(),
816                            c"_nextId".as_ptr(),
817                            init_v.handle().into(),
818                        );
819                        1
820                    }
821                } else {
822                    1
823                }
824            } else {
825                1
826            }
827        } else {
828            1
829        }
830    };
831
832    // Resolve the bao binary: explicit override first (tests run under a
833    // cargo harness whose current_exe is the test binary, not bao), then
834    // current_exe().
835    let exec_str = ::std::env::var("BAO_CLUSTER_EXEC").unwrap_or_else(|_| {
836        ::std::env::current_exe()
837            .unwrap_or_else(|_| ::std::path::PathBuf::from("bao"))
838            .to_string_lossy()
839            .into_owned()
840    });
841
842    // Child environment: inherit current env, then merge the fork(env) object
843    // argument (if any) and the cluster control vars.
844    let primary_pid = ::std::process::id();
845    let mut env_map: ::std::collections::BTreeMap<String, String> =
846        ::std::env::vars().collect();
847    if argc > 0 && (*args.get(0).ptr).is_object() {
848        let env_obj = (*args.get(0).ptr).to_object();
849        rooted!(&in(cx_ref) let env_r = env_obj);
850        let mut ids = mozjs::rust::IdVector::new(cx);
851        if GetPropertyKeys(cx, env_r.handle().into(), JSITER_OWNONLY, ids.handle_mut()) {
852            for jsid in &*ids {
853                if !jsid.is_string() {
854                    continue;
855                }
856                let key_ptr = jsid.to_string();
857                let key = mozjs::conversions::unsafe_jsstr_to_string(
858                    cx,
859                    ::std::ptr::NonNull::new_unchecked(key_ptr),
860                );
861                let c_key = bun_core::ZBox::from_bytes(key.as_bytes());
862                let mut v_val = UndefinedValue();
863                JS_GetProperty(
864                    cx,
865                    env_r.handle().into(),
866                    c_key.as_ptr(),
867                    MutableHandle::<Value> {
868                        _phantom_0: ::std::marker::PhantomData,
869                        ptr: &mut v_val,
870                    },
871                );
872                let val = if v_val.is_string() {
873                    crate::js_to_rust_string(cx, v_val)
874                } else if v_val.is_int32() {
875                    v_val.to_int32().to_string()
876                } else if v_val.is_boolean() {
877                    if v_val.to_boolean() {
878                        "true".to_string()
879                    } else {
880                        "false".to_string()
881                    }
882                } else {
883                    continue;
884                };
885                env_map.insert(key, val);
886            }
887        }
888    }
889    env_map.insert("BAO_CLUSTER_WORKER_ID".to_string(), worker_id.to_string());
890    env_map.insert(
891        "BAO_CLUSTER_PRIMARY_PID".to_string(),
892        primary_pid.to_string(),
893    );
894    env_map.insert("BAO_CLUSTER_IPC_FD".to_string(), "3".to_string());
895    let env_entries: Vec<Box<[u8]>> = env_map
896        .into_iter()
897        .map(|(k, v)| format!("{}={}", k, v).into_bytes().into_boxed_slice())
898        .collect();
899
900    // Build argv for the child: bao run <script>
901    let argv: Vec<Box<[u8]>> = vec![
902        exec_str.as_bytes().to_vec().into_boxed_slice(),
903        b"run".to_vec().into_boxed_slice(),
904        script_path.as_bytes().to_vec().into_boxed_slice(),
905    ];
906
907    // Async spawn with fd-3 IPC + exit tracking (see spawn_cluster_worker).
908    let pid = match super::node_child_process::spawn_cluster_worker(argv, env_entries) {
909        Ok(p) => p,
910        Err(msg) => {
911            let c_msg = bun_core::ZBox::from_bytes(msg.as_bytes());
912            JS_ReportErrorUTF8(cx, c"%s".as_ptr(), c_msg.as_ptr());
913            return false;
914        }
915    };
916
917    // Build a Worker JS object.
918    let worker_obj = mozjs_sys::jsapi::JS_NewPlainObject(cx);
919    if worker_obj.is_null() {
920        args.rval().set(UndefinedValue());
921        return true;
922    }
923    rooted!(&in(cx_ref) let worker_r = worker_obj);
924    let worker_h = worker_r.handle().into();
925
926    // id
927    rooted!(&in(cx_ref) let id_v = Int32Value(worker_id));
928    JS_DefineProperty(
929        cx,
930        worker_h,
931        c"id".as_ptr(),
932        id_v.handle().into(),
933        JSPROP_ENUMERATE as u32,
934    );
935
936    // process — minimal ChildProcess-shaped object; exitCode is filled in by
937    // the JS shim's exit poll once the worker exits.
938    let proc_obj = w2::JS_NewPlainObject(cx_ref);
939    if !proc_obj.is_null() {
940        rooted!(&in(cx_ref) let proc_r = proc_obj);
941        let proc_h = proc_r.handle().into();
942
943        rooted!(&in(cx_ref) let pid_v = Int32Value(pid));
944        JS_DefineProperty(
945            cx,
946            proc_h,
947            c"pid".as_ptr(),
948            pid_v.handle().into(),
949            JSPROP_ENUMERATE as u32,
950        );
951
952        rooted!(&in(cx_ref) let ec_v = NullValue());
953        JS_DefineProperty(
954            cx,
955            proc_h,
956            c"exitCode".as_ptr(),
957            ec_v.handle().into(),
958            JSPROP_ENUMERATE as u32,
959        );
960
961        let proc_val = ObjectValue(proc_r.get());
962        rooted!(&in(cx_ref) let pv = proc_val);
963        JS_DefineProperty(
964            cx,
965            worker_h,
966            c"process".as_ptr(),
967            pv.handle().into(),
968            JSPROP_ENUMERATE as u32,
969        );
970    }
971
972    // isConnected
973    rooted!(&in(cx_ref) let conn_v = BooleanValue(true));
974    JS_DefineProperty(
975        cx,
976        worker_h,
977        c"isConnected".as_ptr(),
978        conn_v.handle().into(),
979        JSPROP_ENUMERATE as u32,
980    );
981
982    // isDead — false at spawn; the async child has not exited yet. The JS
983    // shim flips it on the 'exit' event.
984    rooted!(&in(cx_ref) let dead_v = BooleanValue(false));
985    JS_DefineProperty(
986        cx,
987        worker_h,
988        c"isDead".as_ptr(),
989        dead_v.handle().into(),
990        JSPROP_ENUMERATE as u32,
991    );
992
993    // exitedAfterDisconnect
994    rooted!(&in(cx_ref) let ead_v = BooleanValue(false));
995    JS_DefineProperty(
996        cx,
997        worker_h,
998        c"exitedAfterDisconnect".as_ptr(),
999        ead_v.handle().into(),
1000        JSPROP_ENUMERATE as u32,
1001    );
1002
1003    // _pid (for native send/kill)
1004    rooted!(&in(cx_ref) let npid_v = Int32Value(pid));
1005    JS_DefineProperty(cx, worker_h, c"_pid".as_ptr(), npid_v.handle().into(), 0);
1006
1007    // ─── Mount `send(msg[, sendHandle])` on the worker ─────────────────────
1008    //
1009    // Delegates to the parent IPC channel registered in CP_IPC_CHANNELS[pid]
1010    // by spawn_cluster_worker. Same wire format as child.send in
1011    // node_child_process (newline-delimited JSON, optional SCM_RIGHTS fd).
1012    w2::JS_DefineFunction(
1013        cx_ref,
1014        worker_r.handle(),
1015        c"send".as_ptr(),
1016        Some(cluster_worker_send),
1017        2,
1018        JSPROP_ENUMERATE as u32,
1019    );
1020    // `disconnect()` — close the IPC channel and remove from registry.
1021    w2::JS_DefineFunction(
1022        cx_ref,
1023        worker_r.handle(),
1024        c"disconnect".as_ptr(),
1025        Some(cluster_worker_disconnect),
1026        0,
1027        JSPROP_ENUMERATE as u32,
1028    );
1029    // `_ipcFd` — child-side fd number (Node fd-3 IPC convention).
1030    rooted!(&in(cx_ref) let ipcfd_v = Int32Value(3));
1031    JS_DefineProperty(cx, worker_h, c"_ipcFd".as_ptr(), ipcfd_v.handle().into(), 0);
1032
1033    // Register worker in cluster.workers
1034    {
1035        if let Some(cluster_mod) = crate::require::get_builtin(cx_ref.raw_cx(), "cluster") {
1036            if !cluster_mod.is_null() {
1037                rooted!(&in(cx_ref) let cm_r = cluster_mod);
1038                let mut workers_val = UndefinedValue();
1039                JS_GetProperty(
1040                    cx,
1041                    cm_r.handle().into(),
1042                    c"workers".as_ptr(),
1043                    MutableHandle::<Value> {
1044                        _phantom_0: ::std::marker::PhantomData,
1045                        ptr: &mut workers_val,
1046                    },
1047                );
1048                if workers_val.is_object() {
1049                    let workers_obj = workers_val.to_object();
1050                    rooted!(&in(cx_ref) let workers_r = workers_obj);
1051                    let worker_val = ObjectValue(worker_r.get());
1052                    rooted!(&in(cx_ref) let wv = worker_val);
1053                    let id_c_str = bun_core::ZBox::from_bytes(format!("{}", worker_id).as_bytes());
1054                    JS_SetProperty(
1055                        cx,
1056                        workers_r.handle().into(),
1057                        id_c_str.as_ptr(),
1058                        wv.handle().into(),
1059                    );
1060                }
1061            }
1062        }
1063    }
1064
1065    args.rval().set(ObjectValue(worker_r.get()));
1066    true
1067}
1068
1069/// cluster.disconnect() — disconnect all workers.
1070#[allow(unsafe_op_in_unsafe_fn)]
1071unsafe extern "C" fn cluster_disconnect(
1072    cx: *mut JSContext,
1073    _argc: u32,
1074    vp: *mut mozjs::jsval::JSVal,
1075) -> bool {
1076    let args = CallArgs::from_vp(vp, _argc);
1077
1078    // Send SIGTERM to all worker processes tracked in cluster.workers.
1079    let mut wrapped_cx =
1080        mozjs::context::JSContext::from_ptr(::std::ptr::NonNull::new_unchecked(cx));
1081    let cx_ref = &mut wrapped_cx;
1082
1083    if let Some(cluster_mod) = crate::require::get_builtin(cx_ref.raw_cx(), "cluster") {
1084        if !cluster_mod.is_null() {
1085            rooted!(&in(cx_ref) let cm_r = cluster_mod);
1086            let mut workers_val = UndefinedValue();
1087            JS_GetProperty(
1088                cx,
1089                cm_r.handle().into(),
1090                c"workers".as_ptr(),
1091                MutableHandle::<Value> {
1092                    _phantom_0: ::std::marker::PhantomData,
1093                    ptr: &mut workers_val,
1094                },
1095            );
1096            if workers_val.is_object() {
1097                let workers_obj = workers_val.to_object();
1098                rooted!(&in(cx_ref) let workers_r = workers_obj);
1099                // Iterate over workers and kill each one.
1100                // Since we can't easily enumerate JS objects from Rust,
1101                // we use the JS shim to handle disconnect logic.
1102                // For now, just set a flag that the JS shim will pick up.
1103                let disconnected_v = BooleanValue(true);
1104                rooted!(&in(cx_ref) let dv = disconnected_v);
1105                JS_SetProperty(
1106                    cx,
1107                    cm_r.handle().into(),
1108                    c"_disconnecting".as_ptr(),
1109                    dv.handle().into(),
1110                );
1111            }
1112        }
1113    }
1114
1115    args.rval().set(UndefinedValue());
1116    true
1117}
1118
1119/// cluster.setupPrimary(settings) / cluster.setupMaster(settings) — configure primary.
1120#[allow(unsafe_op_in_unsafe_fn)]
1121unsafe extern "C" fn cluster_setup_primary(
1122    cx: *mut JSContext,
1123    argc: u32,
1124    vp: *mut mozjs::jsval::JSVal,
1125) -> bool {
1126    let args = CallArgs::from_vp(vp, argc);
1127
1128    let mut wrapped_cx =
1129        mozjs::context::JSContext::from_ptr(::std::ptr::NonNull::new_unchecked(cx));
1130    let cx_ref = &mut wrapped_cx;
1131
1132    // Store settings on cluster.settings.
1133    if argc > 0 {
1134        let settings_val = *args.get(0).ptr;
1135        if settings_val.is_object() {
1136            if let Some(cluster_mod) = crate::require::get_builtin(cx_ref.raw_cx(), "cluster") {
1137                if !cluster_mod.is_null() {
1138                    rooted!(&in(cx_ref) let cm_r = cluster_mod);
1139                    rooted!(&in(cx_ref) let sv = settings_val);
1140                    JS_SetProperty(
1141                        cx,
1142                        cm_r.handle().into(),
1143                        c"settings".as_ptr(),
1144                        sv.handle().into(),
1145                    );
1146                }
1147            }
1148        }
1149    }
1150
1151    args.rval().set(UndefinedValue());
1152    true
1153}
1154
1155// ─── Native: worker.send(msg[, sendHandle]) ────────────────────────────────
1156//
1157// Send a JSON message on the cluster worker's IPC channel. If a numeric fd is
1158// passed as the second argument, the message is sent via SCM_RIGHTS ancillary
1159// data (fd handoff — used by master round-robin server handle passing).
1160//
1161// The worker's IPC channel is keyed by pid in CP_IPC_CHANNELS (populated by
1162// cluster_fork). Args from JS:
1163//   args[0] = msg   (string — caller already JSON.stringify'd)
1164//   args[1] = fd    (optional i32 — if present, use SCM_RIGHTS path)
1165
1166#[allow(unsafe_op_in_unsafe_fn)]
1167unsafe extern "C" fn cluster_worker_send(
1168    cx: *mut JSContext,
1169    argc: u32,
1170    vp: *mut JSVal,
1171) -> bool {
1172    let args = CallArgs::from_vp(vp, argc);
1173
1174    // The `this` value is the Worker JS object — read its `_pid`.
1175    let this_v = *args.thisv().ptr;
1176    let this_obj = if this_v.is_object() {
1177        this_v.to_object()
1178    } else {
1179        ::std::ptr::null_mut::<JSObject>()
1180    };
1181
1182    let mut wrapped_cx =
1183        mozjs::context::JSContext::from_ptr(::std::ptr::NonNull::new_unchecked(cx));
1184    let cx_ref = &mut wrapped_cx;
1185
1186    rooted!(&in(cx_ref) let this_r = this_obj);
1187    let mut pid_v = UndefinedValue();
1188    JS_GetProperty(
1189        cx,
1190        this_r.handle().into(),
1191        c"_pid".as_ptr(),
1192        MutableHandle::<Value> {
1193            _phantom_0: ::std::marker::PhantomData,
1194            ptr: &mut pid_v,
1195        },
1196    );
1197    let pid = if pid_v.is_int32() {
1198        pid_v.to_int32()
1199    } else {
1200        0
1201    };
1202    if pid == 0 {
1203        args.rval().set(BooleanValue(false));
1204        return true;
1205    }
1206
1207    let json_str = if argc > 0 {
1208        let v = *args.get(0).ptr;
1209        if v.is_string() {
1210            crate::js_to_rust_string(cx, v)
1211        } else {
1212            String::new()
1213        }
1214    } else {
1215        String::new()
1216    };
1217    let fd_opt: Option<i32> = if argc > 1 {
1218        let v = *args.get(1).ptr;
1219        if v.is_int32() {
1220            let n = v.to_int32();
1221            if n >= 0 {
1222                Some(n)
1223            } else {
1224                None
1225            }
1226        } else {
1227            None
1228        }
1229    } else {
1230        None
1231    };
1232
1233    // Look up channel by pid, send under short-lived lock. No `?` operator
1234    // since we are in an `extern "C" fn` returning bool — chain with
1235    // `.map_err().and_then()` instead.
1236    let outcome: ::std::result::Result<(), String> =
1237        super::node_child_process::CP_IPC_CHANNELS
1238            .lock()
1239            .map_err(|e| format!("registry lock poisoned: {}", e))
1240            .and_then(|registry| {
1241                registry
1242                    .get(&pid)
1243                    .cloned()
1244                    .ok_or_else(|| format!("no ipc channel for worker pid {}", pid))
1245                    .and_then(|chan_mtx| {
1246                        chan_mtx
1247                            .lock()
1248                            .map_err(|e| format!("channel lock poisoned: {}", e))
1249                            .and_then(|mut chan| {
1250                                if let Some(fd) = fd_opt {
1251                                    chan.send_handle(&json_str, fd)
1252                                        .map_err(|e| format!("send_handle: {}", e))
1253                                } else {
1254                                    chan.send_json(&json_str)
1255                                        .map_err(|e| format!("send_json: {}", e))
1256                                }
1257                            })
1258                    })
1259            });
1260
1261    match outcome {
1262        Ok(()) => {
1263            args.rval().set(BooleanValue(true));
1264            true
1265        }
1266        Err(msg) => {
1267            let c_msg = bun_core::ZBox::from_bytes(msg.as_bytes());
1268            JS_ReportErrorUTF8(cx, c"%s".as_ptr(), c_msg.as_ptr());
1269            args.rval().set(BooleanValue(false));
1270            false
1271        }
1272    }
1273}
1274
1275// ─── Native: worker.disconnect() ───────────────────────────────────────────
1276//
1277// Close the IPC channel from the primary side. Removes the channel from
1278// CP_IPC_CHANNELS so subsequent send/recv calls return errors cleanly.
1279
1280#[allow(unsafe_op_in_unsafe_fn)]
1281unsafe extern "C" fn cluster_worker_disconnect(
1282    cx: *mut JSContext,
1283    _argc: u32,
1284    vp: *mut JSVal,
1285) -> bool {
1286    let args = CallArgs::from_vp(vp, _argc);
1287
1288    // Read pid from `this`.
1289    let this_v = *args.thisv().ptr;
1290    let this_obj = if this_v.is_object() {
1291        this_v.to_object()
1292    } else {
1293        ::std::ptr::null_mut::<JSObject>()
1294    };
1295
1296    let mut wrapped_cx =
1297        mozjs::context::JSContext::from_ptr(::std::ptr::NonNull::new_unchecked(cx));
1298    let cx_ref = &mut wrapped_cx;
1299    rooted!(&in(cx_ref) let this_r = this_obj);
1300    let mut pid_v = UndefinedValue();
1301    JS_GetProperty(
1302        cx,
1303        this_r.handle().into(),
1304        c"_pid".as_ptr(),
1305        MutableHandle::<Value> {
1306            _phantom_0: ::std::marker::PhantomData,
1307            ptr: &mut pid_v,
1308        },
1309    );
1310    let pid = if pid_v.is_int32() {
1311        pid_v.to_int32()
1312    } else {
1313        0
1314    };
1315    if pid != 0 {
1316        if let Ok(mut registry) = super::node_child_process::CP_IPC_CHANNELS.lock() {
1317            registry.remove(&pid);
1318        }
1319    }
1320    args.rval().set(UndefinedValue());
1321    true
1322}
1323
1324// ─── Native: __cluster_worker_boot(fd) — worker-side IPC registration ──────
1325//
1326// Runs INSIDE the worker process (called from CLUSTER_JS on boot when
1327// BAO_CLUSTER_WORKER_ID is set). Wraps the inherited fd-3 IPC socket (the
1328// other end of the primary's CP_IPC_CHANNELS[worker_pid] channel) into an
1329// IpcChannel registered under the worker's OWN pid, so child_process's
1330// __cp_ipc_send / __cp_ipc_recv reach it — powering process.send() and
1331// process.on('message') on the worker side.
1332
1333#[allow(unsafe_op_in_unsafe_fn)]
1334unsafe extern "C" fn cluster_worker_boot(_cx: *mut JSContext, argc: u32, vp: *mut JSVal) -> bool {
1335    let args = CallArgs::from_vp(vp, argc);
1336    let fd = if argc > 0 && (*args.get(0).ptr).is_int32() {
1337        (*args.get(0).ptr).to_int32()
1338    } else {
1339        3
1340    };
1341    if fd < 0 {
1342        args.rval().set(BooleanValue(false));
1343        return true;
1344    }
1345
1346    // BCE (cluster kill swallow): the PosixStdio::Ipc spawn path clones the
1347    // child while the C++ spawner holds ALL signals blocked in the parent —
1348    // the worker inherits that mask across exec (strace: first worker syscall
1349    // reports ~[KILL STOP]), so a directed SIGTERM from worker.kill() could
1350    // never be delivered to ANY thread: the signal stayed pending forever,
1351    // the worker lived on, and the primary never saw 'exit'. Restore the
1352    // default (empty) mask here — the first thing the worker boot runs on
1353    // the JS thread — so signals reach this process again.
1354    unsafe {
1355        let mut empty_mask: libc::sigset_t = ::std::mem::zeroed();
1356        libc::sigemptyset(&mut empty_mask);
1357        libc::sigprocmask(libc::SIG_SETMASK, &empty_mask, ::std::ptr::null_mut());
1358    }
1359
1360    // SAFETY: fd comes from PosixStdio::Ipc — a live AF_UNIX socket inherited
1361    // from the primary; from_raw_fd takes sole ownership of it.
1362    let sock = unsafe {
1363        <::std::os::unix::net::UnixStream as ::std::os::unix::io::FromRawFd>::from_raw_fd(fd)
1364    };
1365    let channel = crate::ipc_channel::IpcChannel::new(sock);
1366    let self_pid = unsafe { libc::getpid() } as i32;
1367    if let Ok(mut registry) = super::node_child_process::CP_IPC_CHANNELS.lock() {
1368        registry.insert(self_pid, ::std::sync::Arc::new(::std::sync::Mutex::new(channel)));
1369    }
1370    args.rval().set(BooleanValue(true));
1371    true
1372}
1373
1374// ─── Native: __cluster_ipc_send(pid, json) ─────────────────────────────────
1375//
1376// Send a JSON message on the IPC channel registered under `pid` in
1377// CP_IPC_CHANNELS (the primary side registers by worker pid at fork; the
1378// worker side registers under its own pid in __cluster_worker_boot). Used by
1379// the worker's process.send() — child_process's __cp_ipc_send is attached
1380// per-child-object, not exported on its module.
1381
1382#[allow(unsafe_op_in_unsafe_fn)]
1383unsafe extern "C" fn cluster_ipc_send(cx: *mut JSContext, argc: u32, vp: *mut JSVal) -> bool {
1384    let args = CallArgs::from_vp(vp, argc);
1385    let pid = if argc > 0 && (*args.get(0).ptr).is_int32() {
1386        (*args.get(0).ptr).to_int32()
1387    } else {
1388        0
1389    };
1390    if pid == 0 {
1391        args.rval().set(BooleanValue(false));
1392        return true;
1393    }
1394    let json_str = if argc > 1 && (*args.get(1).ptr).is_string() {
1395        crate::js_to_rust_string(cx, *args.get(1).ptr)
1396    } else {
1397        String::new()
1398    };
1399
1400    let outcome: ::std::result::Result<(), String> =
1401        super::node_child_process::CP_IPC_CHANNELS
1402            .lock()
1403            .map_err(|e| format!("registry lock poisoned: {}", e))
1404            .and_then(|registry| {
1405                registry
1406                    .get(&pid)
1407                    .cloned()
1408                    .ok_or_else(|| format!("no ipc channel for pid {}", pid))
1409                    .and_then(|chan_mtx| {
1410                        chan_mtx
1411                            .lock()
1412                            .map_err(|e| format!("channel lock poisoned: {}", e))
1413                            .and_then(|mut chan| {
1414                                chan
1415                                    .send_json(&json_str)
1416                                    .map_err(|e| format!("send_json: {}", e))
1417                            })
1418                    })
1419            });
1420
1421    match outcome {
1422        Ok(()) => {
1423            args.rval().set(BooleanValue(true));
1424            true
1425        }
1426        Err(msg) => {
1427            let c_msg = bun_core::ZBox::from_bytes(msg.as_bytes());
1428            JS_ReportErrorUTF8(cx, c"%s".as_ptr(), c_msg.as_ptr());
1429            args.rval().set(BooleanValue(false));
1430            false
1431        }
1432    }
1433}
1434
1435// ─── Native: __cluster_worker_kill(pid, signal) ────────────────────────────
1436
1437#[allow(unsafe_op_in_unsafe_fn)]
1438unsafe extern "C" fn cluster_worker_kill(_cx: *mut JSContext, argc: u32, vp: *mut JSVal) -> bool {
1439    let args = CallArgs::from_vp(vp, argc);
1440    let pid = if argc > 0 && (*args.get(0).ptr).is_int32() {
1441        (*args.get(0).ptr).to_int32()
1442    } else {
1443        0
1444    };
1445    let sig = if argc > 1 && (*args.get(1).ptr).is_int32() {
1446        (*args.get(1).ptr).to_int32()
1447    } else {
1448        15 // SIGTERM
1449    };
1450    if pid <= 0 {
1451        args.rval().set(BooleanValue(false));
1452        return true;
1453    }
1454    // SAFETY: libc::kill with a numeric pid/sig — kernel validates both.
1455    let rc = unsafe { libc::kill(pid, sig) };
1456    args.rval().set(BooleanValue(rc == 0));
1457    true
1458}
1459
1460const CLUSTER_JS: &str = r#"
1461(function() {
1462  var cluster = require('cluster');
1463  var cp = (function () { try { return require('child_process'); } catch (e) { return null; } })();
1464
1465  var SIG = { SIGHUP: 1, SIGINT: 2, SIGQUIT: 3, SIGABRT: 6, SIGKILL: 9, SIGUSR1: 10, SIGUSR2: 12, SIGTERM: 15 };
1466
1467  // Worker class with EventEmitter mixin.
1468  function Worker(id, process) {
1469    this.id = id;
1470    this.process = process;
1471    this.isConnected = true;
1472    this.isDead = false;
1473    this.exitedAfterDisconnect = false;
1474    this._events = {};
1475    this._onceFlags = {};
1476    this._online = false;
1477    this._disconnecting = false;
1478  }
1479
1480  Worker.prototype.on = function(event, cb) {
1481    if (!this._events[event]) this._events[event] = [];
1482    this._events[event].push(cb);
1483    return this;
1484  };
1485  Worker.prototype.once = function(event, cb) {
1486    this.on(event, cb);
1487    if (!this._onceFlags[event]) this._onceFlags[event] = [];
1488    this._onceFlags[event].push(this._events[event].length - 1);
1489    return this;
1490  };
1491  Worker.prototype.emit = function(event) {
1492    var args = Array.prototype.slice.call(arguments, 1);
1493    var cbs = this._events[event];
1494    if (!cbs || cbs.length === 0) return false;
1495    var onceIndices = this._onceFlags[event] || [];
1496    var remaining = [];
1497    for (var i = 0; i < cbs.length; i++) {
1498      try { cbs[i].apply(null, args); } catch(e) {}
1499      if (onceIndices.indexOf(i) < 0) remaining.push(cbs[i]);
1500    }
1501    this._events[event] = remaining;
1502    this._onceFlags[event] = [];
1503    return true;
1504  };
1505  Worker.prototype.removeListener = function(event, cb) {
1506    var cbs = this._events[event];
1507    if (!cbs) return this;
1508    var idx = cbs.indexOf(cb);
1509    if (idx >= 0) cbs.splice(idx, 1);
1510    return this;
1511  };
1512  Worker.prototype.removeAllListeners = function(event) {
1513    if (event) {
1514      delete this._events[event];
1515    } else {
1516      this._events = {};
1517    }
1518    return this;
1519  };
1520
1521  cluster._Worker = Worker;
1522
1523  // ─── Worker process boot: IPC wiring + process.send / 'message' ──────────
1524  if (cluster.isWorker && cp) {
1525    var fd = parseInt(process.env.BAO_CLUSTER_IPC_FD || '3', 10);
1526    var booted = typeof cluster.__cluster_worker_boot === 'function'
1527      && cluster.__cluster_worker_boot(fd);
1528    if (booted) {
1529      process.connected = true;
1530      process.send = function(message, sendHandle) {
1531        try { return cluster.__cluster_ipc_send(process.pid, JSON.stringify(message)); }
1532        catch (e) { return false; }
1533      };
1534      process.disconnect = function() {
1535        try { process.exit(0); } catch (e) {}
1536      };
1537      // Primary → worker message poll. BCE (parent-loop stall): this used to
1538      // be a `setInterval(..., 10)` that — never cleared or unref'd — pinned
1539      // the worker's event loop forever, so a worker whose script completed
1540      // never exited and the primary (waiting on that exit) never exited
1541      // either. The pump function below is driven by the native
1542      // cluster_pump_all from the drain hook and NEVER pins: a worker with a
1543      // drained loop exits (Node IPC-channel semantics).
1544      function workerIpcPump() {
1545        try {
1546          var m = cp.__cp_ipc_recv(process.pid);
1547          while (m && m.json) {
1548            var obj = null;
1549            try { obj = JSON.parse(m.json); } catch (e) { obj = null; }
1550            if (obj && obj.__cluster === 'disconnect') {
1551              process.exit(0);
1552            } else if (obj) {
1553              try { process.emit('message', obj); } catch (e) {}
1554            }
1555            m = cp.__cp_ipc_recv(process.pid);
1556          }
1557          // Primary closed the channel (disconnect) — exit gracefully.
1558          if (m && m.closed) {
1559            process.exit(0);
1560          }
1561        } catch (e) {}
1562        return false; // never pins the worker loop
1563      }
1564      if (typeof cluster.__cluster_pump_register === 'function') {
1565        cluster.__cluster_pump_register(workerIpcPump, false);
1566      }
1567      // Online handshake → primary emits worker 'online'.
1568      try {
1569        cluster.__cluster_ipc_send(process.pid, JSON.stringify({
1570          __cluster: 'online',
1571          workerId: process.env.BAO_CLUSTER_WORKER_ID
1572        }));
1573      } catch (e) {}
1574    }
1575    var workerId = parseInt(process.env.BAO_CLUSTER_WORKER_ID || '0', 10);
1576    cluster.worker = new Worker(workerId, process);
1577  }
1578
1579  // ─── Primary: wrap fork() results in Worker objects + event pump ─────────
1580  if (cluster.isPrimary) {
1581    var _originalFork = cluster.fork;
1582
1583    function dispatchMessage(w, json) {
1584      var obj = null;
1585      try { obj = JSON.parse(json); } catch (e) { return; }
1586      if (!obj || typeof obj !== 'object') return;
1587      if (obj.__cluster === 'online') {
1588        if (!w._online) {
1589          w._online = true;
1590          w.emit('online');
1591          cluster.emit('online', w);
1592        }
1593        return;
1594      }
1595      w.emit('message', obj);
1596    }
1597
1598    function handleExit(w, code, signal) {
1599      if (w.isDead) return;
1600      w.isDead = true;
1601      w.isConnected = false;
1602      w.exitedAfterDisconnect = !!w._disconnecting;
1603      if (w.process) w.process.exitCode = (code === -1 && signal) ? null : code;
1604      delete cluster.workers[w.id];
1605      try { if (typeof w.__disconnectNative === 'function') w.__disconnectNative(); } catch (e) {}
1606      w.emit('exit', code, signal);
1607      cluster.emit('exit', w, code, signal);
1608    }
1609
1610    // BCE (parent-loop stall): the old pollWorkers setInterval(10) was never
1611    // cleared while `cluster.workers` stayed non-empty — and because the
1612    // worker side never exited (see workerIpcPump note), the primary spun
1613    // forever. The pump function below is driven by the native
1614    // cluster_pump_all from the drain hook; its boolean return is the ONLY
1615    // loop-liveness contribution (true while any worker is registered).
1616    function pollWorkers() {
1617      var ids = Object.keys(cluster.workers);
1618      for (var i = 0; i < ids.length; i++) {
1619        var w = cluster.workers[ids[i]];
1620        if (!w || !w._pid) continue;
1621        if (cp) {
1622          try {
1623            var m = cp.__cp_ipc_recv(w._pid);
1624            while (m && m.json) {
1625              dispatchMessage(w, m.json);
1626              m = cp.__cp_ipc_recv(w._pid);
1627            }
1628          } catch (e) {}
1629          try {
1630            var ex = cp.__cp_poll_exit(w._pid);
1631            if (ex) handleExit(w, ex[0], ex[1]);
1632          } catch (e) {}
1633        }
1634      }
1635      return Object.keys(cluster.workers).length > 0;
1636    }
1637    if (typeof cluster.__cluster_pump_register === 'function') {
1638      cluster.__cluster_pump_register(pollWorkers, true);
1639    }
1640
1641    cluster.fork = function(env) {
1642      var result = _originalFork ? _originalFork.call(cluster, env) : null;
1643      if (result && result.id) {
1644        var worker = new Worker(result.id, result.process || result);
1645        worker._pid = result._pid || (result.process && result.process.pid) || 0;
1646        if (result.process) worker.process = result.process;
1647
1648        // Native bridges from the fork result object.
1649        if (typeof result.send === 'function') {
1650          var nativeSend = result.send;
1651          worker.send = function(message, sendHandle) {
1652            try { return nativeSend.call(result, JSON.stringify(message), sendHandle); }
1653            catch (e) { return false; }
1654          };
1655        }
1656        if (typeof result.disconnect === 'function') {
1657          var nativeDisconnect = result.disconnect;
1658          worker.__disconnectNative = function() { nativeDisconnect.call(result); };
1659          worker.disconnect = function() {
1660            worker._disconnecting = true;
1661            worker.isConnected = false;
1662            try { nativeDisconnect.call(result); } catch (e) {}
1663          };
1664        }
1665        // worker.kill([signal]) — Node semantics: SEND the signal; lifecycle
1666        // state (isDead / exitedAfterDisconnect / cluster.workers membership /
1667        // 'exit' event) is decided by the observed exit in handleExit, not
1668        // here. BCE (kill 永不达 exit): this used to set isDead=true
1669        // immediately, and handleExit early-returns on isDead — so the REAL
1670        // exit was dropped, 'exit' never fired, the worker stayed in
1671        // cluster.workers, and the primary's loop-liveness leak spun forever.
1672        worker.kill = function(signal) {
1673          var sig = typeof signal === 'number' ? signal : (SIG[String(signal).toUpperCase()] || 15);
1674          if (cluster.__cluster_worker_kill) {
1675            try { cluster.__cluster_worker_kill(worker._pid, sig); } catch (e) {}
1676          }
1677          if (sig === 9) return;
1678          // Grace escalation: SIGTERM is deliverable now (worker boot resets
1679          // the inherited all-blocked signal mask), but a worker wedged mid-
1680          // script must still die — after 1s, escalate to SIGKILL so
1681          // kill() ⇒ child dead ⇒ parent 'exit' is unconditional.
1682          var pid = worker._pid;
1683          setTimeout(function () {
1684            if (!worker.isDead && cluster.__cluster_worker_kill) {
1685              try { cluster.__cluster_worker_kill(pid, 9); } catch (e) {}
1686            }
1687          }, 1000);
1688        };
1689        worker.destroy = function(signal) { worker.kill(signal); };
1690
1691        if (!cluster.workers) cluster.workers = {};
1692        cluster.workers[result.id] = worker;
1693        cluster.emit('fork', worker);
1694        return worker;
1695      }
1696      return result;
1697    };
1698
1699    // Cluster-level EventEmitter.
1700    cluster._clusterEvents = {};
1701    cluster.on = function(event, cb) {
1702      if (!cluster._clusterEvents[event]) cluster._clusterEvents[event] = [];
1703      cluster._clusterEvents[event].push(cb);
1704      return cluster;
1705    };
1706    cluster.once = function(event, cb) {
1707      var wrap = function() {
1708        cluster.removeListener(event, wrap);
1709        cb.apply(null, arguments);
1710      };
1711      cluster.on(event, wrap);
1712      return cluster;
1713    };
1714    cluster.emit = function(event) {
1715      var args = Array.prototype.slice.call(arguments, 1);
1716      var cbs = cluster._clusterEvents[event];
1717      if (!cbs) return false;
1718      for (var i = 0; i < cbs.length; i++) {
1719        try { cbs[i].apply(null, args); } catch(e) {}
1720      }
1721      return true;
1722    };
1723    cluster.removeListener = function(event, cb) {
1724      var cbs = cluster._clusterEvents[event];
1725      if (!cbs) return cluster;
1726      var idx = cbs.indexOf(cb);
1727      if (idx >= 0) cbs.splice(idx, 1);
1728      return cluster;
1729    };
1730
1731    // cluster.disconnect(): ask every worker to exit (the worker exits on the
1732    // disconnect IPC message — bao's orderly-exit path can swallow SIGTERM),
1733    // close its channel, then SIGTERM as a backstop.
1734    cluster.disconnect = function(callback) {
1735      cluster._disconnecting = true;
1736      var ids = Object.keys(cluster.workers || {});
1737      for (var i = 0; i < ids.length; i++) {
1738        var w = cluster.workers[ids[i]];
1739        try { if (w.send) w.send({ __cluster: 'disconnect' }); } catch (e) {}
1740        try { if (w.disconnect) w.disconnect(); } catch (e) {}
1741        try { if (cluster.__cluster_worker_kill) cluster.__cluster_worker_kill(w._pid, 15); } catch (e) {}
1742      }
1743      if (typeof callback === 'function') {
1744        setTimeout(callback, 50);
1745      }
1746    };
1747
1748    // Initialize settings._nextId counter.
1749    if (!cluster.settings) cluster.settings = {};
1750    if (!cluster.settings._nextId) cluster.settings._nextId = 1;
1751  }
1752})();
1753"#;
1754
1755#[cfg(test)]
1756mod tests {
1757    use super::*;
1758
1759    #[test]
1760    fn test_is_cluster_worker_default() {
1761        // Pure predicate: no env set → not a worker. No env mutation, no race
1762        // with parallel tests.
1763        assert!(!is_worker_env(None));
1764    }
1765
1766    #[test]
1767    fn test_is_cluster_worker_with_env() {
1768        // fork() issues ids 1, 2, 3… — anything ≥ 1 is a worker.
1769        assert!(is_worker_env(Some("1")));
1770        assert!(is_worker_env(Some("3")));
1771        // "0" is never issued by fork (first id is 1): classify as primary.
1772        assert!(!is_worker_env(Some("0")));
1773    }
1774
1775    #[test]
1776    fn test_is_cluster_worker_strict_predicate() {
1777        // Empty / malformed env entries (e.g. `BAO_CLUSTER_WORKER_ID= bao`)
1778        // must NOT flip a primary into a worker — fork never issues these.
1779        assert!(!is_worker_env(Some("")));
1780        assert!(!is_worker_env(Some("garbage")));
1781        assert!(!is_worker_env(Some("-1")));
1782        assert!(!is_worker_env(Some("1.5")));
1783        assert!(!is_worker_env(Some("1x")));
1784    }
1785
1786    #[test]
1787    fn test_is_primary_default() {
1788        // is_primary = !is_worker
1789        assert!(!is_worker_env(None));
1790    }
1791}