Repository: denoland/deno
Pull request: feat(ext/signals): support additional signals on Windows

## Summary

- Add SIGTERM, SIGQUIT, SIGKILL, and SIGABRT to the Windows signal dictionary, matching libuv's signal definitions
- `Deno.addSignalListener("SIGTERM", ...)` and `Deno.addSignalListener("SIGQUIT", ...)` now work on Windows
- `Deno.kill()` on Windows now accepts SIGINT, SIGQUIT, SIGKILL, SIGTERM, SIGABRT (all map to `TerminateProcess`), plus signal 0 for process health checks
- SIGKILL and SIGABRT are forbidden for listeners (uncatchable/fatal), matching Unix behavior
- Simplified signal error messages to a cross-platform format

Closes #26331

## Test plan

- [x] Updated `tests/unit/signal_test.ts` — split the old `signalsNotImplemented` test into `windowsUnsupportedSignalsTest`, `windowsForbiddenSignalsTest`, and `windowsSupportedSignalsTest`
- [x] `./x test-unit signal` passes
- [x] Format and lint pass

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Changed files:
filename: cli/tsc/dts/lib.deno.ns.d.ts
patch: @@ -3694,8 +3694,8 @@ declare namespace Deno {
    * );
    * ```
    *
-   * _Note_: On Windows only `"SIGINT"` (CTRL+C) and `"SIGBREAK"` (CTRL+Break)
-   * are supported.
+   * _Note_: On Windows only `"SIGINT"` (CTRL+C), `"SIGBREAK"` (CTRL+Break),
+   * `"SIGTERM"`, `"SIGQUIT"`, `"SIGHUP"`, and `"SIGWINCH"` are supported.
    *
    * @category Runtime
    */
@@ -3712,8 +3712,8 @@ declare namespace Deno {
    * Deno.removeSignalListener("SIGTERM", listener);
    * ```
    *
-   * _Note_: On Windows only `"SIGINT"` (CTRL+C) and `"SIGBREAK"` (CTRL+Break)
-   * are supported.
+   * _Note_: On Windows only `"SIGINT"` (CTRL+C), `"SIGBREAK"` (CTRL+Break),
+   * `"SIGTERM"`, `"SIGQUIT"`, `"SIGHUP"`, and `"SIGWINCH"` are supported.
    *
    * @category Runtime
    */

filename: ext/process/lib.rs
patch: @@ -1370,19 +1370,56 @@ mod deprecated {
     use winapi::um::processthreadsapi::TerminateProcess;
     use winapi::um::winnt::PROCESS_TERMINATE;
 
-    let signal_str = match signal {
-      SignalArg::Int(n) => n.to_string(),
-      SignalArg::String(s) => s.clone(),
+    let signo = match signal {
+      SignalArg::Int(n) => *n,
+      SignalArg::String(s) => deno_signals::signal_str_to_int(s)
+        .map_err(SignalError::InvalidSignalStr)?,
     };
 
-    if !matches!(signal_str.as_str(), "SIGKILL" | "SIGTERM") {
-      Err(
+    if signo == 0 {
+      // Signal 0 is a health check: verify the process is still alive.
+      // SAFETY: winapi call
+      let handle = unsafe {
+        OpenProcess(
+          winapi::um::winnt::PROCESS_QUERY_LIMITED_INFORMATION,
+          FALSE,
+          pid as DWORD,
+        )
+      };
+      if handle.is_null() {
+        return Err(Error::from(NotFound).into());
+      }
+      let mut status: DWORD = 0;
+      // SAFETY: winapi call
+      let alive = unsafe {
+        winapi::um::processthreadsapi::GetExitCodeProcess(handle, &mut status)
+          != FALSE
+          && status == 259 // STILL_ACTIVE
+      };
+      // SAFETY: winapi call
+      unsafe {
+        CloseHandle(handle);
+      }
+      if alive {
+        return Ok(());
+      } else {
+        return Err(Error::from(NotFound).into());
+      }
+    }
+
+    // On Windows, SIGINT/SIGTERM/SIGKILL/SIGQUIT/SIGABRT all result in
+    // process termination via TerminateProcess, matching libuv behavior.
+    // SIGABRT is 22 on Windows (CRT), unlike 6 on Unix.
+    if !matches!(signo, 2 | 3 | 9 | 15 | 22) {
+      return Err(
         SignalError::InvalidSignalStr(deno_signals::InvalidSignalStrError(
-          signal_str,
+          format!("{signo}"),
         ))
         .into(),
-      )
-    } else if pid <= 0 {
+      );
+    }
+
+    if pid <= 0 {
       Err(ProcessError::InvalidPid)
     } else {
       let handle =

filename: ext/signals/dict.rs
patch: @@ -1,41 +1,9 @@
 // Copyright 2018-2026 the Deno authors. MIT license.
 
-#[cfg(target_os = "windows")]
-#[derive(Debug, thiserror::Error)]
-#[error(
-  "Windows only supports ctrl-c (SIGINT), ctrl-break (SIGBREAK), ctrl-close (SIGHUP), and SIGWINCH, but got {0}"
-)]
-pub struct InvalidSignalStrError(pub String);
-
-#[cfg(any(
-  target_os = "android",
-  target_os = "linux",
-  target_os = "freebsd",
-  target_os = "openbsd",
-  target_os = "macos",
-  target_os = "solaris",
-  target_os = "illumos"
-))]
 #[derive(Debug, thiserror::Error)]
 #[error("Invalid signal: {0}")]
 pub struct InvalidSignalStrError(pub String);
 
-#[cfg(target_os = "windows")]
-#[derive(Debug, thiserror::Error)]
-#[error(
-  "Windows only supports ctrl-c (SIGINT), ctrl-break (SIGBREAK), ctrl-close (SIGHUP), and SIGWINCH, but got {0}"
-)]
-pub struct InvalidSignalIntError(pub libc::c_int);
-
-#[cfg(any(
-  target_os = "android",
-  target_os = "linux",
-  target_os = "freebsd",
-  target_os = "openbsd",
-  target_os = "macos",
-  target_os = "solaris",
-  target_os = "illumos"
-))]
 #[derive(Debug, thiserror::Error)]
 #[error("Invalid signal: {0}")]
 pub struct InvalidSignalIntError(pub libc::c_int);
@@ -260,6 +228,10 @@ signal_dict!(
 signal_dict!(
   (1, "SIGHUP"),
   (2, "SIGINT"),
+  (3, "SIGQUIT"),
+  (9, "SIGKILL"),
+  (15, "SIGTERM"),
   (21, "SIGBREAK"),
+  (22, "SIGABRT"),
   (28, "SIGWINCH")
 );

filename: ext/signals/lib.rs
patch: @@ -246,7 +246,18 @@ pub fn run_exit() {
 }
 
 pub fn is_forbidden(signo: i32) -> bool {
-  FORBIDDEN.contains(&signo)
+  if FORBIDDEN.contains(&signo) {
+    return true;
+  }
+  // On Windows, signal_hook's FORBIDDEN list doesn't include SIGKILL/SIGABRT
+  // (they're Unix-specific in the crate). Add them here since listening for
+  // uncatchable/fatal signals doesn't make sense on any platform.
+  #[cfg(windows)]
+  if signo == 9 || signo == 22 {
+    // SIGKILL (9) and SIGABRT (22, Windows CRT value)
+    return true;
+  }
+  false
 }
 
 pub struct SignalStream {

filename: tests/unit/signal_test.ts
patch: @@ -3,93 +3,69 @@ import { assertEquals, assertThrows, delay } from "./test_util.ts";
 
 Deno.test(
   { ignore: Deno.build.os !== "windows" },
-  function signalsNotImplemented() {
-    const msg =
-      "Windows only supports ctrl-c (SIGINT), ctrl-break (SIGBREAK), ctrl-close (SIGHUP), and SIGWINCH, but got ";
-    assertThrows(
-      () => {
-        Deno.addSignalListener("SIGALRM", () => {});
-      },
-      Error,
-      msg + "SIGALRM",
-    );
-    assertThrows(
-      () => {
-        Deno.addSignalListener("SIGCHLD", () => {});
-      },
-      Error,
-      msg + "SIGCHLD",
-    );
-    assertThrows(
-      () => {
-        Deno.addSignalListener("SIGIO", () => {});
-      },
-      Error,
-      msg + "SIGIO",
-    );
-    assertThrows(
-      () => {
-        Deno.addSignalListener("SIGPIPE", () => {});
-      },
-      Error,
-      msg + "SIGPIPE",
-    );
-    assertThrows(
-      () => {
-        Deno.addSignalListener("SIGQUIT", () => {});
-      },
-      Error,
-      msg + "SIGQUIT",
-    );
-    assertThrows(
-      () => {
-        Deno.addSignalListener("SIGTERM", () => {});
-      },
-      Error,
-      msg + "SIGTERM",
-    );
-    assertThrows(
-      () => {
-        Deno.addSignalListener("SIGUSR1", () => {});
-      },
-      Error,
-      msg + "SIGUSR1",
-    );
-    assertThrows(
-      () => {
-        Deno.addSignalListener("SIGUSR2", () => {});
-      },
-      Error,
-      msg + "SIGUSR2",
-    );
+  function windowsUnsupportedSignalsTest() {
+    // These signals are Unix-only and not supported on Windows at all.
+    const unsupported = [
+      "SIGALRM",
+      "SIGCHLD",
+      "SIGFPE",
+      "SIGILL",
+      "SIGIO",
+      "SIGPIPE",
+      "SIGSEGV",
+      "SIGUSR1",
+      "SIGUSR2",
+    ];
+    for (const sig of unsupported) {
+      assertThrows(
+        () => {
+          Deno.addSignalListener(sig as Deno.Signal, () => {});
+        },
+        Error,
+        `Invalid signal: ${sig}`,
+      );
+    }
+  },
+);
+
+Deno.test(
+  { ignore: Deno.build.os !== "windows" },
+  function windowsForbiddenSignalsTest() {
+    // These signals exist on Windows but are forbidden to listen for
+    // (uncatchable/fatal signals).
     assertThrows(
       () => Deno.addSignalListener("SIGKILL", () => {}),
-      Error,
-      msg + "SIGKILL",
-    );
-    assertThrows(
-      () => Deno.addSignalListener("SIGSTOP", () => {}),
-      Error,
-      msg + "SIGSTOP",
-    );
-    assertThrows(
-      () => Deno.addSignalListener("SIGILL", () => {}),
-      Error,
-      msg + "SIGILL",
-    );
-    assertThrows(
-      () => Deno.addSignalListener("SIGFPE", () => {}),
-      Error,
-      msg + "SIGFPE",
+      TypeError,
+      "Binding to signal 'SIGKILL' is not allowed",
     );
     assertThrows(
-      () => Deno.addSignalListener("SIGSEGV", () => {}),
-      Error,
-      msg + "SIGSEGV",
+      () => Deno.addSignalListener("SIGABRT", () => {}),
+      TypeError,
+      "Binding to signal 'SIGABRT' is not allowed",
     );
   },
 );
 
+Deno.test(
+  { ignore: Deno.build.os !== "windows" },
+  function windowsSupportedSignalsTest() {
+    // These signals should be registerable on Windows.
+    const supported: Deno.Signal[] = [
+      "SIGINT",
+      "SIGBREAK",
+      "SIGHUP",
+      "SIGTERM",
+      "SIGQUIT",
+      "SIGWINCH",
+    ];
+    for (const sig of supported) {
+      const listener = () => {};
+      Deno.addSignalListener(sig, listener);
+      Deno.removeSignalListener(sig, listener);
+    }
+  },
+);
+
 Deno.test(
   {
     ignore: Deno.build.os === "windows",