radicle: Prevent `RAD_SOCKET` from polluting tests

The value of `RAD_SOCKET` influences the outcome of `cargo test`.

Users that set this environment variable to, e.g. point `rad` at
a socket at a custom location, were surprised that execution of
tests would interact with their node.

Split `Home::socket` into two functions. `Home::socket_default` is
"pure" and only dependent on the path represented by `Home`. On the
other hand, `Home::socket_from_env` interprets the environment.

Most users will want to use `Home::socket_from_env`.
This commit is contained in:
Lorenz Leutgeb 2026-03-21 15:03:00 +01:00
parent a2de55cbb9
commit 80c1bba76f
No known key found for this signature in database
20 changed files with 57 additions and 25 deletions

View File

@ -30,7 +30,7 @@ pub use args::Args;
pub fn run(args: Args, ctx: impl term::Context) -> anyhow::Result<()> { pub fn run(args: Args, ctx: impl term::Context) -> anyhow::Result<()> {
let profile = ctx.profile()?; let profile = ctx.profile()?;
let mut node = radicle::Node::new(profile.socket()); let mut node = radicle::Node::new(profile.socket_from_env());
if !node.is_running() { if !node.is_running() {
anyhow::bail!( anyhow::bail!(

View File

@ -11,7 +11,7 @@ use args::Operation;
pub fn run(args: Args, ctx: impl term::Context) -> anyhow::Result<()> { pub fn run(args: Args, ctx: impl term::Context) -> anyhow::Result<()> {
let profile = ctx.profile()?; let profile = ctx.profile()?;
let mut node = radicle::Node::new(profile.socket()); let mut node = radicle::Node::new(profile.socket_from_env());
match Operation::from(args) { match Operation::from(args) {
Operation::Follow { nid, alias, .. } => follow(nid, alias, &mut node, &profile)?, Operation::Follow { nid, alias, .. } => follow(nid, alias, &mut node, &profile)?,

View File

@ -136,7 +136,7 @@ pub fn init(repo: git::Repository, args: Args, profile: &profile::Profile) -> an
}; };
let signer = term::signer(profile)?; let signer = term::signer(profile)?;
let mut node = radicle::Node::new(profile.socket()); let mut node = radicle::Node::new(profile.socket_from_env());
let mut spinner = term::spinner("Initializing..."); let mut spinner = term::spinner("Initializing...");
let mut push_cmd = String::from("git push"); let mut push_cmd = String::from("git push");

View File

@ -219,7 +219,7 @@ pub fn run(args: Args, ctx: impl term::Context) -> anyhow::Result<()> {
} }
if announce { if announce {
let mut node = Node::new(profile.socket()); let mut node = Node::new(profile.socket_from_env());
node::announce( node::announce(
&repo, &repo,
node::SyncSettings::default(), node::SyncSettings::default(),

View File

@ -23,7 +23,7 @@ use args::{Addr, Command};
pub fn run(args: Args, ctx: impl term::Context) -> anyhow::Result<()> { pub fn run(args: Args, ctx: impl term::Context) -> anyhow::Result<()> {
let profile = ctx.profile()?; let profile = ctx.profile()?;
let mut node = Node::new(profile.socket()); let mut node = Node::new(profile.socket_from_env());
let command = args.command.unwrap_or_default(); let command = args.command.unwrap_or_default();

View File

@ -314,7 +314,7 @@ pub fn run(args: Args, ctx: impl term::Context) -> anyhow::Result<()> {
} }
if announce { if announce {
let mut node = Node::new(profile.socket()); let mut node = Node::new(profile.socket_from_env());
node::announce( node::announce(
&repository, &repository,
node::SyncSettings::default(), node::SyncSettings::default(),

View File

@ -63,7 +63,7 @@ pub fn run(args: Args, ctx: impl term::Context) -> anyhow::Result<()> {
} }
anyhow::bail!("fatal: repository storage is corrupt"); anyhow::bail!("fatal: repository storage is corrupt");
} }
let mut node = radicle::Node::new(profile.socket()); let mut node = radicle::Node::new(profile.socket_from_env());
let spinner = term::spinner("Updating inventory.."); let spinner = term::spinner("Updating inventory..");
// The repository is now part of our inventory. // The repository is now part of our inventory.

View File

@ -23,7 +23,7 @@ pub fn run(
sync: bool, sync: bool,
) -> anyhow::Result<()> { ) -> anyhow::Result<()> {
if sync { if sync {
let mut node = radicle::Node::new(profile.socket()); let mut node = radicle::Node::new(profile.socket_from_env());
if !profile.policies()?.is_following(nid)? { if !profile.policies()?.is_following(nid)? {
let alias = name.as_ref().and_then(|n| Alias::from_str(n.as_str()).ok()); let alias = name.as_ref().and_then(|n| Alias::from_str(n.as_str()).ok());

View File

@ -13,7 +13,7 @@ pub use args::Args;
pub fn run(args: Args, ctx: impl term::Context) -> anyhow::Result<()> { pub fn run(args: Args, ctx: impl term::Context) -> anyhow::Result<()> {
let profile = ctx.profile()?; let profile = ctx.profile()?;
let mut node = radicle::Node::new(profile.socket()); let mut node = radicle::Node::new(profile.socket_from_env());
match args::Operation::from(args) { match args::Operation::from(args) {
args::Operation::List => seeding(&profile)?, args::Operation::List => seeding(&profile)?,

View File

@ -28,7 +28,7 @@ pub fn run(args: Args, ctx: impl term::Context) -> anyhow::Result<()> {
} else if args.nid { } else if args.nid {
crate::warning::deprecated("rad self --nid", "rad node status --only nid"); crate::warning::deprecated("rad self --nid", "rad node status --only nid");
term::print( term::print(
Node::new(profile.socket()) Node::new(profile.socket_from_env())
.nid() .nid()
.ok() .ok()
.unwrap_or_else(|| *profile.id()), .unwrap_or_else(|| *profile.id()),
@ -54,7 +54,7 @@ fn all(profile: &Profile) -> anyhow::Result<()> {
term::format::tertiary(did).into(), term::format::tertiary(did).into(),
]); ]);
let socket = profile.socket(); let socket = profile.socket_from_env();
let node = if Node::new(&socket).is_running() { let node = if Node::new(&socket).is_running() {
term::format::positive(format!("running ({})", socket.display())) term::format::positive(format!("running ({})", socket.display()))
} else { } else {

View File

@ -30,7 +30,7 @@ use args::{Command, SortBy, SyncDirection, SyncMode};
pub fn run(args: Args, ctx: impl term::Context) -> anyhow::Result<()> { pub fn run(args: Args, ctx: impl term::Context) -> anyhow::Result<()> {
let profile = ctx.profile()?; let profile = ctx.profile()?;
let mut node = radicle::Node::new(profile.socket()); let mut node = radicle::Node::new(profile.socket_from_env());
if !node.is_running() { if !node.is_running() {
anyhow::bail!( anyhow::bail!(
"to sync a repository, your node must be running. To start it, run `rad node start`" "to sync a repository, your node must be running. To start it, run `rad node start`"

View File

@ -8,7 +8,7 @@ pub use args::Args;
pub fn run(args: Args, ctx: impl term::Context) -> anyhow::Result<()> { pub fn run(args: Args, ctx: impl term::Context) -> anyhow::Result<()> {
let profile = ctx.profile()?; let profile = ctx.profile()?;
let mut node = radicle::Node::new(profile.socket()); let mut node = radicle::Node::new(profile.socket_from_env());
let nid = args.nid; let nid = args.nid;
let unfollowed = match node.unfollow(nid) { let unfollowed = match node.unfollow(nid) {

View File

@ -8,7 +8,7 @@ pub use args::Args;
pub fn run(args: Args, ctx: impl term::Context) -> anyhow::Result<()> { pub fn run(args: Args, ctx: impl term::Context) -> anyhow::Result<()> {
let profile = ctx.profile()?; let profile = ctx.profile()?;
let mut node = radicle::Node::new(profile.socket()); let mut node = radicle::Node::new(profile.socket_from_env());
for rid in args.rids { for rid in args.rids {
delete(rid, &mut node, &profile)?; delete(rid, &mut node, &profile)?;

View File

@ -329,11 +329,12 @@ fn execute(options: Options) -> Result<(), ExecutionError> {
signals signals
}; };
let socket = home.socket_from_env();
if options.force { if options.force {
log::debug!(target: "node", "Removing existing control socket.."); log::debug!(target: "node", "Removing existing control socket..");
std::fs::remove_file(home.socket()).ok(); std::fs::remove_file(&socket).ok();
} }
Runtime::init(home, config.node, listen, signals, signer)?.run()?; Runtime::init(home, config.node, socket, listen, signals, signer)?.run()?;
Ok(()) Ok(())
} }

View File

@ -127,6 +127,7 @@ impl Runtime {
pub fn init<G>( pub fn init<G>(
home: Home, home: Home,
config: radicle::node::Config, config: radicle::node::Config,
socket: PathBuf,
listen: Vec<net::SocketAddr>, listen: Vec<net::SocketAddr>,
signals: chan::Receiver<Signal>, signals: chan::Receiver<Signal>,
signer: Device<G>, signer: Device<G>,
@ -233,7 +234,7 @@ impl Runtime {
wire.listen(listener); wire.listen(listener);
} }
let reactor = Reactor::new(wire, thread::name(&id, "service"))?; let reactor = Reactor::new(wire, thread::name(&id, "service"))?;
let handle = Handle::new(home.clone(), reactor.controller(), emitter); let handle = Handle::new(home.clone(), socket.clone(), reactor.controller(), emitter);
let nid = *signer.public_key(); let nid = *signer.public_key();
let fetch = worker::FetchConfig { let fetch = worker::FetchConfig {
@ -255,7 +256,7 @@ impl Runtime {
policies_db: home.node().join(node::POLICIES_DB_FILE), policies_db: home.node().join(node::POLICIES_DB_FILE),
}, },
)?; )?;
let control = Self::bind(home.socket())?; let control = Self::bind(socket)?;
Ok(Runtime { Ok(Runtime {
id, id,

View File

@ -1,5 +1,6 @@
use std::collections::HashSet; use std::collections::HashSet;
use std::net; use std::net;
use std::path::PathBuf;
use std::sync::Arc; use std::sync::Arc;
use std::sync::atomic::{AtomicBool, Ordering}; use std::sync::atomic::{AtomicBool, Ordering};
use std::{fmt, io, time}; use std::{fmt, io, time};
@ -71,6 +72,10 @@ impl<T> From<chan::SendError<T>> for Error {
pub struct Handle { pub struct Handle {
pub(crate) home: Home, pub(crate) home: Home,
/// Path to the control socket in use. Required for shutdown.
pub(crate) socket: PathBuf,
pub(crate) controller: reactor::Controller, pub(crate) controller: reactor::Controller,
/// Whether a shutdown was initiated or not. Prevents attempting to shutdown twice. /// Whether a shutdown was initiated or not. Prevents attempting to shutdown twice.
@ -96,6 +101,7 @@ impl Clone for Handle {
fn clone(&self) -> Self { fn clone(&self) -> Self {
Self { Self {
home: self.home.clone(), home: self.home.clone(),
socket: self.socket.clone(),
controller: self.controller.clone(), controller: self.controller.clone(),
shutdown: self.shutdown.clone(), shutdown: self.shutdown.clone(),
emitter: self.emitter.clone(), emitter: self.emitter.clone(),
@ -104,9 +110,15 @@ impl Clone for Handle {
} }
impl Handle { impl Handle {
pub fn new(home: Home, controller: reactor::Controller, emitter: Emitter<Event>) -> Self { pub fn new(
home: Home,
socket: PathBuf,
controller: reactor::Controller,
emitter: Emitter<Event>,
) -> Self {
Self { Self {
home, home,
socket,
controller, controller,
shutdown: Arc::default(), shutdown: Arc::default(),
emitter, emitter,
@ -352,7 +364,7 @@ impl radicle::node::Handle for Handle {
// Send a shutdown request to our own control socket. This is the only way to kill the // Send a shutdown request to our own control socket. This is the only way to kill the
// control thread gracefully. Since the control thread may have called this function, // control thread gracefully. Since the control thread may have called this function,
// the control socket may already be disconnected. Ignore errors. // the control socket may already be disconnected. Ignore errors.
UnixStream::connect(self.home.socket()) UnixStream::connect(self.socket)
.and_then(|sock| Command::Shutdown.to_writer(sock)) .and_then(|sock| Command::Shutdown.to_writer(sock))
.ok(); .ok();

View File

@ -478,6 +478,7 @@ impl<G: cyphernet::Ecdh<Pk = NodeId> + Signer<Signature> + Clone + Debug> Node<G
let rt = Runtime::init( let rt = Runtime::init(
self.home.clone(), self.home.clone(),
self.config, self.config,
self.home.socket_default(),
listen, listen,
signals, signals,
self.signer.clone(), self.signer.clone(),

View File

@ -65,7 +65,7 @@ pub(super) struct RealNodeSession {
impl RealNodeSession { impl RealNodeSession {
pub(super) fn new(profile: &Profile) -> Self { pub(super) fn new(profile: &Profile) -> Self {
Self { Self {
node: radicle::Node::new(profile.socket()), node: radicle::Node::new(profile.socket_from_env()),
} }
} }
} }

View File

@ -11,6 +11,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Changed ### Changed
- To obtain the location of the control socket, do not use `Home::socket`
(which was removed), but instead choose `Home::socket_from_env` or
`Home::socket_default` depending on your use-case.
### Removed ### Removed
### Security ### Security

View File

@ -667,12 +667,25 @@ impl Home {
self.path.join("cobs") self.path.join("cobs")
} }
pub fn socket(&self) -> PathBuf { /// The location of the control socket of the node.
const DEFAULT_SOCKET_NAME: &str = "control.sock"; /// If the environment variable with name [`env::RAD_SOCKET`] is set,
/// its value is used.
/// Otherwise, the default socket name, which is relative to this
/// [`Home`], is used (see [`Self::socket_default`]).
pub fn socket_from_env(&self) -> PathBuf {
env::var_os(env::RAD_SOCKET) env::var_os(env::RAD_SOCKET)
.map(PathBuf::from) .map(PathBuf::from)
.unwrap_or_else(|| self.node().join(DEFAULT_SOCKET_NAME)) .unwrap_or_else(|| self.socket_default())
}
/// The default location of the control socket of the node.
/// The returned value only depends on `self`, and not on
/// any environment variables.
///
/// See also [`Self::socket_from_env`].
pub fn socket_default(&self) -> PathBuf {
const DEFAULT_SOCKET_NAME: &str = "control.sock";
self.node().join(DEFAULT_SOCKET_NAME)
} }
/// Return a read-write handle to the notifications database. /// Return a read-write handle to the notifications database.