use std::{
collections::HashMap,
io,
sync::{
atomic::{AtomicBool, Ordering},
Arc, Mutex, OnceLock,
},
task::{Context, Poll},
};
use crate::blocking::{current_mailbox, next_id, register_waker, unregister_waker, Mailbox};
struct Sub {
id: u64,
pending: AtomicBool,
mailbox: Arc<Mailbox>,
}
struct Registry {
subs: Mutex<HashMap<i32, Vec<Arc<Sub>>>>,
}
fn registry() -> &'static Registry {
static REGISTRY: OnceLock<Registry> = OnceLock::new();
REGISTRY.get_or_init(|| Registry {
subs: Mutex::new(HashMap::new()),
})
}
fn dispatch(signo: i32) {
let subs: Vec<Arc<Sub>> = registry()
.subs
.lock()
.unwrap_or_else(|e| e.into_inner())
.get(&signo)
.cloned()
.unwrap_or_default();
for s in subs {
s.pending.store(true, Ordering::SeqCst);
s.mailbox.complete(s.id);
}
}
fn subscribe(signo: i32) -> Arc<Sub> {
let sub = Arc::new(Sub {
id: next_id(),
pending: AtomicBool::new(false),
mailbox: current_mailbox(),
});
registry()
.subs
.lock()
.unwrap_or_else(|e| e.into_inner())
.entry(signo)
.or_default()
.push(sub.clone());
sub
}
fn unsubscribe(signo: i32, sub: &Arc<Sub>) {
if let Some(list) = registry()
.subs
.lock()
.unwrap_or_else(|e| e.into_inner())
.get_mut(&signo)
{
list.retain(|s| !Arc::ptr_eq(s, sub));
}
}
fn poll_sub(sub: &Sub, cx: &mut Context<'_>) -> Poll<()> {
register_waker(sub.id, cx.waker());
if sub.pending.swap(false, Ordering::SeqCst) {
unregister_waker(sub.id);
Poll::Ready(())
} else {
Poll::Pending
}
}
#[cfg(unix)]
mod sys {
use super::*;
use std::{
os::{fd::AsRawFd, unix::net::UnixStream as StdUnixStream},
sync::atomic::AtomicI32,
};
static WRITE_FD: AtomicI32 = AtomicI32::new(-1);
pub(super) fn ensure_thread() -> io::Result<()> {
static STARTED: OnceLock<io::Result<()>> = OnceLock::new();
let r = STARTED.get_or_init(|| {
let (read, write) = StdUnixStream::pair()?;
write.set_nonblocking(true)?;
WRITE_FD.store(write.as_raw_fd(), Ordering::SeqCst);
std::mem::forget(write);
std::thread::Builder::new()
.name("nexio-signals".into())
.spawn(move || {
use std::io::Read;
let mut read = read;
let mut byte = [0u8; 1];
loop {
match read.read(&mut byte) {
Ok(1) => dispatch(byte[0] as i32),
Ok(_) => break,
Err(e) if e.kind() == io::ErrorKind::Interrupted => {}
Err(_) => break,
}
}
})?;
Ok(())
});
match r {
Ok(()) => Ok(()),
Err(e) => Err(io::Error::new(e.kind(), e.to_string())),
}
}
pub(super) fn install(signo: i32) -> io::Result<()> {
ensure_thread()?;
static INSTALLED: OnceLock<Mutex<std::collections::HashSet<i32>>> = OnceLock::new();
let installed = INSTALLED.get_or_init(|| Mutex::new(Default::default()));
let mut set = installed.lock().unwrap_or_else(|e| e.into_inner());
if set.contains(&signo) {
return Ok(());
}
unsafe {
signal_hook_registry::register(signo, move || {
let fd = WRITE_FD.load(Ordering::SeqCst);
let byte = signo as u8;
libc::write(fd, &byte as *const u8 as *const libc::c_void, 1);
})?;
}
set.insert(signo);
Ok(())
}
}
#[cfg(unix)]
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub struct SignalKind(i32);
#[cfg(unix)]
impl SignalKind {
pub const fn hangup() -> Self {
Self(libc::SIGHUP)
}
pub const fn interrupt() -> Self {
Self(libc::SIGINT)
}
pub const fn quit() -> Self {
Self(libc::SIGQUIT)
}
pub const fn terminate() -> Self {
Self(libc::SIGTERM)
}
pub const fn alarm() -> Self {
Self(libc::SIGALRM)
}
pub const fn child() -> Self {
Self(libc::SIGCHLD)
}
pub const fn user_defined1() -> Self {
Self(libc::SIGUSR1)
}
pub const fn user_defined2() -> Self {
Self(libc::SIGUSR2)
}
pub const fn window_change() -> Self {
Self(libc::SIGWINCH)
}
pub const fn from_raw(signo: i32) -> Self {
Self(signo)
}
pub const fn as_raw_value(self) -> i32 {
self.0
}
}
#[cfg(unix)]
pub struct Signal {
signo: i32,
sub: Arc<Sub>,
}
#[cfg(unix)]
pub fn signal(kind: SignalKind) -> io::Result<Signal> {
let signo = kind.0;
if signo <= 0 || signo >= 128 {
return Err(io::Error::new(
io::ErrorKind::InvalidInput,
format!("invalid signal number {signo}"),
));
}
const FORBIDDEN: [i32; 6] = [
libc::SIGKILL,
libc::SIGSTOP,
libc::SIGILL,
libc::SIGFPE,
libc::SIGSEGV,
libc::SIGBUS,
];
if FORBIDDEN.contains(&signo) {
return Err(io::Error::new(
io::ErrorKind::InvalidInput,
format!("signal {signo} cannot be caught"),
));
}
sys::install(signo)?;
Ok(Signal {
signo,
sub: subscribe(signo),
})
}
#[cfg(unix)]
impl Signal {
pub async fn recv(&mut self) -> Option<()> {
std::future::poll_fn(|cx| poll_sub(&self.sub, cx).map(Some)).await
}
}
#[cfg(unix)]
impl Drop for Signal {
fn drop(&mut self) {
unregister_waker(self.sub.id);
unsubscribe(self.signo, &self.sub);
}
}
const CTRL_C_KEY: i32 = 2;
#[cfg(windows)]
mod sys {
use super::*;
use windows_sys::Win32::{
Foundation::{BOOL, FALSE, TRUE},
System::Console::{SetConsoleCtrlHandler, CTRL_BREAK_EVENT, CTRL_C_EVENT},
};
unsafe extern "system" fn handler(ctrl_type: u32) -> BOOL {
if ctrl_type == CTRL_C_EVENT || ctrl_type == CTRL_BREAK_EVENT {
dispatch(CTRL_C_KEY);
TRUE
} else {
FALSE
}
}
pub(super) fn install() -> io::Result<()> {
static INSTALLED: OnceLock<io::Result<()>> = OnceLock::new();
let r = INSTALLED.get_or_init(|| {
if unsafe { SetConsoleCtrlHandler(Some(handler), TRUE) } == 0 {
Err(io::Error::last_os_error())
} else {
Ok(())
}
});
match r {
Ok(()) => Ok(()),
Err(e) => Err(io::Error::new(e.kind(), e.to_string())),
}
}
}
pub async fn ctrl_c() -> io::Result<()> {
#[cfg(unix)]
{
sys::install(CTRL_C_KEY)?;
}
#[cfg(windows)]
{
sys::install()?;
}
let sub = subscribe(CTRL_C_KEY);
let result = std::future::poll_fn(|cx| poll_sub(&sub, cx)).await;
unregister_waker(sub.id);
unsubscribe(CTRL_C_KEY, &sub);
let () = result;
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{block_on, spawn, time};
use std::time::Duration;
fn raise(_kind_or_key: i32) {
#[cfg(unix)]
unsafe {
libc::raise(_kind_or_key);
}
#[cfg(windows)]
dispatch(CTRL_C_KEY);
}
#[cfg(unix)]
#[test]
fn a_signal_wakes_a_waiting_task() {
block_on(async {
let mut sig = signal(SignalKind::user_defined1()).unwrap();
let waiter = spawn(async move { sig.recv().await });
time::sleep(Duration::from_millis(20)).await;
raise(libc::SIGUSR1);
assert_eq!(waiter.await.unwrap(), Some(()));
});
}
#[cfg(unix)]
#[test]
fn repeated_signals_before_recv_coalesce_into_one() {
block_on(async {
let mut sig = signal(SignalKind::user_defined2()).unwrap();
for _ in 0..5 {
raise(libc::SIGUSR2);
}
time::sleep(Duration::from_millis(100)).await;
assert_eq!(sig.recv().await, Some(()));
let again = time::timeout(Duration::from_millis(100), sig.recv()).await;
assert!(again.is_err(), "the five signals were coalesced into one");
});
}
#[cfg(unix)]
#[test]
fn dropping_a_pending_recv_loses_nothing() {
block_on(async {
let mut sig = signal(SignalKind::hangup()).unwrap();
let r = time::timeout(Duration::from_millis(30), sig.recv()).await;
assert!(r.is_err(), "no signal yet");
raise(libc::SIGHUP);
assert_eq!(sig.recv().await, Some(()));
});
}
#[cfg(unix)]
#[test]
fn uncatchable_and_invalid_signals_are_rejected() {
block_on(async {
for raw in [libc::SIGKILL, libc::SIGSTOP, 0, -3, 500] {
let err = signal(SignalKind::from_raw(raw)).err().expect("must fail");
assert_eq!(
err.kind(),
io::ErrorKind::InvalidInput,
"signal {raw}: {err:?}"
);
}
});
}
#[cfg(unix)]
#[test]
fn every_reactor_thread_waiting_for_a_signal_is_woken() {
let ready = Arc::new(std::sync::Barrier::new(3));
let mut threads = Vec::new();
for _ in 0..2 {
let ready = ready.clone();
threads.push(std::thread::spawn(move || {
block_on(async move {
let mut sig = signal(SignalKind::user_defined1()).unwrap();
ready.wait();
sig.recv().await
})
}));
}
ready.wait();
std::thread::sleep(Duration::from_millis(50));
raise(libc::SIGUSR1);
for t in threads {
assert_eq!(t.join().unwrap(), Some(()));
}
}
#[test]
fn ctrl_c_resolves_when_interrupted() {
block_on(async {
let waiter = spawn(async { ctrl_c().await });
time::sleep(Duration::from_millis(30)).await;
#[cfg(unix)]
raise(libc::SIGINT);
#[cfg(windows)]
raise(CTRL_C_KEY);
waiter.await.unwrap().unwrap();
});
}
}