1
0
Fork 0
mirror of https://github.com/denoland/deno.git synced 2024-11-25 15:29:32 -05:00

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 <juzi201314@gmail.com>
This commit is contained in:
Geert-Jan Zwiers 2022-06-13 22:39:46 +02:00 committed by GitHub
parent 21dfeea3c4
commit 24571a3952
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
4 changed files with 242 additions and 89 deletions

View file

@ -2342,6 +2342,7 @@ declare namespace Deno {
export type Signal = export type Signal =
| "SIGABRT" | "SIGABRT"
| "SIGALRM" | "SIGALRM"
| "SIGBREAK"
| "SIGBUS" | "SIGBUS"
| "SIGCHLD" | "SIGCHLD"
| "SIGCONT" | "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; export function addSignalListener(signal: Signal, handler: () => void): void;
@ -2397,7 +2398,7 @@ declare namespace Deno {
* Deno.removeSignalListener("SIGTERM", listener); * 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( export function removeSignalListener(
signal: Signal, signal: Signal,
@ -2937,7 +2938,8 @@ declare namespace Deno {
/** Send a signal to process under given `pid`. /** Send a signal to process under given `pid`.
* *
* If `pid` is negative, the signal will be sent to the process group * 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 * ```ts
* const p = Deno.run({ * const p = Deno.run({

View file

@ -4,89 +4,102 @@ import { assertEquals, assertThrows, deferred, delay } from "./test_util.ts";
Deno.test( Deno.test(
{ ignore: Deno.build.os !== "windows" }, { ignore: Deno.build.os !== "windows" },
function signalsNotImplemented() { function signalsNotImplemented() {
assertThrows( const msg =
() => { "Windows only supports ctrl-c (SIGINT) and ctrl-break (SIGBREAK).";
Deno.addSignalListener("SIGINT", () => {});
},
Error,
"not implemented",
);
assertThrows( assertThrows(
() => { () => {
Deno.addSignalListener("SIGALRM", () => {}); Deno.addSignalListener("SIGALRM", () => {});
}, },
Error, Error,
"not implemented", msg,
); );
assertThrows( assertThrows(
() => { () => {
Deno.addSignalListener("SIGCHLD", () => {}); Deno.addSignalListener("SIGCHLD", () => {});
}, },
Error, Error,
"not implemented", msg,
); );
assertThrows( assertThrows(
() => { () => {
Deno.addSignalListener("SIGHUP", () => {}); Deno.addSignalListener("SIGHUP", () => {});
}, },
Error, Error,
"not implemented", msg,
);
assertThrows(
() => {
Deno.addSignalListener("SIGINT", () => {});
},
Error,
"not implemented",
); );
assertThrows( assertThrows(
() => { () => {
Deno.addSignalListener("SIGIO", () => {}); Deno.addSignalListener("SIGIO", () => {});
}, },
Error, Error,
"not implemented", msg,
); );
assertThrows( assertThrows(
() => { () => {
Deno.addSignalListener("SIGPIPE", () => {}); Deno.addSignalListener("SIGPIPE", () => {});
}, },
Error, Error,
"not implemented", msg,
); );
assertThrows( assertThrows(
() => { () => {
Deno.addSignalListener("SIGQUIT", () => {}); Deno.addSignalListener("SIGQUIT", () => {});
}, },
Error, Error,
"not implemented", msg,
); );
assertThrows( assertThrows(
() => { () => {
Deno.addSignalListener("SIGTERM", () => {}); Deno.addSignalListener("SIGTERM", () => {});
}, },
Error, Error,
"not implemented", msg,
); );
assertThrows( assertThrows(
() => { () => {
Deno.addSignalListener("SIGUSR1", () => {}); Deno.addSignalListener("SIGUSR1", () => {});
}, },
Error, Error,
"not implemented", msg,
); );
assertThrows( assertThrows(
() => { () => {
Deno.addSignalListener("SIGUSR2", () => {}); Deno.addSignalListener("SIGUSR2", () => {});
}, },
Error, Error,
"not implemented", msg,
); );
assertThrows( assertThrows(
() => { () => {
Deno.addSignalListener("SIGWINCH", () => {}); Deno.addSignalListener("SIGWINCH", () => {});
}, },
Error, 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. // This tests that pending op_signal_poll doesn't block the runtime from exiting the process.
Deno.test( Deno.test(
{ {
ignore: Deno.build.os === "windows",
permissions: { run: true, read: true }, permissions: { run: true, read: true },
}, },
async function canExitWhileListeningToSignal() { async function canExitWhileListeningToSignal() {
@ -177,7 +189,7 @@ Deno.test(
args: [ args: [
"eval", "eval",
"--unstable", "--unstable",
"Deno.addSignalListener('SIGIO', () => {})", "Deno.addSignalListener('SIGINT', () => {})",
], ],
}); });
assertEquals(status.code, 0); assertEquals(status.code, 0);
@ -186,21 +198,58 @@ Deno.test(
Deno.test( Deno.test(
{ {
ignore: Deno.build.os === "windows", ignore: Deno.build.os !== "windows",
permissions: { run: true }, permissions: { run: true },
}, },
function signalInvalidHandlerTest() { function windowsThrowsOnNegativeProcessIdTest() {
assertThrows(() => { assertThrows(
// deno-lint-ignore no-explicit-any () => {
Deno.addSignalListener("SIGINT", "handler" as any); Deno.kill(-1, "SIGINT");
}); },
assertThrows(() => { TypeError,
// deno-lint-ignore no-explicit-any "Invalid process id (pid) -1 for signal SIGINT.",
Deno.removeSignalListener("SIGINT", "handler" as any); );
});
}, },
); );
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( Deno.test(
{ {
ignore: Deno.build.os === "windows", ignore: Deno.build.os === "windows",

View file

@ -301,7 +301,6 @@ pub fn kill(pid: i32, signal: &str) -> Result<(), AnyError> {
use deno_core::error::type_error; use deno_core::error::type_error;
use std::io::Error; use std::io::Error;
use std::io::ErrorKind::NotFound; use std::io::ErrorKind::NotFound;
use winapi::shared::minwindef::DWORD;
use winapi::shared::minwindef::FALSE; use winapi::shared::minwindef::FALSE;
use winapi::shared::minwindef::TRUE; use winapi::shared::minwindef::TRUE;
use winapi::shared::winerror::ERROR_INVALID_PARAMETER; 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::handleapi::CloseHandle;
use winapi::um::processthreadsapi::OpenProcess; use winapi::um::processthreadsapi::OpenProcess;
use winapi::um::processthreadsapi::TerminateProcess; 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; use winapi::um::winnt::PROCESS_TERMINATE;
if !matches!(signal, "SIGKILL" | "SIGTERM") { if pid < 0 {
Err(type_error(format!("Invalid signal: {}", signal))) return Err(type_error(format!(
} else if pid <= 0 { "Invalid process id (pid) {} for signal {}.",
Err(type_error("Invalid pid")) pid, signal
} else { )));
let handle = unsafe { OpenProcess(PROCESS_TERMINATE, FALSE, pid as DWORD) }; }
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() { if handle.is_null() {
let err = match unsafe { GetLastError() } { let err = match unsafe { GetLastError() } {
ERROR_INVALID_PARAMETER => Error::from(NotFound), // Invalid `pid`. ERROR_INVALID_PARAMETER => Error::from(NotFound), // Invalid `pid`.
@ -324,14 +355,19 @@ pub fn kill(pid: i32, signal: &str) -> Result<(), AnyError> {
}; };
Err(err.into()) Err(err.into())
} else { } else {
let r = unsafe { TerminateProcess(handle, 1) }; let is_terminated = unsafe { TerminateProcess(handle, 1) };
unsafe { CloseHandle(handle) }; unsafe { CloseHandle(handle) };
match r { match is_terminated {
FALSE => Err(Error::last_os_error().into()), FALSE => Err(Error::last_os_error().into()),
TRUE => Ok(()), TRUE => Ok(()),
_ => unreachable!(), _ => unreachable!(),
} }
} }
} else {
Err(type_error(format!(
"Signal {} is unsupported on Windows.",
signal
)))
} }
} }

View file

@ -1,35 +1,24 @@
// Copyright 2018-2022 the Deno authors. All rights reserved. MIT license. // 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::type_error;
use deno_core::error::AnyError; use deno_core::error::AnyError;
use deno_core::op; use deno_core::op;
use deno_core::AsyncRefCell;
use deno_core::CancelFuture;
use deno_core::CancelHandle;
use deno_core::Extension; use deno_core::Extension;
#[cfg(unix)]
use deno_core::OpState; 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; use std::cell::RefCell;
#[cfg(unix)]
use std::rc::Rc; 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)] #[cfg(unix)]
use tokio::signal::unix::{signal, Signal, SignalKind}; use tokio::signal::unix::{signal, Signal, SignalKind};
#[cfg(windows)]
use tokio::signal::windows::{ctrl_break, ctrl_c, CtrlBreak, CtrlC};
pub fn init() -> Extension { pub fn init() -> Extension {
Extension::builder() 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")] #[cfg(target_os = "freebsd")]
pub fn signal_str_to_int(s: &str) -> Result<libc::c_int, AnyError> { pub fn signal_str_to_int(s: &str) -> Result<libc::c_int, AnyError> {
match s { 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)] #[cfg(unix)]
#[op] #[op]
fn op_signal_bind( fn op_signal_bind(
@ -410,7 +470,31 @@ fn op_signal_bind(
Ok(rid) 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] #[op]
async fn op_signal_poll( async fn op_signal_poll(
state: Rc<RefCell<OpState>>, state: Rc<RefCell<OpState>>,
@ -420,6 +504,7 @@ async fn op_signal_poll(
.borrow_mut() .borrow_mut()
.resource_table .resource_table
.get::<SignalStreamResource>(rid)?; .get::<SignalStreamResource>(rid)?;
let cancel = RcRef::map(&resource, |r| &r.cancel); let cancel = RcRef::map(&resource, |r| &r.cancel);
let mut signal = RcRef::map(&resource, |r| &r.signal).borrow_mut().await; let mut signal = RcRef::map(&resource, |r| &r.signal).borrow_mut().await;
@ -429,7 +514,6 @@ async fn op_signal_poll(
} }
} }
#[cfg(unix)]
#[op] #[op]
pub fn op_signal_unbind( pub fn op_signal_unbind(
state: &mut OpState, state: &mut OpState,
@ -438,21 +522,3 @@ pub fn op_signal_unbind(
state.resource_table.close(rid)?; state.resource_table.close(rid)?;
Ok(()) 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"))
}