Repository: denoland/deno
Pull request: feat(process): support integer signals in Deno.kill and child.kill

Solution for https://github.com/denoland/deno/issues/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))

```

Changed files:
filename: cli/tsc/dts/lib.deno.ns.d.ts
patch: @@ -3796,13 +3796,13 @@ declare namespace Deno {
     /** Waits for the child to exit completely, returning all its output and
      * status. */
     output(): Promise<CommandOutput>;
-    /** Kills the process with given {@linkcode Deno.Signal}.
+    /** Kills the process with given {@linkcode Deno.Signal} or numeric signal.
      *
      * Defaults to `SIGTERM` if no signal is provided.
      *
      * @param [signo="SIGTERM"]
      */
-    kill(signo?: Signal): void;
+    kill(signo?: Signal | number): void;
 
     /** Ensure that the status of the child process prevents the Deno process
      * from exiting. */
@@ -4739,12 +4739,14 @@ declare namespace Deno {
    * Deno.kill(child.pid, "SIGINT");
    * ```
    *
+   * As a special case, a signal of 0 can be used to test for the existence of a process.
+   *
    * Requires `allow-run` permission.
    *
    * @tags allow-run
    * @category Subprocess
    */
-  export function kill(pid: number, signo?: Signal): void;
+  export function kill(pid: number, signo?: Signal | number): void;
 
   /** The type of the resource record to resolve via DNS using
    * {@linkcode Deno.resolveDns}.

filename: ext/process/lib.rs
patch: @@ -1047,11 +1047,18 @@ fn op_spawn_sync(
   })
 }
 
-#[op2(fast)]
+#[derive(serde::Deserialize)]
+#[serde(untagged)]
+enum SignalArg {
+  String(String),
+  Int(i32),
+}
+
+#[op2(stack_trace)]
 fn op_spawn_kill(
   state: &mut OpState,
   #[smi] rid: ResourceId,
-  #[string] signal: String,
+  #[serde] signal: SignalArg,
 ) -> Result<(), ProcessError> {
   if let Ok(child_resource) = state.resource_table.get::<ChildResource>(rid) {
     deprecated::kill(child_resource.1 as i32, &signal)?;
@@ -1262,20 +1269,32 @@ mod deprecated {
   }
 
   #[cfg(unix)]
-  pub fn kill(pid: i32, signal: &str) -> Result<(), ProcessError> {
-    let signo = deno_signals::signal_str_to_int(signal)
-      .map_err(SignalError::InvalidSignalStr)?;
+  pub fn kill(pid: i32, signal: &SignalArg) -> Result<(), ProcessError> {
+    let signo = match signal {
+      SignalArg::Int(n) => *n,
+      SignalArg::String(s) => deno_signals::signal_str_to_int(s)
+        .map_err(SignalError::InvalidSignalStr)?,
+    };
     use nix::sys::signal::Signal;
     use nix::sys::signal::kill as unix_kill;
     use nix::unistd::Pid;
-    let sig =
-      Signal::try_from(signo).map_err(|e| ProcessError::Nix(JsNixError(e)))?;
-    unix_kill(Pid::from_raw(pid), Some(sig))
+
+    // Signal 0 is special, it checks if the process exists without sending a signal
+    let sig = if signo == 0 {
+      None
+    } else {
+      Some(
+        Signal::try_from(signo)
+          .map_err(|e| ProcessError::Nix(JsNixError(e)))?,
+      )
+    };
+
+    unix_kill(Pid::from_raw(pid), sig)
       .map_err(|e| ProcessError::Nix(JsNixError(e)))
   }
 
   #[cfg(not(unix))]
-  pub fn kill(pid: i32, signal: &str) -> Result<(), ProcessError> {
+  pub fn kill(pid: i32, signal: &SignalArg) -> Result<(), ProcessError> {
     use std::io::Error;
     use std::io::ErrorKind::NotFound;
 
@@ -1289,10 +1308,15 @@ mod deprecated {
     use winapi::um::processthreadsapi::TerminateProcess;
     use winapi::um::winnt::PROCESS_TERMINATE;
 
-    if !matches!(signal, "SIGKILL" | "SIGTERM") {
+    let signal_str = match signal {
+      SignalArg::Int(n) => n.to_string(),
+      SignalArg::String(s) => s.clone(),
+    };
+
+    if !matches!(signal_str.as_str(), "SIGKILL" | "SIGTERM") {
       Err(
         SignalError::InvalidSignalStr(deno_signals::InvalidSignalStrError(
-          signal.to_string(),
+          signal_str,
         ))
         .into(),
       )
@@ -1325,11 +1349,11 @@ mod deprecated {
     }
   }
 
-  #[op2(fast, stack_trace)]
+  #[op2(stack_trace)]
   pub fn op_kill(
     state: &mut OpState,
     #[smi] pid: i32,
-    #[string] signal: String,
+    #[serde] signal: SignalArg,
     #[string] api_name: String,
   ) -> Result<(), ProcessError> {
     state

filename: tests/unit/command_test.ts
patch: @@ -394,6 +394,55 @@ Deno.test(
   },
 );
 
+Deno.test(
+  {
+    permissions: { run: true, read: true },
+    ignore: Deno.build.os === "windows",
+  },
+  async function commandKillWithIntegerSignal() {
+    const command = new Deno.Command(Deno.execPath(), {
+      args: ["eval", "setTimeout(() => {}, 10000)"],
+      stdout: "null",
+      stderr: "null",
+    });
+    const child = command.spawn();
+
+    // Kill with integer signal (9 = SIGKILL)
+    child.kill(9);
+    const status = await child.status;
+
+    assertEquals(status.success, false);
+    assertEquals(status.code, 137);
+    assertEquals(status.signal, "SIGKILL");
+  },
+);
+
+Deno.test(
+  {
+    permissions: { run: true, read: true },
+    ignore: Deno.build.os === "windows",
+  },
+  async function commandKillWithSignalZero() {
+    const command = new Deno.Command(Deno.execPath(), {
+      args: ["eval", "setTimeout(() => {}, 10000)"],
+      stdout: "null",
+      stderr: "null",
+    });
+    const child = command.spawn();
+
+    // Signal 0 checks if the process exists
+    child.kill(0); // Should not actually kill the process
+
+    // Now kill it for real
+    child.kill("SIGTERM");
+    const status = await child.status;
+
+    assertEquals(status.success, false);
+    assertEquals(status.code, 143);
+    assertEquals(status.signal, "SIGTERM");
+  },
+);
+
 Deno.test(
   { permissions: { run: true, read: true } },
   async function commandAbort() {

filename: tests/unit/signal_test.ts
patch: @@ -308,3 +308,22 @@ Deno.test(
     Deno.removeSignalListener("SIGPOLL", i);
   },
 );
+
+Deno.test(
+  {
+    ignore: Deno.build.os === "windows",
+    permissions: { run: true },
+  },
+  function killWithSignalZero() {
+    // This should not throw for the current process
+    Deno.kill(Deno.pid, 0);
+
+    // Test with a non-existent PID (very high number unlikely to exist)
+    assertThrows(
+      () => {
+        Deno.kill(999999, 0);
+      },
+      Deno.errors.NotFound,
+    );
+  },
+);