rad:z3gqcJUoA1n9HaHKufZs5FCSGazv5 heartwoodd43a2af18b70985404a3c0c9be91ea55fce59d52
{
"request": "trigger",
"version": 1,
"event_type": "patch",
"repository": {
"id": "rad:z3gqcJUoA1n9HaHKufZs5FCSGazv5",
"name": "heartwood",
"description": "Radicle Heartwood Protocol & Stack",
"private": false,
"default_branch": "master",
"delegates": [
"did:key:z6MksFqXN3Yhqk8pTJdUGLwATkRfQvwZXPqR2qMEhbS9wzpT",
"did:key:z6MkireRatUThvd3qzfKht1S44wpm4FEWSSa4PRMTSQZ3voM",
"did:key:z6MkkPvBfjP4bQmco5Dm7UGsX2ruDBieEHi8n9DVJWX5sTEz"
]
},
"action": "Created",
"patch": {
"id": "b25bed2522ef91a5afdb9ee157bd16af20063cea",
"author": {
"id": "did:key:z6MkkPvBfjP4bQmco5Dm7UGsX2ruDBieEHi8n9DVJWX5sTEz",
"alias": null
},
"title": "node: Socket Activation with systemd using `libsystemd`",
"state": {
"status": "archived",
"conflicts": []
},
"before": "064ece32ac0a0bd0efe4f459dcb0462bafc236e6",
"after": "d43a2af18b70985404a3c0c9be91ea55fce59d52",
"commits": [
"d43a2af18b70985404a3c0c9be91ea55fce59d52"
],
"target": "7e1cb406bb935c3de008d2171e8961f57b4336ea",
"labels": [],
"assignees": [],
"revisions": [
{
"id": "b25bed2522ef91a5afdb9ee157bd16af20063cea",
"author": {
"id": "did:key:z6MkkPvBfjP4bQmco5Dm7UGsX2ruDBieEHi8n9DVJWX5sTEz",
"alias": null
},
"description": "This implements socket activation for `radicle-node` by cooperating with systemd. Patch `648d8d0` is an alternative implementation without using `libsystemd`.\n\nFor convenience, the [`libsystemd`](https://crates.io/crates/libsystemd) crate, which implements utilities (without linking any C code) is used. It boils down to [reading a few environment variables](https://github.com/lucab/libsystemd-rs/blob/b43fa5e3b5eca3e6aa16a6c2fad87220dc0ad7a0/src/activation.rs#L112-L150) and [converting them into raw file descriptors](https://github.com/lucab/libsystemd-rs/blob/b43fa5e3b5eca3e6aa16a6c2fad87220dc0ad7a0/src/activation.rs#L208-L230) in a [light wrapper](https://github.com/lucab/libsystemd-rs/blob/b43fa5e3b5eca3e6aa16a6c2fad87220dc0ad7a0/src/activation.rs#L37-L53).\n\nA recording of socket activation in action, where execution of `rad node status` automatically starts the node: <https://asciinema.org/a/R7N2N9JPvU8MMQh4LRD2FJQzW>.\n\nNote that socket activation is nothing specific to systemd. On OS X, `launchd` supports the same mechanism with a similar API.\nThe JavaScript library [node-socket-activation](https://github.com/sorccu/node-socket-activation) implements socket activation for both systemd and launchd.\n\nSee also:\n - [`man:sd_listen_fds(3)`](https://www.mankier.com/3/sd_listen_fds)\n - [Crate `libsystemd`](https://crates.io/crates/libsystemd)\n - [Crate `raunch`](https://crates.io/crates/raunch) (analogue for launchd)\n - <https://0pointer.net/blog/projects/systemd.html>\n - <https://0pointer.de/blog/projects/socket-activation.html>\n - <https://0pointer.de/blog/projects/socket-activation2.html>\n - <https://github.com/moby/moby/pull/3105>\n - <https://github.com/sorccu/node-socket-activation>\n - <https://mgdm.net/weblog/systemd-socket-activation/>",
"base": "a4e10f034490d2b691b3dfefaa2f1c83e3cb7c03",
"oid": "36e6fd0898ad418e4b9dcb69976e02e5940418ba",
"timestamp": 1716086501
},
{
"id": "b0f204a705607124b4b5afc47549b89d2980bdaa",
"author": {
"id": "did:key:z6MkkPvBfjP4bQmco5Dm7UGsX2ruDBieEHi8n9DVJWX5sTEz",
"alias": null
},
"description": "Rebase",
"base": "064ece32ac0a0bd0efe4f459dcb0462bafc236e6",
"oid": "3ed31a24b241cdb72b9e40ad8b1dd294ef4eaf5f",
"timestamp": 1716159439
},
{
"id": "7c4f92d4c2f8ab12031d91d6adf9af0c6a14148a",
"author": {
"id": "did:key:z6MkkPvBfjP4bQmco5Dm7UGsX2ruDBieEHi8n9DVJWX5sTEz",
"alias": null
},
"description": "",
"base": "064ece32ac0a0bd0efe4f459dcb0462bafc236e6",
"oid": "d43a2af18b70985404a3c0c9be91ea55fce59d52",
"timestamp": 1716162171
}
]
}
}
{
"response": "triggered",
"run_id": {
"id": "47af920f-3fc8-4a4e-a3b5-ea8adfdf4962"
},
"info_url": "https://cci.rad.levitte.org//47af920f-3fc8-4a4e-a3b5-ea8adfdf4962.html"
}
Started at: 2026-09-08 17:27:40.763675+02:00
Commands:
$ rad clone rad:z3gqcJUoA1n9HaHKufZs5FCSGazv5 .
✓ Creating checkout in ./...
✓ Remote cloudhead@z6MksFqXN3Yhqk8pTJdUGLwATkRfQvwZXPqR2qMEhbS9wzpT added
✓ Remote-tracking branch cloudhead@z6MksFqXN3Yhqk8pTJdUGLwATkRfQvwZXPqR2qMEhbS9wzpT/master created for z6MksFqXN3Yhqk8pTJdUGLwATkRfQvwZXPqR2qMEhbS9wzpT
✓ Remote fintohaps@z6MkireRatUThvd3qzfKht1S44wpm4FEWSSa4PRMTSQZ3voM added
✓ Remote-tracking branch fintohaps@z6MkireRatUThvd3qzfKht1S44wpm4FEWSSa4PRMTSQZ3voM/master created for z6MkireRatUThvd3qzfKht1S44wpm4FEWSSa4PRMTSQZ3voM
✓ Remote lorenz@z6MkkPvBfjP4bQmco5Dm7UGsX2ruDBieEHi8n9DVJWX5sTEz added
✓ Remote-tracking branch lorenz@z6MkkPvBfjP4bQmco5Dm7UGsX2ruDBieEHi8n9DVJWX5sTEz/master created for z6MkkPvBfjP4bQmco5Dm7UGsX2ruDBieEHi8n9DVJWX5sTEz
✓ Repository successfully cloned under /opt/radcis/ci.rad.levitte.org/cci/state/47af920f-3fc8-4a4e-a3b5-ea8adfdf4962/w/
╭────────────────────────────────────╮
│ heartwood │
│ Radicle Heartwood Protocol & Stack │
│ 189 issues · 39 patches │
╰────────────────────────────────────╯
Run `cd ./.` to go to the repository directory.
Exit code: 0
$ rad patch checkout b25bed2522ef91a5afdb9ee157bd16af20063cea
✓ Switched to branch patch/b25bed2 at revision 7c4f92d
✓ Branch patch/b25bed2 setup to track rad/patches/b25bed2522ef91a5afdb9ee157bd16af20063cea
Exit code: 0
$ git config advice.detachedHead false
Exit code: 0
$ git checkout d43a2af18b70985404a3c0c9be91ea55fce59d52
HEAD is now at d43a2af1 node: Socket Activation with systemd
Exit code: 0
$ rad patch show b25bed2522ef91a5afdb9ee157bd16af20063cea -p
╭────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮
│ Title node: Socket Activation with systemd using `libsystemd` │
│ Patch b25bed2522ef91a5afdb9ee157bd16af20063cea │
│ Author lorenz z6MkkPv…WX5sTEz │
│ Head d43a2af18b70985404a3c0c9be91ea55fce59d52 │
│ Base 064ece32ac0a0bd0efe4f459dcb0462bafc236e6 │
│ Target master │
│ Branches patch/b25bed2 │
│ Commits ahead 1, behind 1337 │
│ Status archived │
│ │
│ This implements socket activation for `radicle-node` by cooperating with │
│ systemd. Patch `648d8d0` is an alternative implementation without using │
│ `libsystemd`. │
│ │
│ For convenience, the [`libsystemd`](https://crates.io/crates/libsystemd) crate, │
│ which implements utilities (without linking any C code) is used. It boils down │
│ to [reading a few environment │
│ variables](https://github.com/lucab/libsystemd-rs/blob/b43fa5e3b5eca3e6aa16a6c2fad87220dc0ad7a0/src/activation.rs#L112-L150) │
│ and [converting them into raw file │
│ descriptors](https://github.com/lucab/libsystemd-rs/blob/b43fa5e3b5eca3e6aa16a6c2fad87220dc0ad7a0/src/activation.rs#L208-L230) │
│ in a [light │
│ wrapper](https://github.com/lucab/libsystemd-rs/blob/b43fa5e3b5eca3e6aa16a6c2fad87220dc0ad7a0/src/activation.rs#L37-L53). │
│ │
│ 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](https://github.com/sorccu/node-socket-activation) │
│ implements socket activation for both systemd and launchd. │
│ │
│ See also: │
│ - [`man:sd_listen_fds(3)`](https://www.mankier.com/3/sd_listen_fds) │
│ - [Crate `libsystemd`](https://crates.io/crates/libsystemd) │
│ - [Crate `raunch`](https://crates.io/crates/raunch) (analogue for launchd) │
│ - <https://0pointer.net/blog/projects/systemd.html> │
│ - <https://0pointer.de/blog/projects/socket-activation.html> │
│ - <https://0pointer.de/blog/projects/socket-activation2.html> │
│ - <https://github.com/moby/moby/pull/3105> │
│ - <https://github.com/sorccu/node-socket-activation> │
│ - <https://mgdm.net/weblog/systemd-socket-activation/> │
├────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┤
│ d43a2af node: Socket Activation with systemd │
├────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┤
│ ● Revision b25bed2 @ a4e10f0..36e6fd0 by lorenz z6MkkPv…WX5sTEz 2 years ago │
│ ↑ Revision b0f204a @ 064ece3..3ed31a2 by lorenz z6MkkPv…WX5sTEz 2 years ago │
│ ↑ Revision 7c4f92d @ 064ece3..d43a2af by lorenz z6MkkPv…WX5sTEz 2 years ago │
│ └─ ✓ accepted by fintohaps z6Mkire…SQZ3voM 2 years ago │
╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯
commit d43a2af18b70985404a3c0c9be91ea55fce59d52
Author: Lorenz Leutgeb <lorenz@leutgeb.xyz>
Date: Sun May 19 04:04:32 2024 +0200
node: Socket Activation with systemd
diff --git a/Cargo.lock b/Cargo.lock
index 34d11e13ec..0019827207 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -1845,6 +1845,24 @@ dependencies = [
"redox_syscall",
]
+[[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"
@@ -1938,12 +1956,27 @@ dependencies = [
"libc",
]
+[[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"
@@ -1998,6 +2031,18 @@ dependencies = [
"unicode-segmentation",
]
+[[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"
@@ -2009,6 +2054,16 @@ dependencies = [
"cyphergraphy",
]
+[[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"
@@ -2667,6 +2722,7 @@ dependencies = [
"io-reactor",
"lexopt",
"libc",
+ "libsystemd",
"localtime",
"log",
"netservices",
@@ -3821,6 +3877,15 @@ version = "0.2.1"
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"
diff --git a/radicle-node/Cargo.toml b/radicle-node/Cargo.toml
index cda98b6d10..8b4ef6c34e 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: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"] }
diff --git a/radicle-node/src/runtime.rs b/radicle-node/src/runtime.rs
index 9f94a93a59..908c4577bf 100644
--- a/radicle-node/src/runtime.rs
+++ b/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()),
+ }
+ }
}
Exit code: 0
shell: 'cargo --version rustc --version cargo fmt --check cargo clippy --all-targets --workspace -- --deny clippy::all cargo build --all-targets --workspace cargo doc --workspace cargo test --workspace --no-fail-fast '
Commands:
$ podman run --name 47af920f-3fc8-4a4e-a3b5-ea8adfdf4962 -v /opt/radcis/ci.rad.levitte.org/cci/state/47af920f-3fc8-4a4e-a3b5-ea8adfdf4962/s:/47af920f-3fc8-4a4e-a3b5-ea8adfdf4962/s:ro -v /opt/radcis/ci.rad.levitte.org/cci/state/47af920f-3fc8-4a4e-a3b5-ea8adfdf4962/w:/47af920f-3fc8-4a4e-a3b5-ea8adfdf4962/w -w /47af920f-3fc8-4a4e-a3b5-ea8adfdf4962/w -v /opt/radcis/ci.rad.levitte.org/.radicle:/${id}/.radicle:ro -e RAD_HOME=/${id}/.radicle rust:trixie bash /47af920f-3fc8-4a4e-a3b5-ea8adfdf4962/s/script.sh
+ cargo --version
info: syncing channel updates for '1.77-x86_64-unknown-linux-gnu'
info: latest update on 2024-04-09, rust version 1.77.2 (25ef9e3d8 2024-04-09)
info: downloading component 'cargo'
info: downloading component 'rust-std'
info: downloading component 'rustc'
info: installing component 'cargo'
info: installing component 'rust-std'
info: installing component 'rustc'
cargo 1.77.2 (e52e36006 2024-03-26)
+ rustc --version
rustc 1.77.2 (25ef9e3d8 2024-04-09)
+ cargo fmt --check
error: 'cargo-fmt' is not installed for the toolchain '1.77-x86_64-unknown-linux-gnu'.
To install, run `rustup component add --toolchain 1.77-x86_64-unknown-linux-gnu rustfmt`
Exit code: 1
{
"response": "finished",
"result": "failure"
}