Skip to content
Closed
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
32 changes: 26 additions & 6 deletions crates/perry-runtime/src/child_process/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}

Expand Down Expand Up @@ -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)
};
Expand Down Expand Up @@ -742,6 +746,14 @@ fn cp_value_to_string(value: f64) -> Option<String> {
}
}

fn cp_js_string_value(value: f64) -> Option<String> {
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<u8> {
let mut k = b"__cpL_".to_vec();
Expand Down Expand Up @@ -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,
Expand All @@ -1435,7 +1447,7 @@ pub(super) fn cp_read_stdio(opts_val: f64, fds: usize) -> Vec<CpStdio> {
}

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),
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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),
Expand Down
112 changes: 105 additions & 7 deletions crates/perry-runtime/src/child_process/sync_run.rs
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -16,6 +21,7 @@ pub(super) struct CpRunOptions {
input: Option<Vec<u8>>,
timeout: Option<Duration>,
pub(super) max_buffer: usize,
stdio: [CpSyncStdio; 3],
}

impl Default for CpRunOptions {
Expand All @@ -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<f64> {
if cp_object_ptr(opts_val).is_none() {
return None;
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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<Stdio> {
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<Stdio> {
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,
Expand Down Expand Up @@ -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();
Expand Down
4 changes: 4 additions & 0 deletions crates/perry-runtime/src/fs/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<fs::File> {
FD_REGISTRY.with(|r| r.borrow().get(&fd).and_then(|file| file.try_clone().ok()))
}

pub(crate) fn filehandle_object_fd(value: f64) -> Option<i32> {
let bits = value.to_bits();
if (bits & !POINTER_MASK) != POINTER_TAG {
Expand Down
101 changes: 101 additions & 0 deletions test-parity/node-suite/child_process/sync/sync-stdio-fd.ts
Original file line number Diff line number Diff line change
@@ -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 });
}