From 723e2741f54605c6404d0ff7f96cd6c14633cfb0 Mon Sep 17 00:00:00 2001 From: Lorenz Leutgeb Date: Sun, 19 May 2024 04:04:32 +0200 Subject: [PATCH] node: Socket Activation with systemd --- Cargo.lock | 5 +++ Cargo.toml | 2 ++ radicle-node/Cargo.toml | 7 ++++ radicle-node/src/runtime.rs | 72 ++++++++++++++++++++++++++++++------- radicle-systemd/Cargo.toml | 7 ++++ radicle-systemd/src/lib.rs | 41 +++++++++++++++++++++ 6 files changed, 121 insertions(+), 13 deletions(-) create mode 100644 radicle-systemd/Cargo.toml create mode 100644 radicle-systemd/src/lib.rs diff --git a/Cargo.lock b/Cargo.lock index c8fb201e..722fd56f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2080,6 +2080,7 @@ dependencies = [ "radicle-fetch", "radicle-git-ext", "radicle-signals", + "radicle-systemd", "scrypt", "serde", "serde_json", @@ -2145,6 +2146,10 @@ dependencies = [ "url", ] +[[package]] +name = "radicle-systemd" +version = "0.9.0" + [[package]] name = "radicle-term" version = "0.10.0" diff --git a/Cargo.toml b/Cargo.toml index ed3644b9..72f8c3ef 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -14,6 +14,7 @@ members = [ "radicle-ssh", "radicle-tools", "radicle-signals", + "radicle-systemd", ] default-members = [ "radicle", @@ -26,6 +27,7 @@ default-members = [ "radicle-remote-helper", "radicle-term", "radicle-signals", + "radicle-systemd", ] resolver = "2" diff --git a/radicle-node/Cargo.toml b/radicle-node/Cargo.toml index 5c6c5a8d..296feeff 100644 --- a/radicle-node/Cargo.toml +++ b/radicle-node/Cargo.toml @@ -9,6 +9,8 @@ edition = "2021" build = "build.rs" [features] +default = ["systemd"] +systemd = ["dep:radicle-systemd"] test = ["radicle/test", "radicle-crypto/test", "radicle-crypto/cyphernet", "qcheck", "snapbox"] [dependencies] @@ -55,6 +57,11 @@ version = "0" path = "../radicle-signals" version = "0" +[dependencies.radicle-systemd] +path = "../radicle-systemd" +version = "0.9.0" +optional = true + [dev-dependencies] radicle = { path = "../radicle", version = "0", features = ["test"] } radicle-crypto = { path = "../radicle-crypto", version = "0", features = ["test", "cyphernet"] } diff --git a/radicle-node/src/runtime.rs b/radicle-node/src/runtime.rs index 7f8ca3de..1b05606b 100644 --- a/radicle-node/src/runtime.rs +++ b/radicle-node/src/runtime.rs @@ -88,11 +88,19 @@ pub enum Error { GitVersion(#[from] git::VersionError), } +/// Wraps a [`UnixListener`] but tracks its origin. +pub enum ControlSocket { + /// The listener was created by binding to it. + Bound(UnixListener, PathBuf), + /// The listener was received via socket activation. + Received(UnixListener), +} + /// Holds join handles to the client threads, as well as a client handle. pub struct Runtime { pub id: NodeId, pub home: Home, - pub control: UnixListener, + pub control: ControlSocket, pub handle: Handle, pub storage: Storage, pub reactor: Reactor, @@ -256,15 +264,7 @@ impl Runtime { policies_db: home.node().join(node::POLICIES_DB_FILE), }, )?; - let control = match UnixListener::bind(home.socket()) { - Ok(sock) => sock, - Err(err) if err.kind() == io::ErrorKind::AddrInUse => { - return Err(Error::AlreadyRunning(home.socket())); - } - Err(err) => { - return Err(err.into()); - } - }; + let control = Self::bind(home.socket())?; Ok(Runtime { id, @@ -281,13 +281,16 @@ impl Runtime { pub fn run(self) -> Result<(), Error> { let home = self.home; + let (listener, remove) = match self.control { + ControlSocket::Bound(listener, path) => (listener, Some(path)), + ControlSocket::Received(listener) => (listener, None), + }; log::info!(target: "node", "Running node {} in {}..", self.id, home.path().display()); - log::info!(target: "node", "Binding control socket {}..", home.socket().display()); thread::spawn(&self.id, "control", { let handle = self.handle.clone(); - || control::listen(self.control, handle) + || control::listen(listener, handle) }); let _signals = thread::spawn(&self.id, "signals", move || loop { match self.signals.recv() { @@ -314,10 +317,53 @@ impl Runtime { // node is shutting down. // Remove control socket file, but don't freak out if it's not there anymore. - fs::remove_file(home.socket()).ok(); + remove.map(|path| fs::remove_file(path).ok()); log::debug!(target: "node", "Node shutdown completed for {}", self.id); Ok(()) } + + #[cfg(all(feature = "systemd", target_family = "unix"))] + fn receive_listener() -> Option { + use std::os::fd::FromRawFd; + match radicle_systemd::listen_fd("control") { + Ok(Some(fd)) => { + // NOTE: Here, we should make a call to [`fstat(2)`](man:fstat(2)) + // and make sure that the file descriptor we received actually + // is `AF_UNIX`. However, this requires fiddling with + // `libc` types or another dependency like `nix`, see + // + // systemd also implements such a check, see + // + Some(unsafe { + // SAFETY: We take ownership of this FD from systemd, + // which guarantees that it is open. + UnixListener::from_raw_fd(fd) + }) + } + Ok(None) => None, + Err(err) => { + log::trace!(target: "node", "Error receiving file descriptors from systemd: {err}"); + None + } + } + } + + fn bind(path: PathBuf) -> Result { + #[cfg(all(feature = "systemd", target_family = "unix"))] + { + if let Some(listener) = Self::receive_listener() { + log::info!(target: "node", "Received control socket."); + return Ok(ControlSocket::Received(listener)); + } + } + + log::info!(target: "node", "Binding control socket {}..", &path.display()); + match UnixListener::bind(&path) { + Ok(sock) => Ok(ControlSocket::Bound(sock, path)), + Err(err) if err.kind() == io::ErrorKind::AddrInUse => Err(Error::AlreadyRunning(path)), + Err(err) => Err(err.into()), + } + } } diff --git a/radicle-systemd/Cargo.toml b/radicle-systemd/Cargo.toml new file mode 100644 index 00000000..534145a0 --- /dev/null +++ b/radicle-systemd/Cargo.toml @@ -0,0 +1,7 @@ +[package] +name = "radicle-systemd" +homepage = "https://radicle.xyz" +repository = "https://app.radicle.xyz/seeds/seed.radicle.xyz/rad:z3gqcJUoA1n9HaHKufZs5FCSGazv5" +license = "MIT OR Apache-2.0" +edition = "2021" +version = "0.9.0" \ No newline at end of file diff --git a/radicle-systemd/src/lib.rs b/radicle-systemd/src/lib.rs new file mode 100644 index 00000000..00ef6340 --- /dev/null +++ b/radicle-systemd/src/lib.rs @@ -0,0 +1,41 @@ +//! Library for interaction with systemd, specialized for Radicle. + +use std::env::{remove_var, var, VarError}; +use std::os::fd::RawFd; +use std::process::id; + +const LISTEN_PID: &str = "LISTEN_PID"; +const LISTEN_FDS: &str = "LISTEN_FDS"; +const LISTEN_FDNAMES: &str = "LISTEN_FDNAMES"; + +/// Minimum file descriptor used by systemd. +/// See . +const SD_LISTEN_FDS_START: RawFd = 3; + +/// Checks whether *at most one* file descriptor with given name was passed, returning it. +/// systemd sending none, more than one, or a file descriptor with a different name, all +/// results in [`Option::None`], but errors decoding environment variables or missing +/// environment variables will error. +/// This is a specialization of [`sd_listen_fds_with_names(3)`](man:sd_listen_fds_with_names(3)). +/// See: +/// - +/// - +/// - +/// - +pub fn listen_fd(name: &str) -> Result, VarError> { + let fd = match var(LISTEN_PID) { + Err(VarError::NotPresent) => Ok(None), + Err(err) => Err(err), + Ok(pid) if pid != id().to_string() => Ok(None), + _ if var(LISTEN_FDS)? != "1" || var(LISTEN_FDNAMES).ok() != Some(name.to_string()) => { + Ok(None) + } + _ => Ok(Some(SD_LISTEN_FDS_START)), + }; + + remove_var(LISTEN_PID); + remove_var(LISTEN_FDS); + remove_var(LISTEN_FDNAMES); + + fd +}