Radish alpha
h
rad:z3gqcJUoA1n9HaHKufZs5FCSGazv5
Radicle Heartwood Protocol & Stack
Radicle
Git
node: Socket Activation with systemd
Merged lorenz opened 1 year ago

This implements socket activation for radicle-node by cooperating with systemd. It is an alternative to patch b25bed2, which uses libsystemd to achieve the same goal. Only one of the two patches should be accepted.

A recording of socket activation in action, where execution of rad node status automatically starts the node: https://asciinema.org/a/R7N2N9JPvU8MMQh4LRD2FJQzW.

Note that socket activation is nothing specific to systemd. On OS X, launchd supports the same mechanism with a similar API. The JavaScript library node-socket-activation implements socket activation for both systemd and launchd.

See also:

6 files changed +121 -13 6f8d75a0 723e2741
modified Cargo.lock
@@ -2227,6 +2227,7 @@ dependencies = [
 "radicle-fetch",
 "radicle-git-ext",
 "radicle-signals",
+
 "radicle-systemd",
 "scrypt",
 "serde",
 "serde_json",
@@ -2293,6 +2294,10 @@ dependencies = [
]

[[package]]
+
name = "radicle-systemd"
+
version = "0.9.0"
+

+
[[package]]
name = "radicle-term"
version = "0.10.0"
dependencies = [
modified 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"

modified 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]
@@ -56,6 +58,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"] }
modified 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<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 || {
            if let Ok(Signal::Terminate | Signal::Interrupt) = self.signals.recv() {
@@ -303,10 +306,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()),
+
        }
+
    }
}
added 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
added 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 <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
+
}