fix(ext/node): support input option in spawnSync (#28792)

This commit is contained in:
Yoshiya Hinosawa 2025-04-09 13:42:12 +09:00 committed by David Sherret
parent d1b4fcd77c
commit 3f510d2f0e
4 changed files with 99 additions and 3 deletions

View file

@ -59,6 +59,7 @@ import { Socket } from "node:net";
import {
kDetached,
kExtraStdio,
kInputOption,
kIpc,
kNeedsNpmProcessState,
} from "ext:deno_process/40_process.js";
@ -985,6 +986,27 @@ function parseSpawnSyncOutputStreams(
}
}
function normalizeInput(input: unknown) {
if (input == null) {
return null;
}
if (typeof input === "string") {
return Buffer.from(input);
}
if (input instanceof Uint8Array) {
return input;
}
if (input instanceof DataView) {
return Buffer.from(input.buffer, input.byteOffset, input.byteLength);
}
throw new ERR_INVALID_ARG_TYPE("input", [
"string",
"Buffer",
"TypedArray",
"DataView",
], input);
}
export function spawnSync(
command: string,
args: string[],
@ -992,6 +1014,7 @@ export function spawnSync(
): SpawnSyncResult {
const {
env = Deno.env.toObject(),
input,
stdio = ["pipe", "pipe", "pipe"],
shell = false,
cwd,
@ -1008,6 +1031,7 @@ export function spawnSync(
_channel, // TODO(kt3k): handle this correctly
] = normalizeStdioOption(stdio);
[command, args] = buildCommand(command, args ?? [], shell);
const input_ = normalizeInput(input);
const result: SpawnSyncResult = {};
try {
@ -1021,6 +1045,7 @@ export function spawnSync(
uid,
gid,
windowsRawArguments: windowsVerbatimArguments,
[kInputOption]: input_,
}).outputSync();
const status = output.signal ? null : output.code;

View file

@ -41,6 +41,9 @@ import {
writableStreamForRid,
} from "ext:deno_web/06_streams.js";
// The key for private `input` option for `Deno.Command`
const kInputOption = Symbol("kInputOption");
function opKill(pid, signo, apiName) {
op_kill(pid, signo, apiName);
}
@ -404,6 +407,7 @@ function spawnSync(command, {
stdout = "piped",
stderr = "piped",
windowsRawArguments = false,
[kInputOption]: input,
} = { __proto__: null }) {
if (stdin === "piped") {
throw new TypeError(
@ -425,6 +429,7 @@ function spawnSync(command, {
extraStdio: [],
detached: false,
needsNpmProcessState: false,
input,
});
return {
success: result.status.success,
@ -484,4 +489,4 @@ class Command {
}
}
export { ChildProcess, Command, kill, Process, run };
export { ChildProcess, Command, kill, kInputOption, Process, run };

View file

@ -20,6 +20,7 @@ use deno_core::op2;
use deno_core::serde_json;
use deno_core::AsyncMutFuture;
use deno_core::AsyncRefCell;
use deno_core::JsBuffer;
use deno_core::OpState;
use deno_core::RcRef;
use deno_core::Resource;
@ -193,6 +194,8 @@ pub struct SpawnArgs {
#[serde(flatten)]
stdio: ChildStdio,
input: Option<JsBuffer>,
extra_stdio: Vec<Stdio>,
detached: bool,
needs_npm_process_state: bool,
@ -437,6 +440,8 @@ fn create_command(
if args.stdio.stdin.is_ipc() {
args.ipc = Some(0);
} else if args.input.is_some() {
command.stdin(std::process::Stdio::piped());
} else {
command.stdin(args.stdio.stdin.as_stdio(state)?);
}
@ -936,13 +941,31 @@ fn op_spawn_sync(
) -> Result<SpawnOutput, ProcessError> {
let stdout = matches!(args.stdio.stdout, StdioOrRid::Stdio(Stdio::Piped));
let stderr = matches!(args.stdio.stderr, StdioOrRid::Stdio(Stdio::Piped));
let input = args.input.clone();
let (mut command, _, _, _) =
create_command(state, args, "Deno.Command().outputSync()")?;
let output = command.output().map_err(|e| ProcessError::SpawnFailed {
let mut child = command.spawn().map_err(|e| ProcessError::SpawnFailed {
command: command.get_program().to_string_lossy().to_string(),
error: Box::new(e.into()),
})?;
if let Some(input) = input {
let mut stdin = child.stdin.take().ok_or_else(|| {
ProcessError::Io(std::io::Error::new(
std::io::ErrorKind::Other,
"stdin is not available",
))
})?;
stdin.write_all(&input)?;
stdin.flush()?;
}
let output =
child
.wait_with_output()
.map_err(|e| ProcessError::SpawnFailed {
command: command.get_program().to_string_lossy().to_string(),
error: Box::new(e.into()),
})?;
Ok(SpawnOutput {
status: output.status.try_into()?,
stdout: if stdout {

View file

@ -1128,3 +1128,46 @@ Deno.test(async function noWarningsFlag() {
await timeout.promise;
});
Deno.test({
name: "[node/child_process] spawnSync supports input option",
fn() {
const text = " console.log('hello')";
const expected = `console.log("hello");\n`;
{
const { stdout } = spawnSync(Deno.execPath(), ["fmt", "-"], {
input: text,
});
assertEquals(stdout.toString(), expected);
}
{
const { stdout } = spawnSync(Deno.execPath(), ["fmt", "-"], {
input: Buffer.from(text),
});
assertEquals(stdout.toString(), expected);
}
{
const { stdout } = spawnSync(Deno.execPath(), ["fmt", "-"], {
input: new TextEncoder().encode(text),
});
assertEquals(stdout.toString(), expected);
}
{
const { stdout } = spawnSync(Deno.execPath(), ["fmt", "-"], {
input: new DataView(Buffer.from(text).buffer),
});
assertEquals(stdout.toString(), expected);
}
assertThrows(
() => {
spawnSync(Deno.execPath(), ["fmt", "-"], {
// deno-lint-ignore no-explicit-any
input: {} as any,
});
},
Error,
'The "input" argument must be of type string or an instance of Buffer, TypedArray, or DataView. Received an instance of Object',
);
},
});