(feat): improve config stuff

This commit is contained in:
Alois 2026-07-25 22:55:02 +02:00
commit 1744357350
Signed by: alois
SSH key fingerprint: SHA256:GBzT2DXvAuGV9XIV5W3WrzVpjU54FThmxHXdbz95J24
4 changed files with 66 additions and 32 deletions

View file

@ -39,4 +39,5 @@ usermod -aG iota-operators USER
``` ```
The user must start a new login session before supplementary group membership The user must start a new login session before supplementary group membership
is visible. `IOTA_SOCKET` remains authoritative for custom deployments. is visible. Unix per-user deployments must set `IOTA_SOCKET` to an absolute
path; Iota does not derive its IPC socket from `XDG_RUNTIME_DIR`.

View file

@ -183,7 +183,7 @@
users.groups.iota = {}; users.groups.iota = {};
systemd.sockets.iota-daemon = { systemd.sockets.iota = {
description = "${descriptionText} IPC socket"; description = "${descriptionText} IPC socket";
wantedBy = ["sockets.target"]; wantedBy = ["sockets.target"];
socketConfig = { socketConfig = {
@ -198,10 +198,11 @@
}; };
}; };
systemd.services.iota-daemon = { systemd.services.iota = {
description = descriptionText; description = descriptionText;
after = ["network.target"]; wantedBy = ["multi-user.target"];
requires = ["iota-daemon.socket"]; after = ["network.target" "iota.socket"];
requires = ["iota.socket"];
serviceConfig = serviceConfig =
{ {

View file

@ -25,6 +25,7 @@ pub enum IpcEndpoint {
#[derive(Debug)] #[derive(Debug)]
pub enum PathError { pub enum PathError {
MissingPlatformDirectory(&'static str), MissingPlatformDirectory(&'static str),
MissingRequiredOverride(&'static str),
EmptyOverride(&'static str), EmptyOverride(&'static str),
RelativeOverride { RelativeOverride {
variable: &'static str, variable: &'static str,
@ -37,6 +38,7 @@ impl fmt::Display for PathError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self { match self {
Self::MissingPlatformDirectory(name) => write!(f, "missing platform directory: {name}"), Self::MissingPlatformDirectory(name) => write!(f, "missing platform directory: {name}"),
Self::MissingRequiredOverride(name) => write!(f, "{name} must be set"),
Self::EmptyOverride(name) => write!(f, "{name} must not be empty"), Self::EmptyOverride(name) => write!(f, "{name} must not be empty"),
Self::RelativeOverride { variable, value } => { Self::RelativeOverride { variable, value } => {
write!(f, "{variable} must be absolute, got {}", value.display()) write!(f, "{variable} must be absolute, got {}", value.display())
@ -81,7 +83,11 @@ impl IotaPaths {
let install_root = override_first(&["IOTA_INSTALL_ROOT"])?.unwrap_or(defaults.install_root); let install_root = override_first(&["IOTA_INSTALL_ROOT"])?.unwrap_or(defaults.install_root);
let config_file = override_first(&["IOTA_CONFIG_FILE"])? let config_file = override_first(&["IOTA_CONFIG_FILE"])?
.unwrap_or_else(|| config_dir.join("config.yaml")); .unwrap_or_else(|| config_dir.join("config.yaml"));
let ipc_endpoint = resolve_ipc(scope, runtime_dir.as_deref(), defaults.ipc_endpoint)?; let ipc_endpoint = resolve_ipc(scope, defaults.ipc_endpoint)?;
let runtime_dir = runtime_dir.or_else(|| match &ipc_endpoint {
IpcEndpoint::UnixSocket(path) => path.parent().map(Path::to_path_buf),
IpcEndpoint::WindowsPipe(_) => None,
});
Ok(Self { Ok(Self {
scope, scope,
config_dir, config_dir,
@ -234,7 +240,7 @@ struct Defaults {
log_dir: PathBuf, log_dir: PathBuf,
asset_dir: PathBuf, asset_dir: PathBuf,
install_root: PathBuf, install_root: PathBuf,
ipc_endpoint: IpcEndpoint, ipc_endpoint: Option<IpcEndpoint>,
} }
impl Defaults { impl Defaults {
fn for_scope(scope: Scope) -> Result<Self, PathError> { fn for_scope(scope: Scope) -> Result<Self, PathError> {
@ -250,7 +256,7 @@ impl Defaults {
log_dir: "/var/log/iota".into(), log_dir: "/var/log/iota".into(),
asset_dir: "/usr/local/share/iota/web".into(), asset_dir: "/usr/local/share/iota/web".into(),
install_root: "/usr/local/libexec/iota".into(), install_root: "/usr/local/libexec/iota".into(),
ipc_endpoint: IpcEndpoint::UnixSocket("/run/iota/iota.sock".into()), ipc_endpoint: Some(IpcEndpoint::UnixSocket("/run/iota/iota.sock".into())),
}) })
} }
#[cfg(not(target_os = "linux"))] #[cfg(not(target_os = "linux"))]
@ -271,18 +277,15 @@ fn user_defaults() -> Result<Defaults, PathError> {
let state_base = xdg_or_home("XDG_STATE_HOME", &home, ".local/state")?; let state_base = xdg_or_home("XDG_STATE_HOME", &home, ".local/state")?;
let cache_base = xdg_or_home("XDG_CACHE_HOME", &home, ".cache")?; let cache_base = xdg_or_home("XDG_CACHE_HOME", &home, ".cache")?;
let data_base = xdg_or_home("XDG_DATA_HOME", &home, ".local/share")?; let data_base = xdg_or_home("XDG_DATA_HOME", &home, ".local/share")?;
let runtime = absolute_env("XDG_RUNTIME_DIR")?
.ok_or(PathError::MissingPlatformDirectory("XDG_RUNTIME_DIR"))?
.join("iota");
Ok(Defaults { Ok(Defaults {
config_dir: config_base.join("iota"), config_dir: config_base.join("iota"),
state_dir: state_base.join("iota"), state_dir: state_base.join("iota"),
cache_dir: cache_base.join("iota"), cache_dir: cache_base.join("iota"),
runtime_dir: Some(runtime.clone()), runtime_dir: None,
log_dir: state_base.join("iota/logs"), log_dir: state_base.join("iota/logs"),
asset_dir: data_base.join("iota/web"), asset_dir: data_base.join("iota/web"),
install_root: data_base.join("iota/bin"), install_root: data_base.join("iota/bin"),
ipc_endpoint: IpcEndpoint::UnixSocket(runtime.join("iota.sock")), ipc_endpoint: None,
}) })
} }
#[cfg(windows)] #[cfg(windows)]
@ -301,7 +304,9 @@ fn user_defaults() -> Result<Defaults, PathError> {
log_dir: local.join("logs"), log_dir: local.join("logs"),
asset_dir: local.join("data"), asset_dir: local.join("data"),
install_root: local.join("bin"), install_root: local.join("bin"),
ipc_endpoint: IpcEndpoint::WindowsPipe(r"\\.\pipe\Tensamin.Iota.User".into()), ipc_endpoint: Some(IpcEndpoint::WindowsPipe(
r"\\.\pipe\Tensamin.Iota.User".into(),
)),
}) })
} }
@ -332,20 +337,14 @@ fn override_first(names: &[&'static str]) -> Result<Option<PathBuf>, PathError>
} }
Ok(None) Ok(None)
} }
fn resolve_ipc( fn resolve_ipc(scope: Scope, default: Option<IpcEndpoint>) -> Result<IpcEndpoint, PathError> {
scope: Scope,
runtime: Option<&Path>,
default: IpcEndpoint,
) -> Result<IpcEndpoint, PathError> {
#[cfg(unix)] #[cfg(unix)]
{ {
if let Some(path) = absolute_env("IOTA_SOCKET")? { if let Some(path) = absolute_env("IOTA_SOCKET")? {
return Ok(IpcEndpoint::UnixSocket(path)); return Ok(IpcEndpoint::UnixSocket(path));
} }
if scope == Scope::User { if scope == Scope::User {
return runtime return Err(PathError::MissingRequiredOverride("IOTA_SOCKET"));
.map(|dir| IpcEndpoint::UnixSocket(dir.join("iota.sock")))
.ok_or(PathError::MissingPlatformDirectory("XDG_RUNTIME_DIR"));
} }
} }
#[cfg(windows)] #[cfg(windows)]
@ -358,7 +357,7 @@ fn resolve_ipc(
return Ok(IpcEndpoint::WindowsPipe(name)); return Ok(IpcEndpoint::WindowsPipe(name));
} }
} }
Ok(default) default.ok_or(PathError::UnsupportedScope)
} }
fn create_directory(path: &Path, private: bool) -> std::io::Result<()> { fn create_directory(path: &Path, private: bool) -> std::io::Result<()> {
std::fs::create_dir_all(path)?; std::fs::create_dir_all(path)?;

View file

@ -218,6 +218,8 @@ mod systemd {
pub struct SystemdManager { pub struct SystemdManager {
executor: Arc<dyn CommandExecutor>, executor: Arc<dyn CommandExecutor>,
service: &'static str,
socket: &'static str,
} }
impl SystemdManager { impl SystemdManager {
pub async fn detect() -> Option<Self> { pub async fn detect() -> Option<Self> {
@ -225,16 +227,29 @@ mod systemd {
return None; return None;
} }
let executor: Arc<dyn CommandExecutor> = Arc::new(RealExecutor); let executor: Arc<dyn CommandExecutor> = Arc::new(RealExecutor);
executor let mut manager = executor
.output("systemctl", &["--version", &COMMON[0], &COMMON[1]]) .output("systemctl", &["--version", &COMMON[0], &COMMON[1]])
.await .await
.ok() .ok()
.filter(|r| r.success) .filter(|r| r.success)
.map(|_| Self { executor }) .map(|_| Self {
executor,
service: SERVICE,
socket: SOCKET,
})?;
if manager.status("iota.service").await.is_ok() {
manager.service = "iota.service";
manager.socket = "iota.socket";
}
Some(manager)
} }
#[cfg(test)] #[cfg(test)]
pub fn with_executor(executor: Arc<dyn CommandExecutor>) -> Self { pub fn with_executor(executor: Arc<dyn CommandExecutor>) -> Self {
Self { executor } Self {
executor,
service: SERVICE,
socket: SOCKET,
}
} }
async fn run(&self, action: &[&str]) -> Result<(), ProcessManagerError> { async fn run(&self, action: &[&str]) -> Result<(), ProcessManagerError> {
let mut args = COMMON.to_vec(); let mut args = COMMON.to_vec();
@ -358,19 +373,25 @@ mod systemd {
async fn unit_status(&self, unit: &str) -> Result<UnitStatus, ProcessManagerError> { async fn unit_status(&self, unit: &str) -> Result<UnitStatus, ProcessManagerError> {
self.status(unit).await self.status(unit).await
} }
async fn iota_startup_status(&self) -> Result<DaemonStartupStatus, ProcessManagerError> {
Ok(DaemonStartupStatus::classify(
self.status(self.service).await?,
self.status(self.socket).await?,
))
}
async fn set_iota_startup_mode( async fn set_iota_startup_mode(
&self, &self,
mode: StartupMode, mode: StartupMode,
) -> Result<DaemonStartupStatus, ProcessManagerError> { ) -> Result<DaemonStartupStatus, ProcessManagerError> {
match mode { match mode {
StartupMode::AlwaysOn => { StartupMode::AlwaysOn => {
self.run(&["disable", SOCKET]).await?; self.run(&["disable", self.socket]).await?;
self.run(&["enable", "--now", SERVICE]).await?; self.run(&["enable", "--now", self.service]).await?;
self.verify(DetectedStartupMode::AlwaysOn).await self.verify(DetectedStartupMode::AlwaysOn).await
} }
StartupMode::SocketActivated => { StartupMode::SocketActivated => {
self.run(&["disable", "--now", SERVICE]).await?; self.run(&["disable", "--now", self.service]).await?;
self.run(&["enable", "--now", SOCKET]).await?; self.run(&["enable", "--now", self.socket]).await?;
self.verify(DetectedStartupMode::SocketActivated).await self.verify(DetectedStartupMode::SocketActivated).await
} }
} }
@ -378,9 +399,21 @@ mod systemd {
async fn unit_action(&self, action: &[&str]) -> Result<(), ProcessManagerError> { async fn unit_action(&self, action: &[&str]) -> Result<(), ProcessManagerError> {
self.run(action).await self.run(action).await
} }
async fn process_action(
&self,
action: ProcessAction,
) -> Result<DaemonStartupStatus, ProcessManagerError> {
let verb = match action {
ProcessAction::Start => "start",
ProcessAction::Stop => "stop",
ProcessAction::Restart => "restart",
};
self.run(&[verb, self.service]).await?;
self.iota_startup_status().await
}
async fn disable_startup(&self) -> Result<DaemonStartupStatus, ProcessManagerError> { async fn disable_startup(&self) -> Result<DaemonStartupStatus, ProcessManagerError> {
self.run(&["disable", "--now", SERVICE]).await?; self.run(&["disable", "--now", self.service]).await?;
self.run(&["disable", "--now", SOCKET]).await?; self.run(&["disable", "--now", self.socket]).await?;
self.verify(DetectedStartupMode::Disabled).await self.verify(DetectedStartupMode::Disabled).await
} }
} }