summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--cli/dts/lib.deno.ns.d.ts8
-rw-r--r--cli/tests/unit/signal_test.ts121
-rw-r--r--runtime/ops/process.rs54
-rw-r--r--runtime/ops/signal.rs144
4 files changed, 240 insertions, 87 deletions
diff --git a/cli/dts/lib.deno.ns.d.ts b/cli/dts/lib.deno.ns.d.ts
index 9ef1370ec..f9c999cb3 100644
--- a/cli/dts/lib.deno.ns.d.ts
+++ b/cli/dts/lib.deno.ns.d.ts
@@ -2342,6 +2342,7 @@ declare namespace Deno {
export type Signal =
| "SIGABRT"
| "SIGALRM"
+ | "SIGBREAK"
| "SIGBUS"
| "SIGCHLD"
| "SIGCONT"
@@ -2382,7 +2383,7 @@ declare namespace Deno {
* });
* ```
*
- * NOTE: This functionality is not yet implemented on Windows.
+ * NOTE: On Windows only SIGINT (ctrl+c) and SIGBREAK (ctrl+break) are supported.
*/
export function addSignalListener(signal: Signal, handler: () => void): void;
@@ -2397,7 +2398,7 @@ declare namespace Deno {
* Deno.removeSignalListener("SIGTERM", listener);
* ```
*
- * NOTE: This functionality is not yet implemented on Windows.
+ * NOTE: On Windows only SIGINT (ctrl+c) and SIGBREAK (ctrl+break) are supported.
*/
export function removeSignalListener(
signal: Signal,
@@ -2937,7 +2938,8 @@ declare namespace Deno {
/** Send a signal to process under given `pid`.
*
* If `pid` is negative, the signal will be sent to the process group
- * identified by `pid`.
+ * identified by `pid`. An error will be thrown if a negative
+ * `pid` is used on Windows.
*
* ```ts
* const p = Deno.run({
diff --git a/cli/tests/unit/signal_test.ts b/cli/tests/unit/signal_test.ts
index 4ff4d38e1..4bb92bb31 100644
--- a/cli/tests/unit/signal_test.ts
+++ b/cli/tests/unit/signal_test.ts
@@ -4,89 +4,102 @@ import { assertEquals, assertThrows, deferred, delay } from "./test_util.ts";
Deno.test(
{ ignore: Deno.build.os !== "windows" },
function signalsNotImplemented() {
- assertThrows(
- () => {
- Deno.addSignalListener("SIGINT", () => {});
- },
- Error,
- "not implemented",
- );
+ const msg =
+ "Windows only supports ctrl-c (SIGINT) and ctrl-break (SIGBREAK).";
assertThrows(
() => {
Deno.addSignalListener("SIGALRM", () => {});
},
Error,
- "not implemented",
+ msg,
);
assertThrows(
() => {
Deno.addSignalListener("SIGCHLD", () => {});
},
Error,
- "not implemented",
+ msg,
);
assertThrows(
() => {
Deno.addSignalListener("SIGHUP", () => {});
},
Error,
- "not implemented",
- );
- assertThrows(
- () => {
- Deno.addSignalListener("SIGINT", () => {});
- },
- Error,
- "not implemented",
+ msg,
);
assertThrows(
() => {
Deno.addSignalListener("SIGIO", () => {});
},
Error,
- "not implemented",
+ msg,
);
assertThrows(
() => {
Deno.addSignalListener("SIGPIPE", () => {});
},
Error,
- "not implemented",
+ msg,
);
assertThrows(
() => {
Deno.addSignalListener("SIGQUIT", () => {});
},
Error,
- "not implemented",
+ msg,
);
assertThrows(
() => {
Deno.addSignalListener("SIGTERM", () => {});
},
Error,
- "not implemented",
+ msg,
);
assertThrows(
() => {
Deno.addSignalListener("SIGUSR1", () => {});
},
Error,
- "not implemented",
+ msg,
);
assertThrows(
() => {
Deno.addSignalListener("SIGUSR2", () => {});
},
Error,
- "not implemented",
+ msg,
);
assertThrows(
() => {
Deno.addSignalListener("SIGWINCH", () => {});
},
Error,
- "not implemented",
+ msg,
+ );
+ assertThrows(
+ () => Deno.addSignalListener("SIGKILL", () => {}),
+ Error,
+ msg,
+ );
+ assertThrows(
+ () => Deno.addSignalListener("SIGSTOP", () => {}),
+ Error,
+ msg,
+ );
+ assertThrows(
+ () => Deno.addSignalListener("SIGILL", () => {}),
+ Error,
+ msg,
+ );
+ assertThrows(
+ () => Deno.addSignalListener("SIGFPE", () => {}),
+ Error,
+ msg,
+ );
+ assertThrows(
+ () => Deno.addSignalListener("SIGSEGV", () => {}),
+ Error,
+ msg,
);
},
);
@@ -169,7 +182,6 @@ Deno.test(
// This tests that pending op_signal_poll doesn't block the runtime from exiting the process.
Deno.test(
{
- ignore: Deno.build.os === "windows",
permissions: { run: true, read: true },
},
async function canExitWhileListeningToSignal() {
@@ -177,7 +189,7 @@ Deno.test(
args: [
"eval",
"--unstable",
- "Deno.addSignalListener('SIGIO', () => {})",
+ "Deno.addSignalListener('SIGINT', () => {})",
],
});
assertEquals(status.code, 0);
@@ -186,23 +198,60 @@ Deno.test(
Deno.test(
{
- ignore: Deno.build.os === "windows",
+ ignore: Deno.build.os !== "windows",
permissions: { run: true },
},
- function signalInvalidHandlerTest() {
- assertThrows(() => {
- // deno-lint-ignore no-explicit-any
- Deno.addSignalListener("SIGINT", "handler" as any);
- });
- assertThrows(() => {
- // deno-lint-ignore no-explicit-any
- Deno.removeSignalListener("SIGINT", "handler" as any);
- });
+ function windowsThrowsOnNegativeProcessIdTest() {
+ assertThrows(
+ () => {
+ Deno.kill(-1, "SIGINT");
+ },
+ TypeError,
+ "Invalid process id (pid) -1 for signal SIGINT.",
+ );
},
);
Deno.test(
{
+ ignore: Deno.build.os !== "windows",
+ permissions: { run: true },
+ },
+ function noOpenSystemIdleProcessTest() {
+ let signal: Deno.Signal = "SIGKILL";
+
+ assertThrows(
+ () => {
+ Deno.kill(0, signal);
+ },
+ TypeError,
+ `Cannot use ${signal} on PID 0`,
+ );
+
+ signal = "SIGTERM";
+ assertThrows(
+ () => {
+ Deno.kill(0, signal);
+ },
+ TypeError,
+ `Cannot use ${signal} on PID 0`,
+ );
+ },
+);
+
+Deno.test(function signalInvalidHandlerTest() {
+ assertThrows(() => {
+ // deno-lint-ignore no-explicit-any
+ Deno.addSignalListener("SIGINT", "handler" as any);
+ });
+ assertThrows(() => {
+ // deno-lint-ignore no-explicit-any
+ Deno.removeSignalListener("SIGINT", "handler" as any);
+ });
+});
+
+Deno.test(
+ {
ignore: Deno.build.os === "windows",
permissions: { run: true },
},
diff --git a/runtime/ops/process.rs b/runtime/ops/process.rs
index ab303e210..bd6328ae9 100644
--- a/runtime/ops/process.rs
+++ b/runtime/ops/process.rs
@@ -301,7 +301,6 @@ pub fn kill(pid: i32, signal: &str) -> Result<(), AnyError> {
use deno_core::error::type_error;
use std::io::Error;
use std::io::ErrorKind::NotFound;
- use winapi::shared::minwindef::DWORD;
use winapi::shared::minwindef::FALSE;
use winapi::shared::minwindef::TRUE;
use winapi::shared::winerror::ERROR_INVALID_PARAMETER;
@@ -309,14 +308,46 @@ pub fn kill(pid: i32, signal: &str) -> Result<(), AnyError> {
use winapi::um::handleapi::CloseHandle;
use winapi::um::processthreadsapi::OpenProcess;
use winapi::um::processthreadsapi::TerminateProcess;
+ use winapi::um::wincon::GenerateConsoleCtrlEvent;
+ use winapi::um::wincon::{CTRL_BREAK_EVENT, CTRL_CLOSE_EVENT, CTRL_C_EVENT};
use winapi::um::winnt::PROCESS_TERMINATE;
- if !matches!(signal, "SIGKILL" | "SIGTERM") {
- Err(type_error(format!("Invalid signal: {}", signal)))
- } else if pid <= 0 {
- Err(type_error("Invalid pid"))
- } else {
- let handle = unsafe { OpenProcess(PROCESS_TERMINATE, FALSE, pid as DWORD) };
+ if pid < 0 {
+ return Err(type_error(format!(
+ "Invalid process id (pid) {} for signal {}.",
+ pid, signal
+ )));
+ }
+
+ if matches!(signal, "SIGINT" | "SIGBREAK" | "SIGHUP") {
+ let is_generated = unsafe {
+ GenerateConsoleCtrlEvent(
+ match signal {
+ "SIGINT" => CTRL_C_EVENT,
+ "SIGBREAK" => CTRL_BREAK_EVENT,
+ // Need tokio::windows::signal::CtrlClose or equivalent
+ // in signal.rs to get this working
+ "SIGHUP" => CTRL_CLOSE_EVENT,
+ _ => unreachable!(),
+ },
+ pid as u32,
+ )
+ };
+ match is_generated {
+ FALSE => {
+ Err(Error::from_raw_os_error(unsafe { GetLastError() } as i32).into())
+ }
+ TRUE => Ok(()),
+ _ => unreachable!(),
+ }
+ } else if matches!(signal, "SIGKILL" | "SIGTERM") {
+ // PID 0 = System Idle Process and can't be opened.
+ if pid == 0 {
+ return Err(type_error(format!("Cannot use {} on PID 0", signal)));
+ }
+
+ let handle = unsafe { OpenProcess(PROCESS_TERMINATE, FALSE, pid as u32) };
+
if handle.is_null() {
let err = match unsafe { GetLastError() } {
ERROR_INVALID_PARAMETER => Error::from(NotFound), // Invalid `pid`.
@@ -324,14 +355,19 @@ pub fn kill(pid: i32, signal: &str) -> Result<(), AnyError> {
};
Err(err.into())
} else {
- let r = unsafe { TerminateProcess(handle, 1) };
+ let is_terminated = unsafe { TerminateProcess(handle, 1) };
unsafe { CloseHandle(handle) };
- match r {
+ match is_terminated {
FALSE => Err(Error::last_os_error().into()),
TRUE => Ok(()),
_ => unreachable!(),
}
}
+ } else {
+ Err(type_error(format!(
+ "Signal {} is unsupported on Windows.",
+ signal
+ )))
}
}
diff --git a/runtime/ops/signal.rs b/runtime/ops/signal.rs
index 72e402094..95c166787 100644
--- a/runtime/ops/signal.rs
+++ b/runtime/ops/signal.rs
@@ -1,35 +1,24 @@
// Copyright 2018-2022 the Deno authors. All rights reserved. MIT license.
-#[cfg(not(unix))]
-use deno_core::error::generic_error;
-#[cfg(not(target_os = "windows"))]
use deno_core::error::type_error;
use deno_core::error::AnyError;
use deno_core::op;
-
-use deno_core::Extension;
-#[cfg(unix)]
-use deno_core::OpState;
-#[cfg(unix)]
-use std::cell::RefCell;
-#[cfg(unix)]
-use std::rc::Rc;
-
-#[cfg(unix)]
use deno_core::AsyncRefCell;
-#[cfg(unix)]
use deno_core::CancelFuture;
-#[cfg(unix)]
use deno_core::CancelHandle;
-#[cfg(unix)]
+use deno_core::Extension;
+use deno_core::OpState;
use deno_core::RcRef;
-#[cfg(unix)]
use deno_core::Resource;
-#[cfg(unix)]
use deno_core::ResourceId;
-#[cfg(unix)]
+
use std::borrow::Cow;
+use std::cell::RefCell;
+use std::rc::Rc;
+
#[cfg(unix)]
use tokio::signal::unix::{signal, Signal, SignalKind};
+#[cfg(windows)]
+use tokio::signal::windows::{ctrl_break, ctrl_c, CtrlBreak, CtrlC};
pub fn init() -> Extension {
Extension::builder()
@@ -60,6 +49,55 @@ impl Resource for SignalStreamResource {
}
}
+// TODO: CtrlClose could be mapped to SIGHUP but that needs a
+// tokio::windows::signal::CtrlClose type, or something from a different crate
+#[cfg(windows)]
+enum WindowsSignal {
+ Sigint(CtrlC),
+ Sigbreak(CtrlBreak),
+}
+
+#[cfg(windows)]
+impl From<CtrlC> for WindowsSignal {
+ fn from(ctrl_c: CtrlC) -> Self {
+ WindowsSignal::Sigint(ctrl_c)
+ }
+}
+
+#[cfg(windows)]
+impl From<CtrlBreak> for WindowsSignal {
+ fn from(ctrl_break: CtrlBreak) -> Self {
+ WindowsSignal::Sigbreak(ctrl_break)
+ }
+}
+
+#[cfg(windows)]
+impl WindowsSignal {
+ pub async fn recv(&mut self) -> Option<()> {
+ match self {
+ WindowsSignal::Sigint(ctrl_c) => ctrl_c.recv().await,
+ WindowsSignal::Sigbreak(ctrl_break) => ctrl_break.recv().await,
+ }
+ }
+}
+
+#[cfg(windows)]
+struct SignalStreamResource {
+ signal: AsyncRefCell<WindowsSignal>,
+ cancel: CancelHandle,
+}
+
+#[cfg(windows)]
+impl Resource for SignalStreamResource {
+ fn name(&self) -> Cow<str> {
+ "signal".into()
+ }
+
+ fn close(self: Rc<Self>) {
+ self.cancel.cancel();
+ }
+}
+
#[cfg(target_os = "freebsd")]
pub fn signal_str_to_int(s: &str) -> Result<libc::c_int, AnyError> {
match s {
@@ -389,6 +427,28 @@ pub fn signal_int_to_str(s: libc::c_int) -> Result<&'static str, AnyError> {
}
}
+#[cfg(target_os = "windows")]
+pub fn signal_str_to_int(s: &str) -> Result<libc::c_int, AnyError> {
+ match s {
+ "SIGINT" => Ok(2),
+ "SIGBREAK" => Ok(21),
+ _ => Err(type_error(
+ "Windows only supports ctrl-c (SIGINT) and ctrl-break (SIGBREAK).",
+ )),
+ }
+}
+
+#[cfg(target_os = "windows")]
+pub fn signal_int_to_str(s: libc::c_int) -> Result<&'static str, AnyError> {
+ match s {
+ 2 => Ok("SIGINT"),
+ 21 => Ok("SIGBREAK"),
+ _ => Err(type_error(
+ "Windows only supports ctrl-c (SIGINT) and ctrl-break (SIGBREAK).",
+ )),
+ }
+}
+
#[cfg(unix)]
#[op]
fn op_signal_bind(
@@ -410,7 +470,31 @@ fn op_signal_bind(
Ok(rid)
}
-#[cfg(unix)]
+#[cfg(windows)]
+#[op]
+fn op_signal_bind(
+ state: &mut OpState,
+ sig: String,
+) -> Result<ResourceId, AnyError> {
+ let signo = signal_str_to_int(&sig)?;
+ let resource = SignalStreamResource {
+ signal: AsyncRefCell::new(match signo {
+ // SIGINT
+ 2 => ctrl_c()
+ .expect("There was an issue creating ctrl+c event stream.")
+ .into(),
+ // SIGBREAK
+ 21 => ctrl_break()
+ .expect("There was an issue creating ctrl+break event stream.")
+ .into(),
+ _ => unimplemented!(),
+ }),
+ cancel: Default::default(),
+ };
+ let rid = state.resource_table.add(resource);
+ Ok(rid)
+}
+
#[op]
async fn op_signal_poll(
state: Rc<RefCell<OpState>>,
@@ -420,6 +504,7 @@ async fn op_signal_poll(
.borrow_mut()
.resource_table
.get::<SignalStreamResource>(rid)?;
+
let cancel = RcRef::map(&resource, |r| &r.cancel);
let mut signal = RcRef::map(&resource, |r| &r.signal).borrow_mut().await;
@@ -429,7 +514,6 @@ async fn op_signal_poll(
}
}
-#[cfg(unix)]
#[op]
pub fn op_signal_unbind(
state: &mut OpState,
@@ -438,21 +522,3 @@ pub fn op_signal_unbind(
state.resource_table.close(rid)?;
Ok(())
}
-
-#[cfg(not(unix))]
-#[op]
-pub fn op_signal_bind() -> Result<(), AnyError> {
- Err(generic_error("not implemented"))
-}
-
-#[cfg(not(unix))]
-#[op]
-fn op_signal_unbind() -> Result<(), AnyError> {
- Err(generic_error("not implemented"))
-}
-
-#[cfg(not(unix))]
-#[op]
-async fn op_signal_poll() -> Result<(), AnyError> {
- Err(generic_error("not implemented"))
-}