// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license. use deno_core::op2; use deno_core::AsyncRefCell; use deno_core::CancelFuture; use deno_core::CancelHandle; use deno_core::OpState; use deno_core::RcRef; use deno_core::Resource; use deno_core::ResourceId; use std::borrow::Cow; use std::cell::RefCell; #[cfg(unix)] use std::collections::BTreeMap; use std::rc::Rc; #[cfg(unix)] use std::sync::atomic::AtomicBool; #[cfg(unix)] use std::sync::Arc; #[cfg(unix)] use tokio::signal::unix::signal; #[cfg(unix)] use tokio::signal::unix::Signal; #[cfg(unix)] use tokio::signal::unix::SignalKind; #[cfg(windows)] use tokio::signal::windows::ctrl_break; #[cfg(windows)] use tokio::signal::windows::ctrl_c; #[cfg(windows)] use tokio::signal::windows::CtrlBreak; #[cfg(windows)] use tokio::signal::windows::CtrlC; deno_core::extension!( deno_signal, ops = [op_signal_bind, op_signal_unbind, op_signal_poll], state = |state| { #[cfg(unix)] { state.put(SignalState::default()); } } ); #[derive(Debug, thiserror::Error)] pub enum SignalError { #[cfg(any( target_os = "android", target_os = "linux", target_os = "openbsd", target_os = "openbsd", target_os = "macos", target_os = "solaris", target_os = "illumos" ))] #[error("Invalid signal: {0}")] InvalidSignalStr(String), #[cfg(any( target_os = "android", target_os = "linux", target_os = "openbsd", target_os = "openbsd", target_os = "macos", target_os = "solaris", target_os = "illumos" ))] #[error("Invalid signal: {0}")] InvalidSignalInt(libc::c_int), #[cfg(target_os = "windows")] #[error("Windows only supports ctrl-c (SIGINT) and ctrl-break (SIGBREAK), but got {0}")] InvalidSignalStr(String), #[cfg(target_os = "windows")] #[error("Windows only supports ctrl-c (SIGINT) and ctrl-break (SIGBREAK), but got {0}")] InvalidSignalInt(libc::c_int), #[error("Binding to signal '{0}' is not allowed")] SignalNotAllowed(String), #[error("{0}")] Io(#[from] std::io::Error), } #[cfg(unix)] #[derive(Default)] struct SignalState { enable_default_handlers: BTreeMap>, } #[cfg(unix)] impl SignalState { /// Disable the default signal handler for the given signal. /// /// Returns the shared flag to enable the default handler later, and whether a default handler already existed. fn disable_default_handler( &mut self, signo: libc::c_int, ) -> (Arc, bool) { use std::collections::btree_map::Entry; match self.enable_default_handlers.entry(signo) { Entry::Occupied(entry) => { let enable = entry.get(); enable.store(false, std::sync::atomic::Ordering::Release); (enable.clone(), true) } Entry::Vacant(entry) => { let enable = Arc::new(AtomicBool::new(false)); entry.insert(enable.clone()); (enable, false) } } } } #[cfg(unix)] /// The resource for signal stream. /// The second element is the waker of polling future. struct SignalStreamResource { signal: AsyncRefCell, enable_default_handler: Arc, cancel: CancelHandle, } #[cfg(unix)] impl Resource for SignalStreamResource { fn name(&self) -> Cow { "signal".into() } fn close(self: Rc) { self.cancel.cancel(); } } // 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(); } } macro_rules! first_literal { ($head:literal $(, $tail:literal)*) => { $head }; } macro_rules! signal_dict { ($(($number:literal, $($name:literal)|+)),*) => { pub fn signal_str_to_int(s: &str) -> Result { match s { $($($name)|* => Ok($number),)* _ => Err(SignalError::InvalidSignalStr(s.to_string())), } } pub fn signal_int_to_str(s: libc::c_int) -> Result<&'static str, SignalError> { match s { $($number => Ok(first_literal!($($name),+)),)* _ => Err(SignalError::InvalidSignalInt(s)), } } } } #[cfg(target_os = "freebsd")] signal_dict!( (1, "SIGHUP"), (2, "SIGINT"), (3, "SIGQUIT"), (4, "SIGILL"), (5, "SIGTRAP"), (6, "SIGABRT" | "SIGIOT"), (7, "SIGEMT"), (8, "SIGFPE"), (9, "SIGKILL"), (10, "SIGBUS"), (11, "SIGSEGV"), (12, "SIGSYS"), (13, "SIGPIPE"), (14, "SIGALRM"), (15, "SIGTERM"), (16, "SIGURG"), (17, "SIGSTOP"), (18, "SIGTSTP"), (19, "SIGCONT"), (20, "SIGCHLD"), (21, "SIGTTIN"), (22, "SIGTTOU"), (23, "SIGIO"), (24, "SIGXCPU"), (25, "SIGXFSZ"), (26, "SIGVTALRM"), (27, "SIGPROF"), (28, "SIGWINCH"), (29, "SIGINFO"), (30, "SIGUSR1"), (31, "SIGUSR2"), (32, "SIGTHR"), (33, "SIGLIBRT") ); #[cfg(target_os = "openbsd")] signal_dict!( (1, "SIGHUP"), (2, "SIGINT"), (3, "SIGQUIT"), (4, "SIGILL"), (5, "SIGTRAP"), (6, "SIGABRT" | "SIGIOT"), (7, "SIGEMT"), (8, "SIGKILL"), (10, "SIGBUS"), (11, "SIGSEGV"), (12, "SIGSYS"), (13, "SIGPIPE"), (14, "SIGALRM"), (15, "SIGTERM"), (16, "SIGURG"), (17, "SIGSTOP"), (18, "SIGTSTP"), (19, "SIGCONT"), (20, "SIGCHLD"), (21, "SIGTTIN"), (22, "SIGTTOU"), (23, "SIGIO"), (24, "SIGXCPU"), (25, "SIGXFSZ"), (26, "SIGVTALRM"), (27, "SIGPROF"), (28, "SIGWINCH"), (29, "SIGINFO"), (30, "SIGUSR1"), (31, "SIGUSR2"), (32, "SIGTHR") ); #[cfg(any(target_os = "android", target_os = "linux"))] signal_dict!( (1, "SIGHUP"), (2, "SIGINT"), (3, "SIGQUIT"), (4, "SIGILL"), (5, "SIGTRAP"), (6, "SIGABRT" | "SIGIOT"), (7, "SIGBUS"), (8, "SIGFPE"), (9, "SIGKILL"), (10, "SIGUSR1"), (11, "SIGSEGV"), (12, "SIGUSR2"), (13, "SIGPIPE"), (14, "SIGALRM"), (15, "SIGTERM"), (16, "SIGSTKFLT"), (17, "SIGCHLD"), (18, "SIGCONT"), (19, "SIGSTOP"), (20, "SIGTSTP"), (21, "SIGTTIN"), (22, "SIGTTOU"), (23, "SIGURG"), (24, "SIGXCPU"), (25, "SIGXFSZ"), (26, "SIGVTALRM"), (27, "SIGPROF"), (28, "SIGWINCH"), (29, "SIGIO" | "SIGPOLL"), (30, "SIGPWR"), (31, "SIGSYS" | "SIGUNUSED") ); #[cfg(target_os = "macos")] signal_dict!( (1, "SIGHUP"), (2, "SIGINT"), (3, "SIGQUIT"), (4, "SIGILL"), (5, "SIGTRAP"), (6, "SIGABRT" | "SIGIOT"), (7, "SIGEMT"), (8, "SIGFPE"), (9, "SIGKILL"), (10, "SIGBUS"), (11, "SIGSEGV"), (12, "SIGSYS"), (13, "SIGPIPE"), (14, "SIGALRM"), (15, "SIGTERM"), (16, "SIGURG"), (17, "SIGSTOP"), (18, "SIGTSTP"), (19, "SIGCONT"), (20, "SIGCHLD"), (21, "SIGTTIN"), (22, "SIGTTOU"), (23, "SIGIO"), (24, "SIGXCPU"), (25, "SIGXFSZ"), (26, "SIGVTALRM"), (27, "SIGPROF"), (28, "SIGWINCH"), (29, "SIGINFO"), (30, "SIGUSR1"), (31, "SIGUSR2") ); #[cfg(any(target_os = "solaris", target_os = "illumos"))] signal_dict!( (1, "SIGHUP"), (2, "SIGINT"), (3, "SIGQUIT"), (4, "SIGILL"), (5, "SIGTRAP"), (6, "SIGABRT" | "SIGIOT"), (7, "SIGEMT"), (8, "SIGFPE"), (9, "SIGKILL"), (10, "SIGBUS"), (11, "SIGSEGV"), (12, "SIGSYS"), (13, "SIGPIPE"), (14, "SIGALRM"), (15, "SIGTERM"), (16, "SIGUSR1"), (17, "SIGUSR2"), (18, "SIGCHLD"), (19, "SIGPWR"), (20, "SIGWINCH"), (21, "SIGURG"), (22, "SIGPOLL"), (23, "SIGSTOP"), (24, "SIGTSTP"), (25, "SIGCONT"), (26, "SIGTTIN"), (27, "SIGTTOU"), (28, "SIGVTALRM"), (29, "SIGPROF"), (30, "SIGXCPU"), (31, "SIGXFSZ"), (32, "SIGWAITING"), (33, "SIGLWP"), (34, "SIGFREEZE"), (35, "SIGTHAW"), (36, "SIGCANCEL"), (37, "SIGLOST"), (38, "SIGXRES"), (39, "SIGJVM1"), (40, "SIGJVM2") ); #[cfg(target_os = "windows")] signal_dict!((2, "SIGINT"), (21, "SIGBREAK")); #[cfg(unix)] #[op2(fast)] #[smi] fn op_signal_bind( state: &mut OpState, #[string] sig: &str, ) -> Result { let signo = signal_str_to_int(sig)?; if signal_hook_registry::FORBIDDEN.contains(&signo) { return Err(SignalError::SignalNotAllowed(sig.to_string())); } let signal = AsyncRefCell::new(signal(SignalKind::from_raw(signo))?); let (enable_default_handler, has_default_handler) = state .borrow_mut::() .disable_default_handler(signo); let resource = SignalStreamResource { signal, cancel: Default::default(), enable_default_handler: enable_default_handler.clone(), }; let rid = state.resource_table.add(resource); if !has_default_handler { // restore default signal handler when the signal is unbound // this can error if the signal is not supported, if so let's just leave it as is let _ = signal_hook::flag::register_conditional_default( signo, enable_default_handler, ); } Ok(rid) } #[cfg(windows)] #[op2(fast)] #[smi] fn op_signal_bind( state: &mut OpState, #[string] sig: &str, ) -> 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) } #[op2(async)] async fn op_signal_poll( state: Rc>, #[smi] rid: ResourceId, ) -> Result { let resource = state .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; match signal.recv().or_cancel(cancel).await { Ok(result) => Ok(result.is_none()), Err(_) => Ok(true), } } #[op2(fast)] pub fn op_signal_unbind( state: &mut OpState, #[smi] rid: ResourceId, ) -> Result<(), deno_core::error::AnyError> { let resource = state.resource_table.take::(rid)?; #[cfg(unix)] { resource .enable_default_handler .store(true, std::sync::atomic::Ordering::Release); } resource.close(); Ok(()) }