From 24571a395203aad7cda07ffef0ef64285351e42b Mon Sep 17 00:00:00 2001 From: Geert-Jan Zwiers Date: Mon, 13 Jun 2022 22:39:46 +0200 Subject: [PATCH] feat(runtime/signal): implement SIGINT and SIGBREAK for windows (#14694) This commit adds support for SIGINT and SIGBREAK signals on Windows platform. Co-authored-by: orange soeur --- cli/dts/lib.deno.ns.d.ts | 8 +- cli/tests/unit/signal_test.ts | 121 ++++++++++++++++++--------- runtime/ops/process.rs | 54 ++++++++++--- runtime/ops/signal.rs | 148 ++++++++++++++++++++++++---------- 4 files changed, 242 insertions(+), 89 deletions(-) diff --git a/cli/dts/lib.deno.ns.d.ts b/cli/dts/lib.deno.ns.d.ts index 9ef1370ec7..f9c999cb3b 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 4ff4d38e1a..4bb92bb31e 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,21 +198,58 @@ 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", diff --git a/runtime/ops/process.rs b/runtime/ops/process.rs index ab303e2104..bd6328ae98 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 72e4020940..95c166787c 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::AsyncRefCell; +use deno_core::CancelFuture; +use deno_core::CancelHandle; use deno_core::Extension; -#[cfg(unix)] use deno_core::OpState; -#[cfg(unix)] +use deno_core::RcRef; +use deno_core::Resource; +use deno_core::ResourceId; + +use std::borrow::Cow; 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::RcRef; -#[cfg(unix)] -use deno_core::Resource; -#[cfg(unix)] -use deno_core::ResourceId; -#[cfg(unix)] -use std::borrow::Cow; #[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 for WindowsSignal { + fn from(ctrl_c: CtrlC) -> Self { + WindowsSignal::Sigint(ctrl_c) + } +} + +#[cfg(windows)] +impl From 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, + cancel: CancelHandle, +} + +#[cfg(windows)] +impl Resource for SignalStreamResource { + fn name(&self) -> Cow { + "signal".into() + } + + fn close(self: Rc) { + self.cancel.cancel(); + } +} + #[cfg(target_os = "freebsd")] pub fn signal_str_to_int(s: &str) -> Result { 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 { + 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 { + 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>, @@ -420,6 +504,7 @@ async fn op_signal_poll( .borrow_mut() .resource_table .get::(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")) -}