node: Socket Activation with systemd
This commit is contained in:
parent
6f8d75a00d
commit
723e2741f5
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
||||
|
|
|
|||
|
|
@ -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"] }
|
||||
|
|
|
|||
|
|
@ -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()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
@ -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
|
||||
}
|
||||
Loading…
Reference in New Issue