rad:z3gqcJUoA1n9HaHKufZs5FCSGazv5 heartwood5dd479cf66d9e51a975baa8a3c6142aa9acddbe8
{
"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": "648d8d0b90a2ba7b938d30e6037205851f62ffb7",
"author": {
"id": "did:key:z6MkkPvBfjP4bQmco5Dm7UGsX2ruDBieEHi8n9DVJWX5sTEz",
"alias": null
},
"title": "node: Socket Activation with systemd",
"state": {
"status": "merged",
"conflicts": []
},
"before": "297646412ad38be27ab515245066b3783d83736f",
"after": "5dd479cf66d9e51a975baa8a3c6142aa9acddbe8",
"commits": [
"5dd479cf66d9e51a975baa8a3c6142aa9acddbe8"
],
"target": "3419821106e274c96f56948a05d2f51f28f5a6c7",
"labels": [],
"assignees": [],
"revisions": [
{
"id": "648d8d0b90a2ba7b938d30e6037205851f62ffb7",
"author": {
"id": "did:key:z6MkkPvBfjP4bQmco5Dm7UGsX2ruDBieEHi8n9DVJWX5sTEz",
"alias": null
},
"description": "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.\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": "064ece32ac0a0bd0efe4f459dcb0462bafc236e6",
"oid": "0dc2c39d05edbaadf5a2bf6c11da5b3a076e416d",
"timestamp": 1716159679
},
{
"id": "d4ea40c76e3830f318be464749c8e99e3a324a5b",
"author": {
"id": "did:key:z6MkkPvBfjP4bQmco5Dm7UGsX2ruDBieEHi8n9DVJWX5sTEz",
"alias": null
},
"description": "",
"base": "064ece32ac0a0bd0efe4f459dcb0462bafc236e6",
"oid": "c744407ec287fe052b3ea4198b7b1b2487d008ab",
"timestamp": 1716160097
},
{
"id": "0ffaa035a19d84e7de5b04c1c82f83e98a86f8e3",
"author": {
"id": "did:key:z6MkkPvBfjP4bQmco5Dm7UGsX2ruDBieEHi8n9DVJWX5sTEz",
"alias": null
},
"description": "",
"base": "064ece32ac0a0bd0efe4f459dcb0462bafc236e6",
"oid": "a26956c623b7907a0304810c1c1b2b522916a01a",
"timestamp": 1716162516
},
{
"id": "8d74709053ff75362790af7fe606038d87b2cd22",
"author": {
"id": "did:key:z6MkkPvBfjP4bQmco5Dm7UGsX2ruDBieEHi8n9DVJWX5sTEz",
"alias": null
},
"description": "",
"base": "297646412ad38be27ab515245066b3783d83736f",
"oid": "b79181f29ce018940e490aeac31b8061fdf9a3a0",
"timestamp": 1721134310
},
{
"id": "9fa593f8703b1694df0910eab37b48fd7ae25038",
"author": {
"id": "did:key:z6MkkPvBfjP4bQmco5Dm7UGsX2ruDBieEHi8n9DVJWX5sTEz",
"alias": null
},
"description": "",
"base": "297646412ad38be27ab515245066b3783d83736f",
"oid": "5dd479cf66d9e51a975baa8a3c6142aa9acddbe8",
"timestamp": 1721134437
},
{
"id": "2c8973db1edbd642d0c420abe511a69540e4945c",
"author": {
"id": "did:key:z6MkkPvBfjP4bQmco5Dm7UGsX2ruDBieEHi8n9DVJWX5sTEz",
"alias": null
},
"description": "Rebase.",
"base": "6f8d75a00daac96ee4b375b4a7e93cfb8988820c",
"oid": "723e2741f54605c6404d0ff7f96cd6c14633cfb0",
"timestamp": 1721638000
}
]
}
}
{
"response": "triggered",
"run_id": {
"id": "a2b208f4-4084-481a-8a15-3ea0886b718a"
},
"info_url": "https://cci.rad.levitte.org//a2b208f4-4084-481a-8a15-3ea0886b718a.html"
}
Started at: 2026-09-08 13:17:58.501746+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/a2b208f4-4084-481a-8a15-3ea0886b718a/w/
╭────────────────────────────────────╮
│ heartwood │
│ Radicle Heartwood Protocol & Stack │
│ 189 issues · 39 patches │
╰────────────────────────────────────╯
Run `cd ./.` to go to the repository directory.
Exit code: 0
$ rad patch checkout 648d8d0b90a2ba7b938d30e6037205851f62ffb7
✓ Switched to branch patch/648d8d0 at revision 9fa593f
✓ Branch patch/648d8d0 setup to track rad/patches/648d8d0b90a2ba7b938d30e6037205851f62ffb7
Exit code: 0
$ git config advice.detachedHead false
Exit code: 0
$ git checkout 5dd479cf66d9e51a975baa8a3c6142aa9acddbe8
HEAD is now at 5dd479cf node: Socket Activation with systemd
Exit code: 0
$ rad patch show 648d8d0b90a2ba7b938d30e6037205851f62ffb7 -p
╭──────────────────────────────────────────────────────────────────────────────────╮
│ Title node: Socket Activation with systemd │
│ Patch 648d8d0b90a2ba7b938d30e6037205851f62ffb7 │
│ Author lorenz z6MkkPv…WX5sTEz │
│ Head 5dd479cf66d9e51a975baa8a3c6142aa9acddbe8 │
│ Base 297646412ad38be27ab515245066b3783d83736f │
│ Target master │
│ Branches patch/648d8d0 │
│ Commits ahead 1, behind 1254 │
│ Status merged │
│ │
│ 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](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/> │
├──────────────────────────────────────────────────────────────────────────────────┤
│ 5dd479c node: Socket Activation with systemd │
├──────────────────────────────────────────────────────────────────────────────────┤
│ ● Revision 648d8d0 @ 064ece3..0dc2c39 by lorenz z6MkkPv…WX5sTEz 2 years ago │
│ ↑ Revision d4ea40c @ 064ece3..c744407 by lorenz z6MkkPv…WX5sTEz 2 years ago │
│ ↑ Revision 0ffaa03 @ 064ece3..a26956c by lorenz z6MkkPv…WX5sTEz 2 years ago │
│ ↑ Revision 8d74709 @ 2976464..b79181f by lorenz z6MkkPv…WX5sTEz 2 years ago │
│ ↑ Revision 9fa593f @ 2976464..5dd479c by lorenz z6MkkPv…WX5sTEz 2 years ago │
│ ↑ Revision 2c8973d @ 6f8d75a..723e274 by cloudhead z6MksFq…bS9wzpT 2 years ago │
│ └─ ✓ merged by cloudhead z6MksFq…bS9wzpT │
╰──────────────────────────────────────────────────────────────────────────────────╯
commit 5dd479cf66d9e51a975baa8a3c6142aa9acddbe8
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 3006eaa3f..a56d88b3f 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -2227,6 +2227,7 @@ dependencies = [
"radicle-fetch",
"radicle-git-ext",
"radicle-signals",
+ "radicle-systemd",
"scrypt",
"serde",
"serde_json",
@@ -2292,6 +2293,10 @@ dependencies = [
"url",
]
+[[package]]
+name = "radicle-systemd"
+version = "0.9.0"
+
[[package]]
name = "radicle-term"
version = "0.10.0"
diff --git a/Cargo.toml b/Cargo.toml
index ed3644b98..72f8c3ef9 100644
--- a/Cargo.toml
+++ b/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"
diff --git a/radicle-node/Cargo.toml b/radicle-node/Cargo.toml
index 506b995d5..5cd286b3f 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: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"] }
diff --git a/radicle-node/src/runtime.rs b/radicle-node/src/runtime.rs
index dae86c082..51e72f7eb 100644
--- a/radicle-node/src/runtime.rs
+++ b/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()),
+ }
+ }
}
diff --git a/radicle-systemd/Cargo.toml b/radicle-systemd/Cargo.toml
new file mode 100644
index 000000000..534145a0d
--- /dev/null
+++ b/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
diff --git a/radicle-systemd/src/lib.rs b/radicle-systemd/src/lib.rs
new file mode 100644
index 000000000..00ef63404
--- /dev/null
+++ b/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
+}
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 a2b208f4-4084-481a-8a15-3ea0886b718a -v /opt/radcis/ci.rad.levitte.org/cci/state/a2b208f4-4084-481a-8a15-3ea0886b718a/s:/a2b208f4-4084-481a-8a15-3ea0886b718a/s:ro -v /opt/radcis/ci.rad.levitte.org/cci/state/a2b208f4-4084-481a-8a15-3ea0886b718a/w:/a2b208f4-4084-481a-8a15-3ea0886b718a/w -w /a2b208f4-4084-481a-8a15-3ea0886b718a/w -v /opt/radcis/ci.rad.levitte.org/.radicle:/${id}/.radicle:ro -e RAD_HOME=/${id}/.radicle rust:trixie bash /a2b208f4-4084-481a-8a15-3ea0886b718a/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"
}