diff --git a/crates/perry-runtime/src/child_process/mod.rs b/crates/perry-runtime/src/child_process/mod.rs index 477526851..8ebd472a3 100644 --- a/crates/perry-runtime/src/child_process/mod.rs +++ b/crates/perry-runtime/src/child_process/mod.rs @@ -395,11 +395,11 @@ pub extern "C" fn js_child_process_exec_sync( let run_options = cp_read_run_options(opts_val); let run = cp_run_to_completion(command, &run_options); - let stdout_box = cp_box_output(&run.stdout, &mode); + let stdout_box = cp_box_sync_output(&run.stdout, &mode, run_options.stdout_piped()); if run.success() { return stdout_box; } - let stderr_box = cp_box_output(&run.stderr, &mode); + let stderr_box = cp_box_sync_output(&run.stderr, &mode, run_options.stderr_piped()); cp_sync_throw_error(&run, &cmd_str, stdout_box, stderr_box); } @@ -441,11 +441,15 @@ pub extern "C" fn js_child_process_spawn_sync( let spawn_failed_before_pid = run.spawn_error.is_some() && run.pid.is_none(); let stdout_box = if spawn_failed_before_pid { cp_undefined() + } else if !run_options.stdout_piped() { + TAG_NULL_F64 } else { cp_box_output(&run.stdout, &mode) }; let stderr_box = if spawn_failed_before_pid { cp_undefined() + } else if !run_options.stderr_piped() { + TAG_NULL_F64 } else { cp_box_output(&run.stderr, &mode) }; @@ -742,6 +746,14 @@ fn cp_value_to_string(value: f64) -> Option { } } +fn cp_js_string_value(value: f64) -> Option { + if JSValue::from_bits(value.to_bits()).is_any_string() { + cp_value_to_string(value) + } else { + None + } +} + /// Hidden field key holding the listener array for `event`. fn cp_listener_key(event: &str) -> Vec { let mut k = b"__cpL_".to_vec(); @@ -1418,7 +1430,7 @@ pub(super) enum CpStdio { } fn cp_stdio_kind(value: f64) -> CpStdio { - match cp_value_to_string(value).as_deref() { + match cp_js_string_value(value).as_deref() { Some("ignore") => CpStdio::Ignore, Some("inherit") => CpStdio::Inherit, _ => CpStdio::Pipe, @@ -1435,7 +1447,7 @@ pub(super) fn cp_read_stdio(opts_val: f64, fds: usize) -> Vec { } let stdio = cp_get_field(opts_val, b"stdio"); - if let Some(s) = cp_value_to_string(stdio) { + if let Some(s) = cp_js_string_value(stdio) { match s.as_str() { "ignore" => out.fill(CpStdio::Ignore), "inherit" => out.fill(CpStdio::Inherit), @@ -1591,6 +1603,14 @@ fn cp_box_output(bytes: &[u8], mode: &CpOutput) -> f64 { } } +fn cp_box_sync_output(bytes: &[u8], mode: &CpOutput, piped: bool) -> f64 { + if piped { + cp_box_output(bytes, mode) + } else { + TAG_NULL_F64 + } +} + /// Decoded exit disposition of a finished child. struct CpExit { /// Exit code when the child exited normally; `None` when killed by signal. @@ -1953,11 +1973,11 @@ pub extern "C" fn js_child_process_exec_file_sync( let run_options = cp_read_run_options(opts_val); let run = cp_run_to_completion(command, &run_options); - let stdout_box = cp_box_output(&run.stdout, &mode); + let stdout_box = cp_box_sync_output(&run.stdout, &mode, run_options.stdout_piped()); if run.success() { return stdout_box; } - let stderr_box = cp_box_output(&run.stderr, &mode); + let stderr_box = cp_box_sync_output(&run.stderr, &mode, run_options.stderr_piped()); cp_sync_throw_error( &run, &cp_file_cmd_display(&file_str, &arg_strs), diff --git a/crates/perry-runtime/src/child_process/sync_run.rs b/crates/perry-runtime/src/child_process/sync_run.rs index 2115cb0b2..90822aaac 100644 --- a/crates/perry-runtime/src/child_process/sync_run.rs +++ b/crates/perry-runtime/src/child_process/sync_run.rs @@ -1,11 +1,16 @@ +#[cfg(unix)] +use std::fs::File; use std::io::Write; +#[cfg(unix)] +use std::os::fd::FromRawFd; use std::process::{Command, Stdio}; use std::time::{Duration, Instant}; use crate::value::JSValue; use super::{ - cp_decode_status, cp_get_field, cp_io_error_code, cp_object_ptr, cp_value_to_bytes, CpExit, + cp_array_ptr, cp_decode_status, cp_get_field, cp_io_error_code, cp_js_string_value, + cp_object_ptr, cp_value_to_bytes, CpExit, }; const CP_DEFAULT_MAX_BUFFER: usize = 1024 * 1024; @@ -16,6 +21,7 @@ pub(super) struct CpRunOptions { input: Option>, timeout: Option, pub(super) max_buffer: usize, + stdio: [CpSyncStdio; 3], } impl Default for CpRunOptions { @@ -24,10 +30,29 @@ impl Default for CpRunOptions { input: None, timeout: None, max_buffer: CP_DEFAULT_MAX_BUFFER, + stdio: [CpSyncStdio::Pipe; 3], } } } +#[derive(Clone, Copy, PartialEq, Eq)] +enum CpSyncStdio { + Pipe, + Ignore, + Inherit, + Fd(i32), +} + +impl CpRunOptions { + pub(super) fn stdout_piped(&self) -> bool { + self.stdio[1] == CpSyncStdio::Pipe + } + + pub(super) fn stderr_piped(&self) -> bool { + self.stdio[2] == CpSyncStdio::Pipe + } +} + fn cp_read_option_number(opts_val: f64, key: &[u8]) -> Option { if cp_object_ptr(opts_val).is_none() { return None; @@ -92,6 +117,7 @@ pub(super) fn cp_read_run_options(opts_val: f64) -> CpRunOptions { if let Some(input) = cp_read_input_bytes(cp_get_field(opts_val, b"input")) { options.input = Some(input); } + options.stdio = cp_read_sync_stdio(opts_val); cp_read_timing_and_buffer_options(opts_val, &mut options); options @@ -123,6 +149,77 @@ fn cp_read_timing_and_buffer_options(opts_val: f64, options: &mut CpRunOptions) } } +fn cp_sync_stdio_kind(value: f64) -> CpSyncStdio { + match cp_js_string_value(value).as_deref() { + Some("ignore") => return CpSyncStdio::Ignore, + Some("inherit") => return CpSyncStdio::Inherit, + Some("pipe") => return CpSyncStdio::Pipe, + _ => {} + } + + let js_value = JSValue::from_bits(value.to_bits()); + if js_value.is_number() || js_value.is_int32() { + let n = js_value.to_number(); + if n.is_finite() && n >= 0.0 && n.fract() == 0.0 && n <= i32::MAX as f64 { + return CpSyncStdio::Fd(n as i32); + } + } + + CpSyncStdio::Pipe +} + +fn cp_read_sync_stdio(opts_val: f64) -> [CpSyncStdio; 3] { + let mut out = [CpSyncStdio::Pipe; 3]; + let stdio = cp_get_field(opts_val, b"stdio"); + + if let Some(s) = cp_js_string_value(stdio) { + match s.as_str() { + "ignore" => out = [CpSyncStdio::Ignore; 3], + "inherit" => out = [CpSyncStdio::Inherit; 3], + "pipe" => {} + _ => {} + } + return out; + } + + let Some(arr) = cp_array_ptr(stdio) else { + return out; + }; + let n = unsafe { (*arr).length }.min(3); + for i in 0..n { + out[i as usize] = cp_sync_stdio_kind(crate::array::js_array_get_f64(arr, i)); + } + out +} + +#[cfg(unix)] +fn cp_fd_stdio(fd: i32) -> Option { + if let Some(file) = crate::fs::clone_registered_fd(fd) { + return Some(Stdio::from(file)); + } + + let dup_fd = unsafe { libc::dup(fd) }; + if dup_fd < 0 { + return None; + } + let file = unsafe { File::from_raw_fd(dup_fd) }; + Some(Stdio::from(file)) +} + +#[cfg(not(unix))] +fn cp_fd_stdio(fd: i32) -> Option { + crate::fs::clone_registered_fd(fd).map(Stdio::from) +} + +fn cp_stdio_from_kind(kind: CpSyncStdio, pipe: Stdio) -> Stdio { + match kind { + CpSyncStdio::Pipe => pipe, + CpSyncStdio::Ignore => Stdio::null(), + CpSyncStdio::Inherit => Stdio::inherit(), + CpSyncStdio::Fd(fd) => cp_fd_stdio(fd).unwrap_or_else(Stdio::null), + } +} + #[derive(Clone, Copy, PartialEq, Eq)] pub(super) enum CpRunError { MaxBuffer, @@ -162,13 +259,14 @@ impl CpRun { /// capture stdout/stderr/exit/pid. Used by the synchronous + buffered-callback /// entry points. pub(super) fn cp_run_to_completion(mut command: Command, options: &CpRunOptions) -> CpRun { - if options.input.is_some() { - command.stdin(Stdio::piped()); + let pipe_stdin = if options.input.is_some() { + Stdio::piped() } else { - command.stdin(Stdio::null()); - } - command.stdout(Stdio::piped()); - command.stderr(Stdio::piped()); + Stdio::null() + }; + command.stdin(cp_stdio_from_kind(options.stdio[0], pipe_stdin)); + command.stdout(cp_stdio_from_kind(options.stdio[1], Stdio::piped())); + command.stderr(cp_stdio_from_kind(options.stdio[2], Stdio::piped())); match command.spawn() { Ok(mut child) => { let pid = child.id(); diff --git a/crates/perry-runtime/src/fs/mod.rs b/crates/perry-runtime/src/fs/mod.rs index 65d9a2ce6..2c29b7095 100644 --- a/crates/perry-runtime/src/fs/mod.rs +++ b/crates/perry-runtime/src/fs/mod.rs @@ -74,6 +74,10 @@ pub(crate) fn fd_is_registered(fd: i32) -> bool { FD_REGISTRY.with(|r| r.borrow().contains_key(&fd)) } +pub(crate) fn clone_registered_fd(fd: i32) -> Option { + FD_REGISTRY.with(|r| r.borrow().get(&fd).and_then(|file| file.try_clone().ok())) +} + pub(crate) fn filehandle_object_fd(value: f64) -> Option { let bits = value.to_bits(); if (bits & !POINTER_MASK) != POINTER_TAG { diff --git a/test-parity/node-suite/child_process/sync/sync-stdio-fd.ts b/test-parity/node-suite/child_process/sync/sync-stdio-fd.ts new file mode 100644 index 000000000..5b597e0f6 --- /dev/null +++ b/test-parity/node-suite/child_process/sync/sync-stdio-fd.ts @@ -0,0 +1,101 @@ +import { execFileSync, execSync, spawnSync } from "node:child_process"; +import { closeSync, mkdirSync, openSync, readFileSync, rmSync, writeFileSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; + +function text(value: unknown) { + if (value === null) return "null"; + if (value === undefined) return "undefined"; + if (Buffer.isBuffer(value)) return `Buffer:${value.toString()}`; + return String(value); +} + +function outputText(output: unknown) { + return Array.isArray(output) ? output.map(text).join("|") : text(output); +} + +function readText(path: string) { + return readFileSync(path, "utf8"); +} + +const dir = join(tmpdir(), `perry-sync-stdio-fd-${process.pid}`); +rmSync(dir, { recursive: true, force: true }); +mkdirSync(dir); + +try { + const outPath = join(dir, "out.txt"); + const errPath = join(dir, "err.txt"); + const stdinPath = join(dir, "stdin.txt"); + rmSync(outPath, { force: true }); + rmSync(errPath, { force: true }); + + let outFd = openSync(outPath, "w"); + let errFd = openSync(errPath, "w"); + const spawnResult = spawnSync("sh", ["-c", "printf out; printf err >&2; exit 7"], { + stdio: ["ignore", outFd, errFd], + encoding: "utf8", + }); + closeSync(outFd); + closeSync(errFd); + + console.log("spawn status:", spawnResult.status); + console.log("spawn stdout:", text(spawnResult.stdout)); + console.log("spawn stderr:", text(spawnResult.stderr)); + console.log("spawn output:", outputText(spawnResult.output)); + console.log("spawn files:", readText(outPath), readText(errPath)); + + outFd = openSync(outPath, "w"); + errFd = openSync(errPath, "w"); + const execFileResult = execFileSync("sh", ["-c", "printf fileout; printf fileerr >&2"], { + stdio: ["ignore", outFd, errFd], + encoding: "utf8", + }); + closeSync(outFd); + closeSync(errFd); + + console.log("execFile return:", text(execFileResult)); + console.log("execFile files:", readText(outPath), readText(errPath)); + + outFd = openSync(outPath, "w"); + errFd = openSync(errPath, "w"); + const execResult = execSync("printf execout; printf execerr >&2", { + stdio: ["ignore", outFd, errFd], + encoding: "utf8", + }); + closeSync(outFd); + closeSync(errFd); + + console.log("exec return:", text(execResult)); + console.log("exec files:", readText(outPath), readText(errPath)); + + outFd = openSync(outPath, "w"); + errFd = openSync(errPath, "w"); + try { + execFileSync("sh", ["-c", "printf failout; printf failerr >&2; exit 9"], { + stdio: ["ignore", outFd, errFd], + encoding: "utf8", + }); + } catch (err: any) { + console.log("execFile throw status:", err.status); + console.log("execFile throw stdout:", text(err.stdout)); + console.log("execFile throw stderr:", text(err.stderr)); + console.log("execFile throw output:", outputText(err.output)); + } + closeSync(outFd); + closeSync(errFd); + + console.log("execFile throw files:", readText(outPath), readText(errPath)); + + writeFileSync(stdinPath, "fdinput"); + const inFd = openSync(stdinPath, "r"); + const stdinResult = spawnSync("cat", [], { + stdio: [inFd, "pipe", "pipe"], + encoding: "utf8", + }); + closeSync(inFd); + + console.log("spawn stdin fd stdout:", text(stdinResult.stdout)); + console.log("spawn stdin fd output:", outputText(stdinResult.output)); +} finally { + rmSync(dir, { recursive: true, force: true }); +}