Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 28 additions & 2 deletions crates/perry-runtime/src/child_process/fork.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@

use super::*;
use std::process::Command;
use std::time::Duration;

/// `child_process.fork(modulePath[, args][, options])`. `module_ptr`/`args_ptr`
/// are raw (unboxed) `StringHeader` / `ArrayHeader` pointers; `opts_ptr` is a
Expand Down Expand Up @@ -66,6 +67,8 @@ pub extern "C" fn js_child_process_fork(module_ptr: i64, args_ptr: i64, opts_ptr
let stderr_obj = cp_build_readable();
let stdin_obj = cp_build_writable();
let stdio_kinds = cp_read_stdio(opts_val, 3);
let timeout = cp_read_timeout(opts_val);
let kill_signal = cp_read_kill_signal(opts_val);

// spawnargs = [argv0 ?? execPath, ...execArgv, module, ...args] (matches Node).
let mut spawnargs = crate::array::js_array_alloc((arg_strs.len() + exec_argv.len() + 2) as u32);
Expand Down Expand Up @@ -128,7 +131,16 @@ pub extern "C" fn js_child_process_fork(module_ptr: i64, args_ptr: i64, opts_ptr
cp_apply_options(&mut command, opts_val);
cp_apply_live_stdio(&mut command, &stdio_kinds);

let launched = fork_launch(cp, stdout_obj, stderr_obj, stdin_obj, command, advanced);
let launched = fork_launch(
cp,
stdout_obj,
stderr_obj,
stdin_obj,
command,
advanced,
timeout,
kill_signal,
);
if !launched {
// Spawn failure: emit a deferred `error`, leave `connected` false.
let msg = format!("fork failed: {exec_path}");
Expand Down Expand Up @@ -158,6 +170,8 @@ fn fork_launch(
stdin_obj: f64,
mut command: Command,
advanced: bool,
timeout: Option<Duration>,
kill_signal: i32,
) -> bool {
use std::os::unix::io::AsRawFd;
use std::os::unix::net::UnixStream;
Expand Down Expand Up @@ -203,6 +217,8 @@ fn fork_launch(
child,
Some(parent_sock),
advanced,
timeout,
kill_signal,
);
true
}
Expand All @@ -218,12 +234,22 @@ fn fork_launch(
stdin_obj: f64,
mut command: Command,
advanced: bool,
timeout: Option<Duration>,
kill_signal: i32,
) -> bool {
let _ = advanced;
match command.spawn() {
Ok(child) => {
reactor::cp_register_live_child(
cp, stdout_obj, stderr_obj, stdin_obj, child, None, false,
cp,
stdout_obj,
stderr_obj,
stdin_obj,
child,
None,
false,
timeout,
kill_signal,
);
true
}
Expand Down
97 changes: 96 additions & 1 deletion crates/perry-runtime/src/child_process/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -800,7 +800,34 @@ fn cp_emit(target: f64, event: &str, args: &[f64]) -> bool {
fired
}

fn cp_signal_name(sig: i32) -> &'static str {
pub(super) const CP_SIGTERM: i32 = 15;

#[cfg(unix)]
pub(super) fn cp_signal_name(sig: i32) -> &'static str {
match sig {
x if x == libc::SIGHUP => "SIGHUP",
x if x == libc::SIGINT => "SIGINT",
x if x == libc::SIGQUIT => "SIGQUIT",
x if x == libc::SIGILL => "SIGILL",
x if x == libc::SIGTRAP => "SIGTRAP",
x if x == libc::SIGABRT => "SIGABRT",
x if x == libc::SIGBUS => "SIGBUS",
x if x == libc::SIGFPE => "SIGFPE",
x if x == libc::SIGKILL => "SIGKILL",
x if x == libc::SIGUSR1 => "SIGUSR1",
x if x == libc::SIGSEGV => "SIGSEGV",
x if x == libc::SIGUSR2 => "SIGUSR2",
x if x == libc::SIGPIPE => "SIGPIPE",
x if x == libc::SIGALRM => "SIGALRM",
x if x == libc::SIGTERM => "SIGTERM",
x if x == libc::SIGSTOP => "SIGSTOP",
x if x == libc::SIGCONT => "SIGCONT",
_ => "SIGTERM",
}
}

#[cfg(not(unix))]
pub(super) fn cp_signal_name(sig: i32) -> &'static str {
match sig {
1 => "SIGHUP",
2 => "SIGINT",
Expand All @@ -812,6 +839,74 @@ fn cp_signal_name(sig: i32) -> &'static str {
}
}

#[cfg(unix)]
pub(super) fn cp_signal_number(name: &str) -> Option<i32> {
Some(match name {
"SIGHUP" => libc::SIGHUP,
"SIGINT" => libc::SIGINT,
"SIGQUIT" => libc::SIGQUIT,
"SIGILL" => libc::SIGILL,
"SIGTRAP" => libc::SIGTRAP,
"SIGABRT" => libc::SIGABRT,
"SIGBUS" => libc::SIGBUS,
"SIGFPE" => libc::SIGFPE,
"SIGKILL" => libc::SIGKILL,
"SIGUSR1" => libc::SIGUSR1,
"SIGSEGV" => libc::SIGSEGV,
"SIGUSR2" => libc::SIGUSR2,
"SIGPIPE" => libc::SIGPIPE,
"SIGALRM" => libc::SIGALRM,
"SIGTERM" => libc::SIGTERM,
"SIGSTOP" => libc::SIGSTOP,
"SIGCONT" => libc::SIGCONT,
_ => return None,
})
}

#[cfg(not(unix))]
pub(super) fn cp_signal_number(_name: &str) -> Option<i32> {
None
}

pub(super) fn cp_signal_from_value(signal: f64) -> i32 {
let js = JSValue::from_bits(signal.to_bits());
if js.is_undefined() || js.is_null() {
return CP_SIGTERM;
}
if let Some(name) = cp_value_to_string(signal) {
return cp_signal_number(&name).unwrap_or(CP_SIGTERM);
}
if signal.is_finite() {
let n = signal as i32;
return if n == 0 { CP_SIGTERM } else { n };
}
CP_SIGTERM
}

pub(super) fn cp_read_kill_signal(opts_val: f64) -> i32 {
if cp_object_ptr(opts_val).is_none() {
return CP_SIGTERM;
}
cp_signal_from_value(cp_get_field(opts_val, b"killSignal"))
}

pub(super) fn cp_read_timeout(opts_val: f64) -> Option<std::time::Duration> {
if cp_object_ptr(opts_val).is_none() {
return None;
}
let value = cp_get_field(opts_val, b"timeout");
let js = JSValue::from_bits(value.to_bits());
if js.is_undefined() || js.is_null() {
return None;
}
let timeout = js.to_number();
if timeout.is_finite() && timeout > 0.0 {
Some(std::time::Duration::from_millis(timeout as u64))
} else {
None
}
}

// ----- method bodies (each receives the closure; slot 0 = host `this`) -----

extern "C" fn cp_method_on(closure: *const ClosureHeader, event: f64, cb: f64) -> f64 {
Expand Down
99 changes: 51 additions & 48 deletions crates/perry-runtime/src/child_process/reactor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ use std::io::{Read, Write};
use std::process::{Child, ChildStdin};
use std::sync::atomic::{AtomicU64, Ordering};
use std::sync::{Mutex, PoisonError};
use std::time::Duration;

// Brings in the `cp_*` helpers, `CpFn`, `CP_SHAPE_ID`, the `TAG_*` consts, and
// (since this is a descendant module) `mod.rs`'s `use` bindings such as
Expand Down Expand Up @@ -76,6 +77,9 @@ enum CpEvent {
/// #1933: the IPC channel closed (child disconnected or exited) — flip
/// `connected`/`channel` and emit `'disconnect'`.
IpcClosed { handle: u64 },
/// Timeout expired; terminate the live child with this signal on the main
/// thread so JS-visible `killed` is updated with the same event ordering.
Timeout { handle: u64, signal: i32 },
}

static CP_EVENT_QUEUE: Mutex<Vec<CpEvent>> = Mutex::new(Vec::new());
Expand Down Expand Up @@ -177,6 +181,13 @@ fn cp_spawn_waiter(handle: u64, mut child: Child) {
});
}

fn cp_spawn_timeout(handle: u64, timeout: Duration, signal: i32) {
std::thread::spawn(move || {
std::thread::sleep(timeout);
cp_push_event(CpEvent::Timeout { handle, signal });
});
}

/// IPC reader (#1933): read newline-delimited JSON from the parent socket and
/// push each line for main-thread parse + `'message'` delivery. For
/// `serialization: 'advanced'` (#2130) the framing is instead a 4-byte
Expand Down Expand Up @@ -256,6 +267,8 @@ pub(super) fn cp_register_live_child(
mut child: Child,
ipc: Option<IpcStream>,
ipc_advanced: bool,
timeout: Option<Duration>,
kill_signal: i32,
) -> u64 {
let pid = child.id();
cp_set_field(cp, b"pid", pid as f64);
Expand Down Expand Up @@ -311,6 +324,9 @@ pub(super) fn cp_register_live_child(
cp_spawn_reader(handle, e, true);
}
cp_spawn_waiter(handle, child);
if let Some(timeout) = timeout {
cp_spawn_timeout(handle, timeout, kill_signal);
}
#[cfg(unix)]
{
if let Some(sock) = ipc {
Expand Down Expand Up @@ -438,6 +454,8 @@ pub extern "C" fn js_child_process_spawn_streams(
let stderr_obj = cp_build_readable();
let stdin_obj = cp_build_writable();
let stdio_kinds = cp_read_stdio(opts_val, 3);
let timeout = cp_read_timeout(opts_val);
let kill_signal = cp_read_kill_signal(opts_val);

// spawnargs = [argv0 ?? command, ...args]
let mut spawnargs = crate::array::js_array_alloc((arg_strs.len() + 1) as u32);
Expand Down Expand Up @@ -490,7 +508,17 @@ pub extern "C" fn js_child_process_spawn_streams(

match command.spawn() {
Ok(child) => {
cp_register_live_child(cp, stdout_obj, stderr_obj, stdin_obj, child, None, false);
cp_register_live_child(
cp,
stdout_obj,
stderr_obj,
stdin_obj,
child,
None,
false,
timeout,
kill_signal,
);
}
Err(e) => {
// Spawn failure (e.g. ENOENT): Node emits a single `error` event and
Expand Down Expand Up @@ -656,6 +684,11 @@ fn cp_reactor_pump_inner() {
}
}
}
CpEvent::Timeout { handle, signal } => {
if let Some(cp_bits) = cp_live_kill_signum(handle, signal) {
cp_set_field(f64::from_bits(cp_bits), b"killed", TAG_TRUE_F64);
}
}
}
}

Expand Down Expand Up @@ -739,66 +772,36 @@ pub(super) fn cp_live_stdin_close(handle: u64) {
}
}

/// Signal a live child. `signal` is the JS `kill([signal])` argument (a signal
/// name string, a number, or — for the no-arg / default case — undefined or the
/// `0.0` arg-padding, both treated as `SIGTERM`). Returns whether the signal
/// was delivered.
pub(super) fn cp_live_kill(handle: u64, signal: f64) -> bool {
let pid = {
fn cp_live_kill_signum(handle: u64, signum: i32) -> Option<u64> {
let (pid, cp_bits) = {
let guard = cp_live_lock();
match guard.as_ref().and_then(|map| map.get(&handle)) {
// Skip if already reaped — the pid may have been recycled by the OS.
Some(lc) if lc.exited.is_none() => lc.pid,
_ => return false,
Some(lc) if lc.exited.is_none() => (lc.pid, lc.cp_bits),
_ => return None,
}
};
#[cfg(unix)]
{
let signum = cp_parse_signal(signal);
unsafe { libc::kill(pid, signum) == 0 }
if unsafe { libc::kill(pid, signum) == 0 } {
Some(cp_bits)
} else {
None
}
}
#[cfg(not(unix))]
{
let _ = pid;
false
}
}

/// Map a JS `kill` signal argument to a Unix signal number. Default / no-arg
/// (`undefined` or the `0.0` padding) → `SIGTERM`.
#[cfg(unix)]
fn cp_parse_signal(signal: f64) -> i32 {
const SIGTERM: i32 = libc::SIGTERM;
if JSValue::from_bits(signal.to_bits()).is_undefined() {
return SIGTERM;
}
if let Some(name) = cp_value_to_string(signal) {
return cp_signal_number(&name).unwrap_or(SIGTERM);
}
if signal.is_finite() {
let n = signal as i32;
// 0 is the "no-arg" padding sentinel — treat as the default SIGTERM.
return if n == 0 { SIGTERM } else { n };
let _ = (pid, signum, cp_bits);
None
}
SIGTERM
}

/// Inverse of `super::cp_signal_name` for the common signals.
#[cfg(unix)]
fn cp_signal_number(name: &str) -> Option<i32> {
Some(match name {
"SIGHUP" => libc::SIGHUP,
"SIGINT" => libc::SIGINT,
"SIGQUIT" => libc::SIGQUIT,
"SIGABRT" => libc::SIGABRT,
"SIGKILL" => libc::SIGKILL,
"SIGTERM" => libc::SIGTERM,
"SIGUSR1" => libc::SIGUSR1,
"SIGUSR2" => libc::SIGUSR2,
"SIGSTOP" => libc::SIGSTOP,
"SIGCONT" => libc::SIGCONT,
_ => return None,
})
/// Signal a live child. `signal` is the JS `kill([signal])` argument (a signal
/// name string, a number, or — for the no-arg / default case — undefined or the
/// `0.0` arg-padding, both treated as `SIGTERM`). Returns whether the signal
/// was delivered.
pub(super) fn cp_live_kill(handle: u64, signal: f64) -> bool {
cp_live_kill_signum(handle, cp_signal_from_value(signal)).is_some()
}

// ============================================================================
Expand Down
Loading