node: Socket Activation with systemd

This commit is contained in:
Lorenz Leutgeb 2024-05-19 04:04:32 +02:00 committed by cloudhead
parent 6f8d75a00d
commit 723e2741f5
No known key found for this signature in database
6 changed files with 121 additions and 13 deletions

5
Cargo.lock generated
View File

@ -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"

View File

@ -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"

View File

@ -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"] }

View File

@ -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<wire::Control, popol::Poller>,
@ -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<UnixListener> {
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
// <https://github.com/lucab/libsystemd-rs/blob/b43fa5e3b5eca3e6aa16a6c2fad87220dc0ad7a0/src/activation.rs#L192-L196>
// systemd also implements such a check, see
// <https://github.com/systemd/systemd/blob/v254/src/libsystemd/sd-daemon/sd-daemon.c#L357-L398>
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<ControlSocket, Error> {
#[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()),
}
}
}

View File

@ -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"

View File

@ -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 <https://github.com/systemd/systemd/blob/v254/src/systemd/sd-daemon.h#L56>.
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:
/// - <https://www.freedesktop.org/software/systemd/man/254/sd_listen_fds_with_names.html>
/// - <https://github.com/systemd/systemd/blob/v254/src/libsystemd/sd-daemon/sd-daemon.c>
/// - <https://0pointer.de/blog/projects/socket-activation.html>
/// - <https://0pointer.de/blog/projects/socket-activation2.html>
pub fn listen_fd(name: &str) -> Result<Option<RawFd>, 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
}