diff --git a/radicle-node/Cargo.toml b/radicle-node/Cargo.toml index c039a85d..9af20f70 100644 --- a/radicle-node/Cargo.toml +++ b/radicle-node/Cargo.toml @@ -19,13 +19,13 @@ crossbeam-channel = { version = "0.5.6" } cyphernet = { version = "0.1.0", features = ["tor", "dns", "ed25519", "p2p-ed25519"] } fastrand = { version = "1.8.0" } git-ref-format = { version = "0.2", features = ["serde", "macro"] } +io-reactor = { version = "0.1.0", features = ["popol"] } lexopt = { version = "0.2.1" } libc = { version = "0.2.137" } log = { version = "0.4.17", features = ["std"] } localtime = { version = "1" } netservices = { version = "0.1.0", features = ["io-reactor", "socket2"] } nonempty = { version = "0.8.1", features = ["serialize"] } -io-reactor = { version = "0.1.0", features = ["popol"] } qcheck = { version = "1", default-features = false, optional = true } sqlite = { version = "0.30.3" } sqlite3-src = { version = "0.4.0", features = ["bundled"] } # Ensures static linking diff --git a/radicle-node/src/lib.rs b/radicle-node/src/lib.rs index 5c8fd57a..193b34ce 100644 --- a/radicle-node/src/lib.rs +++ b/radicle-node/src/lib.rs @@ -6,6 +6,7 @@ pub mod deserializer; pub mod logger; pub mod runtime; pub mod service; +pub mod signals; pub mod sql; #[cfg(any(test, feature = "test"))] pub mod test; diff --git a/radicle-node/src/main.rs b/radicle-node/src/main.rs index 414b6571..2560374e 100644 --- a/radicle-node/src/main.rs +++ b/radicle-node/src/main.rs @@ -1,6 +1,7 @@ use std::{env, net, process}; use anyhow::Context as _; +use crossbeam_channel as chan; use cyphernet::addr::PeerAddr; use localtime::LocalDuration; @@ -8,7 +9,7 @@ use radicle::profile; use radicle_node::crypto::ssh::keystore::{Keystore, MemorySigner}; use radicle_node::prelude::{Address, NodeId}; use radicle_node::Runtime; -use radicle_node::{logger, service}; +use radicle_node::{logger, service, signals}; pub const HELP_MSG: &str = r#" Usage @@ -117,7 +118,10 @@ fn execute() -> anyhow::Result<()> { ) }); - Runtime::init(home, config, options.listen, proxy, daemon, signer)?.run()?; + let (notify, signals) = chan::bounded(1); + signals::install(notify)?; + + Runtime::init(home, config, options.listen, proxy, daemon, signals, signer)?.run()?; Ok(()) } diff --git a/radicle-node/src/runtime.rs b/radicle-node/src/runtime.rs index 78473508..dda63a53 100644 --- a/radicle-node/src/runtime.rs +++ b/radicle-node/src/runtime.rs @@ -9,6 +9,7 @@ use crossbeam_channel as chan; use cyphernet::{Cert, EcSign}; use netservices::resource::NetAccept; use radicle::git; +use radicle::node::Handle as _; use radicle::profile::Home; use radicle::Storage; use reactor::poller::popol; @@ -77,6 +78,7 @@ pub struct Runtime { pub daemon: net::SocketAddr, pub pool: worker::Pool, pub local_addrs: Vec, + pub signals: chan::Receiver<()>, } impl Runtime { @@ -89,6 +91,7 @@ impl Runtime { listen: Vec, proxy: net::SocketAddr, daemon: net::SocketAddr, + signals: chan::Receiver<()>, signer: G, ) -> Result, Error> where @@ -176,6 +179,7 @@ impl Runtime { daemon, handle, pool, + signals, local_addrs, }) } @@ -195,9 +199,18 @@ impl Runtime { return Err(err.into()); } }; - let control = thread::Builder::new() + let control = thread::Builder::new().name(self.id.to_human()).spawn({ + let handle = self.handle.clone(); + move || control::listen(listener, handle) + })?; + let _signals = thread::Builder::new() .name(self.id.to_human()) - .spawn(move || control::listen(listener, self.handle))?; + .spawn(move || { + if let Ok(()) = self.signals.recv() { + log::info!(target: "node", "Termination signal received; shutting down.."); + self.handle.shutdown().ok(); + } + })?; log::info!(target: "node", "Spawning git daemon at {}..", self.storage.path().display()); diff --git a/radicle-node/src/signals.rs b/radicle-node/src/signals.rs new file mode 100644 index 00000000..d079b227 --- /dev/null +++ b/radicle-node/src/signals.rs @@ -0,0 +1,55 @@ +use std::io; +use std::sync::Mutex; + +use crossbeam_channel as chan; + +/// Signal notifications are sent via this channel. +static NOTIFY: Mutex>> = Mutex::new(None); + +/// Install global signal handlers for `SIGTERM` and `SIGINT`. +pub fn install(notify: chan::Sender<()>) -> io::Result<()> { + if let Ok(mut channel) = NOTIFY.try_lock() { + if channel.is_some() { + return Err(io::Error::new( + io::ErrorKind::AlreadyExists, + "signal handler is already installed", + )); + } + *channel = Some(notify); + + unsafe { _install() }?; + } else { + return Err(io::Error::new( + io::ErrorKind::WouldBlock, + "unable to install signal handler", + )); + } + Ok(()) +} + +/// Install global signal handlers for `SIGTERM` and `SIGINT`. +/// +/// # Safety +/// +/// Calls `libc` functions safely. +unsafe fn _install() -> io::Result<()> { + if libc::signal(libc::SIGTERM, handler as libc::sighandler_t) == libc::SIG_ERR { + return Err(io::Error::last_os_error()); + } + if libc::signal(libc::SIGINT, handler as libc::sighandler_t) == libc::SIG_ERR { + return Err(io::Error::last_os_error()); + } + Ok(()) +} + +/// Called by `libc` when a signal is received. +extern "C" fn handler(sig: libc::c_int, _info: *mut libc::siginfo_t, _data: *mut libc::c_void) { + if sig != libc::SIGTERM && sig != libc::SIGINT { + return; + } + if let Ok(guard) = NOTIFY.try_lock() { + if let Some(c) = &*guard { + c.try_send(()).ok(); + } + } +} diff --git a/radicle-node/src/test/environment.rs b/radicle-node/src/test/environment.rs index 0441a0ed..3c2bb74e 100644 --- a/radicle-node/src/test/environment.rs +++ b/radicle-node/src/test/environment.rs @@ -6,6 +6,8 @@ use std::{ time::Duration, }; +use crossbeam_channel as chan; + use radicle::crypto::ssh::{keystore::MemorySigner, Keystore}; use radicle::crypto::test::signer::MockSigner; use radicle::crypto::{KeyPair, Seed, Signature, Signer}; @@ -249,12 +251,14 @@ impl + Signer + Clone> Node