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

This implements socket activation for radicle-node by cooperating with systemd. Patch 648d8d0 is an alternative implementation without using libsystemd.

For convenience, the libsystemd crate, which implements utilities (without linking any C code) is used. It boils down to reading a few environment variables and converting them into raw file descriptors in a light wrapper.

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:

3 files changed +133 -13 064ece32 d43a2af1
modified Cargo.lock
@@ -1846,6 +1846,24 @@ dependencies = [
]

[[package]]
+
name = "libsystemd"
+
version = "0.7.0"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "c592dc396b464005f78a5853555b9f240bc5378bf5221acc4e129910b2678869"
+
dependencies = [
+
 "hmac",
+
 "libc",
+
 "log",
+
 "nix",
+
 "nom",
+
 "once_cell",
+
 "serde",
+
 "sha2",
+
 "thiserror",
+
 "uuid",
+
]
+

+
[[package]]
name = "libz-sys"
version = "1.1.16"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1939,12 +1957,27 @@ dependencies = [
]

[[package]]
+
name = "memoffset"
+
version = "0.9.1"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "488016bfae457b036d996092f6cb448677611ce4449e970ceaf42695203f218a"
+
dependencies = [
+
 "autocfg",
+
]
+

+
[[package]]
name = "mime"
version = "0.3.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a"

[[package]]
+
name = "minimal-lexical"
+
version = "0.2.1"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a"
+

+
[[package]]
name = "miniz_oxide"
version = "0.7.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1999,6 +2032,18 @@ dependencies = [
]

[[package]]
+
name = "nix"
+
version = "0.27.1"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "2eb04e9c688eff1c89d72b407f168cf79bb9e867a9d3323ed6c01519eb9cc053"
+
dependencies = [
+
 "bitflags 2.5.0",
+
 "cfg-if",
+
 "libc",
+
 "memoffset",
+
]
+

+
[[package]]
name = "noise-framework"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -2010,6 +2055,16 @@ dependencies = [
]

[[package]]
+
name = "nom"
+
version = "7.1.3"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a"
+
dependencies = [
+
 "memchr",
+
 "minimal-lexical",
+
]
+

+
[[package]]
name = "nonempty"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -2667,6 +2722,7 @@ dependencies = [
 "io-reactor",
 "lexopt",
 "libc",
+
 "libsystemd",
 "localtime",
 "log",
 "netservices",
@@ -3822,6 +3878,15 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a"

[[package]]
+
name = "uuid"
+
version = "1.8.0"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "a183cf7feeba97b4dd1c0d46788634f6221d87fa961b305bed08c851829efcc0"
+
dependencies = [
+
 "serde",
+
]
+

+
[[package]]
name = "valuable"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
modified radicle-node/Cargo.toml
@@ -9,6 +9,8 @@ edition = "2021"
build = "build.rs"

[features]
+
default = ["systemd"]
+
systemd = ["dep:libsystemd"]
test = ["radicle/test", "radicle-crypto/test", "radicle-crypto/cyphernet", "qcheck", "snapbox"]

[dependencies]
@@ -25,6 +27,7 @@ gix-protocol = { version = "0.41.1", features = ["blocking-client"] }
io-reactor = { version = "0.5.1", features = ["popol"] }
lexopt = { version = "0.3.0" }
libc = { version = "0.2.137" }
+
libsystemd = { version = "0.7.0", optional = true }
log = { version = "0.4.17", features = ["std"] }
localtime = { version = "1.2.0" }
netservices = { version = "0.8.0", features = ["io-reactor", "socket2"] }
modified radicle-node/src/runtime.rs
@@ -84,11 +84,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>,
@@ -238,15 +246,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,
@@ -263,13 +263,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() {
@@ -285,10 +288,59 @@ 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 libsystemd::activation::IsType;
+
        use std::os::fd::FromRawFd;
+
        use std::os::fd::IntoRawFd;
+

+
        const NAME: &str = "control";
+

+
        match libsystemd::activation::receive_descriptors_with_names(true) {
+
            Ok(descriptors) => match descriptors
+
                .into_iter()
+
                .find_map(|(fd, name)| (name == NAME).then_some(fd))
+
            {
+
                Some(fd) if fd.is_unix() => {
+
                    let raw_fd = fd.into_raw_fd();
+
                    return Some(unsafe {
+
                        // SAFETY: We take ownership of this FD from systemd, which guarantees that it is open.
+
                        UnixListener::from_raw_fd(raw_fd)
+
                    });
+
                }
+
                Some(_) => {
+
                    log::debug!(target: "node", "Unexpected type of file descriptor from systemd.")
+
                }
+
                None => {
+
                    log::debug!(target: "node", "No file descriptor named '{NAME}' from systemd.")
+
                }
+
            },
+
            Err(err) => log::trace!(target: "node", "No file descriptors received: {err}"),
+
        }
+

+
        None
+
    }
+

+
    fn bind(path: PathBuf) -> Result<ControlSocket, Error> {
+
        if 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()),
+
        }
+
    }
}