Skip to content

Commit cc5a133

Browse files
authored
feat(process): support integer signals in Deno.kill and child.kill (#31153)
Solution for #30910 now this is valid: ```js function isRunning(pid: number): boolean { try { Deno.kill(pid, 0) return true } catch (e) { if (e instanceof Deno.errors.NotFound) { console.log(`pid (${pid}) does not exist`) return false } if (e instanceof Deno.errors.PermissionDenied) { console.log(`pid (${pid}) has been reused`) return true } console.error(`cannot determine status of pid (${pid})`) throw e } } console.log(`isRunning(${Deno.pid}) =>`, isRunning(Deno.pid)) console.log(`isRunning(${1}) =>`, isRunning(1)) ```
1 parent 5eccca8 commit cc5a133

File tree

4 files changed

+110
-16
lines changed

4 files changed

+110
-16
lines changed

cli/tsc/dts/lib.deno.ns.d.ts

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3796,13 +3796,13 @@ declare namespace Deno {
37963796
/** Waits for the child to exit completely, returning all its output and
37973797
* status. */
37983798
output(): Promise<CommandOutput>;
3799-
/** Kills the process with given {@linkcode Deno.Signal}.
3799+
/** Kills the process with given {@linkcode Deno.Signal} or numeric signal.
38003800
*
38013801
* Defaults to `SIGTERM` if no signal is provided.
38023802
*
38033803
* @param [signo="SIGTERM"]
38043804
*/
3805-
kill(signo?: Signal): void;
3805+
kill(signo?: Signal | number): void;
38063806

38073807
/** Ensure that the status of the child process prevents the Deno process
38083808
* from exiting. */
@@ -4739,12 +4739,14 @@ declare namespace Deno {
47394739
* Deno.kill(child.pid, "SIGINT");
47404740
* ```
47414741
*
4742+
* As a special case, a signal of 0 can be used to test for the existence of a process.
4743+
*
47424744
* Requires `allow-run` permission.
47434745
*
47444746
* @tags allow-run
47454747
* @category Subprocess
47464748
*/
4747-
export function kill(pid: number, signo?: Signal): void;
4749+
export function kill(pid: number, signo?: Signal | number): void;
47484750

47494751
/** The type of the resource record to resolve via DNS using
47504752
* {@linkcode Deno.resolveDns}.

ext/process/lib.rs

Lines changed: 37 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1047,11 +1047,18 @@ fn op_spawn_sync(
10471047
})
10481048
}
10491049

1050-
#[op2(fast)]
1050+
#[derive(serde::Deserialize)]
1051+
#[serde(untagged)]
1052+
enum SignalArg {
1053+
String(String),
1054+
Int(i32),
1055+
}
1056+
1057+
#[op2(stack_trace)]
10511058
fn op_spawn_kill(
10521059
state: &mut OpState,
10531060
#[smi] rid: ResourceId,
1054-
#[string] signal: String,
1061+
#[serde] signal: SignalArg,
10551062
) -> Result<(), ProcessError> {
10561063
if let Ok(child_resource) = state.resource_table.get::<ChildResource>(rid) {
10571064
deprecated::kill(child_resource.1 as i32, &signal)?;
@@ -1262,20 +1269,32 @@ mod deprecated {
12621269
}
12631270

12641271
#[cfg(unix)]
1265-
pub fn kill(pid: i32, signal: &str) -> Result<(), ProcessError> {
1266-
let signo = deno_signals::signal_str_to_int(signal)
1267-
.map_err(SignalError::InvalidSignalStr)?;
1272+
pub fn kill(pid: i32, signal: &SignalArg) -> Result<(), ProcessError> {
1273+
let signo = match signal {
1274+
SignalArg::Int(n) => *n,
1275+
SignalArg::String(s) => deno_signals::signal_str_to_int(s)
1276+
.map_err(SignalError::InvalidSignalStr)?,
1277+
};
12681278
use nix::sys::signal::Signal;
12691279
use nix::sys::signal::kill as unix_kill;
12701280
use nix::unistd::Pid;
1271-
let sig =
1272-
Signal::try_from(signo).map_err(|e| ProcessError::Nix(JsNixError(e)))?;
1273-
unix_kill(Pid::from_raw(pid), Some(sig))
1281+
1282+
// Signal 0 is special, it checks if the process exists without sending a signal
1283+
let sig = if signo == 0 {
1284+
None
1285+
} else {
1286+
Some(
1287+
Signal::try_from(signo)
1288+
.map_err(|e| ProcessError::Nix(JsNixError(e)))?,
1289+
)
1290+
};
1291+
1292+
unix_kill(Pid::from_raw(pid), sig)
12741293
.map_err(|e| ProcessError::Nix(JsNixError(e)))
12751294
}
12761295

12771296
#[cfg(not(unix))]
1278-
pub fn kill(pid: i32, signal: &str) -> Result<(), ProcessError> {
1297+
pub fn kill(pid: i32, signal: &SignalArg) -> Result<(), ProcessError> {
12791298
use std::io::Error;
12801299
use std::io::ErrorKind::NotFound;
12811300

@@ -1289,10 +1308,15 @@ mod deprecated {
12891308
use winapi::um::processthreadsapi::TerminateProcess;
12901309
use winapi::um::winnt::PROCESS_TERMINATE;
12911310

1292-
if !matches!(signal, "SIGKILL" | "SIGTERM") {
1311+
let signal_str = match signal {
1312+
SignalArg::Int(n) => n.to_string(),
1313+
SignalArg::String(s) => s.clone(),
1314+
};
1315+
1316+
if !matches!(signal_str.as_str(), "SIGKILL" | "SIGTERM") {
12931317
Err(
12941318
SignalError::InvalidSignalStr(deno_signals::InvalidSignalStrError(
1295-
signal.to_string(),
1319+
signal_str,
12961320
))
12971321
.into(),
12981322
)
@@ -1325,11 +1349,11 @@ mod deprecated {
13251349
}
13261350
}
13271351

1328-
#[op2(fast, stack_trace)]
1352+
#[op2(stack_trace)]
13291353
pub fn op_kill(
13301354
state: &mut OpState,
13311355
#[smi] pid: i32,
1332-
#[string] signal: String,
1356+
#[serde] signal: SignalArg,
13331357
#[string] api_name: String,
13341358
) -> Result<(), ProcessError> {
13351359
state

tests/unit/command_test.ts

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -394,6 +394,55 @@ Deno.test(
394394
},
395395
);
396396

397+
Deno.test(
398+
{
399+
permissions: { run: true, read: true },
400+
ignore: Deno.build.os === "windows",
401+
},
402+
async function commandKillWithIntegerSignal() {
403+
const command = new Deno.Command(Deno.execPath(), {
404+
args: ["eval", "setTimeout(() => {}, 10000)"],
405+
stdout: "null",
406+
stderr: "null",
407+
});
408+
const child = command.spawn();
409+
410+
// Kill with integer signal (9 = SIGKILL)
411+
child.kill(9);
412+
const status = await child.status;
413+
414+
assertEquals(status.success, false);
415+
assertEquals(status.code, 137);
416+
assertEquals(status.signal, "SIGKILL");
417+
},
418+
);
419+
420+
Deno.test(
421+
{
422+
permissions: { run: true, read: true },
423+
ignore: Deno.build.os === "windows",
424+
},
425+
async function commandKillWithSignalZero() {
426+
const command = new Deno.Command(Deno.execPath(), {
427+
args: ["eval", "setTimeout(() => {}, 10000)"],
428+
stdout: "null",
429+
stderr: "null",
430+
});
431+
const child = command.spawn();
432+
433+
// Signal 0 checks if the process exists
434+
child.kill(0); // Should not actually kill the process
435+
436+
// Now kill it for real
437+
child.kill("SIGTERM");
438+
const status = await child.status;
439+
440+
assertEquals(status.success, false);
441+
assertEquals(status.code, 143);
442+
assertEquals(status.signal, "SIGTERM");
443+
},
444+
);
445+
397446
Deno.test(
398447
{ permissions: { run: true, read: true } },
399448
async function commandAbort() {

tests/unit/signal_test.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -308,3 +308,22 @@ Deno.test(
308308
Deno.removeSignalListener("SIGPOLL", i);
309309
},
310310
);
311+
312+
Deno.test(
313+
{
314+
ignore: Deno.build.os === "windows",
315+
permissions: { run: true },
316+
},
317+
function killWithSignalZero() {
318+
// This should not throw for the current process
319+
Deno.kill(Deno.pid, 0);
320+
321+
// Test with a non-existent PID (very high number unlikely to exist)
322+
assertThrows(
323+
() => {
324+
Deno.kill(999999, 0);
325+
},
326+
Deno.errors.NotFound,
327+
);
328+
},
329+
);

0 commit comments

Comments
 (0)