Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
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
13 changes: 13 additions & 0 deletions ext/node/polyfills/internal/child_process.ts
Original file line number Diff line number Diff line change
Expand Up @@ -74,8 +74,10 @@ import {
kExtraStdio,
kInputOption,
kIpc,
kKillSignalOption,
kNeedsNpmProcessState,
kSerialization,
kTimeoutOption,
} from "ext:deno_process/40_process.js";

export function mapValues<T, O>(
Expand Down Expand Up @@ -1634,6 +1636,8 @@ export function spawnSync(
uid,
gid,
maxBuffer,
timeout,
killSignal,
Comment on lines +1645 to +1646
Copy link

Copilot AI Mar 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Now that spawnSync forwards timeout/killSignal into Deno.Command and interprets _killedByTimeout, the SpawnSyncOptions.killSignal typing/comment earlier in this file is out of date (it currently says the option is not implemented and only types it as string). Consider updating the type to string | number (Node accepts both) and removing/updating the "not yet implemented" note to match the new behavior.

Copilot uses AI. Check for mistakes.
windowsVerbatimArguments = false,
} = options;
const [
Expand Down Expand Up @@ -1668,6 +1672,8 @@ export function spawnSync(
// deno-lint-ignore no-explicit-any
[kNeedsNpmProcessState]: (options as any)[kNeedsNpmProcessState] ||
includeNpmProcessState,
[kTimeoutOption]: timeout,
[kKillSignalOption]: killSignal,
}).outputSync();

const status = output.signal ? null : output.code;
Expand All @@ -1681,11 +1687,18 @@ export function spawnSync(
result.error = _createSpawnError("ENOBUFS", command, args, true);
}

// deno-lint-ignore no-explicit-any
if ((output as any).killedByTimeout) {
result.error = _createSpawnError("ETIMEDOUT", command, args, true);
}

if (encoding && encoding !== "buffer") {
stdout = stdout && stdout.toString(encoding);
stderr = stderr && stderr.toString(encoding);
}

// deno-lint-ignore no-explicit-any
result.pid = (output as any).pid;
result.status = status;
result.signal = output.signal;
result.stdout = stdout;
Expand Down
23 changes: 21 additions & 2 deletions ext/process/40_process.js
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,10 @@ import {

// The key for private `input` option for `Deno.Command`
const kInputOption = Symbol("kInputOption");
// The key for private `timeout` option for `Deno.Command`
const kTimeoutOption = Symbol("kTimeoutOption");
// The key for private `killSignal` option for `Deno.Command`
const kKillSignalOption = Symbol("kKillSignalOption");

function opKill(pid, signo, apiName) {
op_kill(pid, signo, apiName);
Expand Down Expand Up @@ -471,13 +475,15 @@ function spawnSyncInner(command, {
windowsRawArguments = false,
[kInputOption]: input,
[kNeedsNpmProcessState]: needsNpmProcessState = false,
[kTimeoutOption]: timeout,
[kKillSignalOption]: killSignal,
} = { __proto__: null }) {
if (stdin === "piped") {
throw new TypeError(
"Piped stdin is not supported for this function, use 'Deno.Command().spawn()' instead",
);
}
const result = op_spawn_sync({
const spawnArgs = {
cmd: pathFromURL(command),
args: ArrayPrototypeMap(args, String),
cwd: pathFromURL(cwd),
Expand All @@ -493,11 +499,22 @@ function spawnSyncInner(command, {
detached: false,
needsNpmProcessState,
input,
});
};
if (timeout != null && timeout > 0) {
spawnArgs.timeout = timeout;
if (killSignal != null) {
spawnArgs.killSignal = typeof killSignal === "number"
? String(killSignal)
: killSignal;
}
}
const result = op_spawn_sync(spawnArgs);
return {
pid: result.pid,
success: result.status.success,
code: result.status.code,
signal: result.status.signal,
killedByTimeout: result.killedByTimeout,
get stdout() {
if (result.stdout == null) {
throw new TypeError("Cannot get 'stdout': 'stdout' is not piped");
Expand Down Expand Up @@ -597,6 +614,8 @@ export {
Command,
kill,
kInputOption,
kKillSignalOption,
kTimeoutOption,
Process,
run,
spawn,
Expand Down
55 changes: 55 additions & 0 deletions ext/process/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -250,6 +250,11 @@
extra_stdio: Vec<Stdio>,
detached: bool,
needs_npm_process_state: bool,

#[serde(default)]
timeout: Option<u64>,
#[serde(default)]
kill_signal: Option<String>,
}

#[derive(Deserialize)]
Expand Down Expand Up @@ -393,9 +398,11 @@

#[derive(ToV8)]
pub struct SpawnOutput {
pid: u32,
status: ChildStatus,
stdout: Option<Uint8Array>,
stderr: Option<Uint8Array>,
killed_by_timeout: bool,
}

type CreateCommand = (
Expand Down Expand Up @@ -1110,28 +1117,75 @@
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 timeout = args.timeout;
let kill_signal_str = args.kill_signal.clone();
let (mut command, _, _, _) =
create_command(state, args, "Deno.Command().outputSync()")?;

let mut child = command.spawn().map_err(|e| ProcessError::SpawnFailed {
command: command.get_program().to_string_lossy().into_owned(),
error: Box::new(e.into()),
})?;
let pid = child.id();
if let Some(input) = input {
let mut stdin = child.stdin.take().ok_or_else(|| {
ProcessError::Io(std::io::Error::other("stdin is not available"))
})?;
stdin.write_all(&input)?;
stdin.flush()?;
}

// If timeout is specified, spawn a thread that will kill the child
// after the timeout expires.
let killed_by_timeout =
std::sync::Arc::new(std::sync::atomic::AtomicBool::new(false));
if let Some(timeout_ms) = timeout {

Check failure on line 1142 in ext/process/lib.rs

View workflow job for this annotation

GitHub Actions / lint debug linux-x86_64

this `if` statement can be collapsed
if timeout_ms > 0 {
let child_id = child.id();
let killed = killed_by_timeout.clone();
let kill_signal = kill_signal_str.as_deref().unwrap_or("SIGTERM");
#[cfg(unix)]
let signal =
deno_signals::signal_str_to_int(kill_signal).unwrap_or(libc::SIGTERM);
Copy link

Copilot AI Mar 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

killSignal numeric values from Node are converted to strings (eg 9 -> "9") and then passed through deno_signals::signal_str_to_int, which won't recognize numeric strings. This means numeric killSignal values won't work on Unix and will silently fall back to SIGTERM. Consider supporting numeric signals (eg parse integers or accept a number in SpawnArgs.kill_signal).

Suggested change
let signal =
deno_signals::signal_str_to_int(kill_signal).unwrap_or(libc::SIGTERM);
let signal = deno_signals::signal_str_to_int(kill_signal)
.or_else(|| kill_signal.parse::<i32>().ok())
.unwrap_or(libc::SIGTERM);

Copilot uses AI. Check for mistakes.
std::thread::spawn(move || {
std::thread::sleep(std::time::Duration::from_millis(timeout_ms));
killed.store(true, std::sync::atomic::Ordering::SeqCst);
#[cfg(unix)]
unsafe {

Check failure on line 1154 in ext/process/lib.rs

View workflow job for this annotation

GitHub Actions / lint debug linux-x86_64

unsafe block missing a safety comment
libc::kill(child_id as i32, signal);
}
#[cfg(windows)]
{
// On Windows, use TerminateProcess via the child handle
// We can't easily signal, so just kill the process
unsafe {
let handle = windows_sys::Win32::System::Threading::OpenProcess(
windows_sys::Win32::System::Threading::PROCESS_TERMINATE,
0,
child_id,
);
if handle != 0 {
windows_sys::Win32::System::Threading::TerminateProcess(
handle, 1,
);
windows_sys::Win32::Foundation::CloseHandle(handle);
}
}
}
});
}
}

let output =
child
.wait_with_output()
.map_err(|e| ProcessError::SpawnFailed {
command: command.get_program().to_string_lossy().into_owned(),
error: Box::new(e.into()),
})?;
let timed_out = killed_by_timeout.load(std::sync::atomic::Ordering::SeqCst);
Ok(SpawnOutput {
pid,
status: output.status.try_into()?,
stdout: if stdout {
Some(output.stdout.into())
Expand All @@ -1143,6 +1197,7 @@
} else {
None
},
killed_by_timeout: timed_out,
})
}

Expand Down
2 changes: 2 additions & 0 deletions tests/node_compat/config.jsonc
Original file line number Diff line number Diff line change
Expand Up @@ -253,6 +253,7 @@
"parallel/test-child-process-spawnsync-env.js": {},
"parallel/test-child-process-spawnsync-input.js": {},
"parallel/test-child-process-spawnsync-maxbuf.js": {},
"parallel/test-child-process-spawnsync-timeout.js": {},
"parallel/test-child-process-spawnsync-validation-errors.js": {},
"parallel/test-child-process-spawnsync.js": {},
"parallel/test-child-process-stdin-ipc.js": {},
Expand Down Expand Up @@ -2524,6 +2525,7 @@
// "pummel/test-heapdump-inspector.js": {},
"pummel/test-string-decoder-large-buffer.js": {},
"sequential/test-buffer-creation-regression.js": {},
"sequential/test-child-process-execsync.js": {},
"sequential/test-child-process-exit.js": {},
"sequential/test-cli-syntax-bad.js": {},
"sequential/test-cli-syntax-file-not-found.js": {
Expand Down
Loading