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<()> {
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() {
anyhow::bail!(

View File

@ -11,7 +11,7 @@ use args::Operation;
pub fn run(args: Args, ctx: impl term::Context) -> anyhow::Result<()> {
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) {
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 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 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 {
let mut node = Node::new(profile.socket());
let mut node = Node::new(profile.socket_from_env());
node::announce(
&repo,
node::SyncSettings::default(),

View File

@ -23,7 +23,7 @@ use args::{Addr, Command};
pub fn run(args: Args, ctx: impl term::Context) -> anyhow::Result<()> {
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();

View File

@ -314,7 +314,7 @@ pub fn run(args: Args, ctx: impl term::Context) -> anyhow::Result<()> {
}
if announce {
let mut node = Node::new(profile.socket());
let mut node = Node::new(profile.socket_from_env());
node::announce(
&repository,
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");
}
let mut node = radicle::Node::new(profile.socket());
let mut node = radicle::Node::new(profile.socket_from_env());
let spinner = term::spinner("Updating inventory..");
// The repository is now part of our inventory.

View File

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

View File

@ -30,7 +30,7 @@ use args::{Command, SortBy, SyncDirection, SyncMode};
pub fn run(args: Args, ctx: impl term::Context) -> anyhow::Result<()> {
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() {
anyhow::bail!(
"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<()> {
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 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<()> {
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 {
delete(rid, &mut node, &profile)?;

View File

@ -329,11 +329,12 @@ fn execute(options: Options) -> Result<(), ExecutionError> {
signals
};
let socket = home.socket_from_env();
if options.force {
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(())
}

View File

@ -127,6 +127,7 @@ impl Runtime {
pub fn init<G>(
home: Home,
config: radicle::node::Config,
socket: PathBuf,
listen: Vec<net::SocketAddr>,
signals: chan::Receiver<Signal>,
signer: Device<G>,
@ -233,7 +234,7 @@ impl Runtime {
wire.listen(listener);
}
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 fetch = worker::FetchConfig {
@ -255,7 +256,7 @@ impl Runtime {
policies_db: home.node().join(node::POLICIES_DB_FILE),
},
)?;
let control = Self::bind(home.socket())?;
let control = Self::bind(socket)?;
Ok(Runtime {
id,

View File

@ -1,5 +1,6 @@
use std::collections::HashSet;
use std::net;
use std::path::PathBuf;
use std::sync::Arc;
use std::sync::atomic::{AtomicBool, Ordering};
use std::{fmt, io, time};
@ -71,6 +72,10 @@ impl<T> From<chan::SendError<T>> for Error {
pub struct Handle {
pub(crate) home: Home,
/// Path to the control socket in use. Required for shutdown.
pub(crate) socket: PathBuf,
pub(crate) controller: reactor::Controller,
/// Whether a shutdown was initiated or not. Prevents attempting to shutdown twice.
@ -96,6 +101,7 @@ impl Clone for Handle {
fn clone(&self) -> Self {
Self {
home: self.home.clone(),
socket: self.socket.clone(),
controller: self.controller.clone(),
shutdown: self.shutdown.clone(),
emitter: self.emitter.clone(),
@ -104,9 +110,15 @@ impl Clone for 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 {
home,
socket,
controller,
shutdown: Arc::default(),
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
// control thread gracefully. Since the control thread may have called this function,
// 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))
.ok();

View File

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

View File

@ -65,7 +65,7 @@ pub(super) struct RealNodeSession {
impl RealNodeSession {
pub(super) fn new(profile: &Profile) -> 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
- 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
### Security

View File

@ -667,12 +667,25 @@ impl Home {
self.path.join("cobs")
}
pub fn socket(&self) -> PathBuf {
const DEFAULT_SOCKET_NAME: &str = "control.sock";
/// The location of the control socket of the node.
/// 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)
.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.