diff --git a/README.md b/README.md index 4a278f3..8739e1d 100644 --- a/README.md +++ b/README.md @@ -39,4 +39,5 @@ usermod -aG iota-operators USER ``` 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`. diff --git a/flake.nix b/flake.nix index ed9a49a..e2ca8a1 100644 --- a/flake.nix +++ b/flake.nix @@ -183,7 +183,7 @@ users.groups.iota = {}; - systemd.sockets.iota-daemon = { + systemd.sockets.iota = { description = "${descriptionText} IPC socket"; wantedBy = ["sockets.target"]; socketConfig = { @@ -198,10 +198,11 @@ }; }; - systemd.services.iota-daemon = { + systemd.services.iota = { description = descriptionText; - after = ["network.target"]; - requires = ["iota-daemon.socket"]; + wantedBy = ["multi-user.target"]; + after = ["network.target" "iota.socket"]; + requires = ["iota.socket"]; serviceConfig = { diff --git a/iota-paths/src/lib.rs b/iota-paths/src/lib.rs index 35a51bd..9143ea1 100644 --- a/iota-paths/src/lib.rs +++ b/iota-paths/src/lib.rs @@ -25,6 +25,7 @@ pub enum IpcEndpoint { #[derive(Debug)] pub enum PathError { MissingPlatformDirectory(&'static str), + MissingRequiredOverride(&'static str), EmptyOverride(&'static str), RelativeOverride { variable: &'static str, @@ -37,6 +38,7 @@ impl fmt::Display for PathError { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { 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::RelativeOverride { variable, value } => { 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 config_file = override_first(&["IOTA_CONFIG_FILE"])? .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 { scope, config_dir, @@ -234,7 +240,7 @@ struct Defaults { log_dir: PathBuf, asset_dir: PathBuf, install_root: PathBuf, - ipc_endpoint: IpcEndpoint, + ipc_endpoint: Option, } impl Defaults { fn for_scope(scope: Scope) -> Result { @@ -250,7 +256,7 @@ impl Defaults { log_dir: "/var/log/iota".into(), asset_dir: "/usr/local/share/iota/web".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"))] @@ -271,18 +277,15 @@ fn user_defaults() -> Result { let state_base = xdg_or_home("XDG_STATE_HOME", &home, ".local/state")?; let cache_base = xdg_or_home("XDG_CACHE_HOME", &home, ".cache")?; 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 { config_dir: config_base.join("iota"), state_dir: state_base.join("iota"), cache_dir: cache_base.join("iota"), - runtime_dir: Some(runtime.clone()), + runtime_dir: None, log_dir: state_base.join("iota/logs"), asset_dir: data_base.join("iota/web"), install_root: data_base.join("iota/bin"), - ipc_endpoint: IpcEndpoint::UnixSocket(runtime.join("iota.sock")), + ipc_endpoint: None, }) } #[cfg(windows)] @@ -301,7 +304,9 @@ fn user_defaults() -> Result { log_dir: local.join("logs"), asset_dir: local.join("data"), 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, PathError> } Ok(None) } -fn resolve_ipc( - scope: Scope, - runtime: Option<&Path>, - default: IpcEndpoint, -) -> Result { +fn resolve_ipc(scope: Scope, default: Option) -> Result { #[cfg(unix)] { if let Some(path) = absolute_env("IOTA_SOCKET")? { return Ok(IpcEndpoint::UnixSocket(path)); } if scope == Scope::User { - return runtime - .map(|dir| IpcEndpoint::UnixSocket(dir.join("iota.sock"))) - .ok_or(PathError::MissingPlatformDirectory("XDG_RUNTIME_DIR")); + return Err(PathError::MissingRequiredOverride("IOTA_SOCKET")); } } #[cfg(windows)] @@ -358,7 +357,7 @@ fn resolve_ipc( return Ok(IpcEndpoint::WindowsPipe(name)); } } - Ok(default) + default.ok_or(PathError::UnsupportedScope) } fn create_directory(path: &Path, private: bool) -> std::io::Result<()> { std::fs::create_dir_all(path)?; diff --git a/iota-process-manager/src/lib.rs b/iota-process-manager/src/lib.rs index e3a03d5..c0ebb0d 100644 --- a/iota-process-manager/src/lib.rs +++ b/iota-process-manager/src/lib.rs @@ -218,6 +218,8 @@ mod systemd { pub struct SystemdManager { executor: Arc, + service: &'static str, + socket: &'static str, } impl SystemdManager { pub async fn detect() -> Option { @@ -225,16 +227,29 @@ mod systemd { return None; } let executor: Arc = Arc::new(RealExecutor); - executor + let mut manager = executor .output("systemctl", &["--version", &COMMON[0], &COMMON[1]]) .await .ok() .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)] pub fn with_executor(executor: Arc) -> Self { - Self { executor } + Self { + executor, + service: SERVICE, + socket: SOCKET, + } } async fn run(&self, action: &[&str]) -> Result<(), ProcessManagerError> { let mut args = COMMON.to_vec(); @@ -358,19 +373,25 @@ mod systemd { async fn unit_status(&self, unit: &str) -> Result { self.status(unit).await } + async fn iota_startup_status(&self) -> Result { + Ok(DaemonStartupStatus::classify( + self.status(self.service).await?, + self.status(self.socket).await?, + )) + } async fn set_iota_startup_mode( &self, mode: StartupMode, ) -> Result { match mode { StartupMode::AlwaysOn => { - self.run(&["disable", SOCKET]).await?; - self.run(&["enable", "--now", SERVICE]).await?; + self.run(&["disable", self.socket]).await?; + self.run(&["enable", "--now", self.service]).await?; self.verify(DetectedStartupMode::AlwaysOn).await } StartupMode::SocketActivated => { - self.run(&["disable", "--now", SERVICE]).await?; - self.run(&["enable", "--now", SOCKET]).await?; + self.run(&["disable", "--now", self.service]).await?; + self.run(&["enable", "--now", self.socket]).await?; self.verify(DetectedStartupMode::SocketActivated).await } } @@ -378,9 +399,21 @@ mod systemd { async fn unit_action(&self, action: &[&str]) -> Result<(), ProcessManagerError> { self.run(action).await } + async fn process_action( + &self, + action: ProcessAction, + ) -> Result { + 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 { - self.run(&["disable", "--now", SERVICE]).await?; - self.run(&["disable", "--now", SOCKET]).await?; + self.run(&["disable", "--now", self.service]).await?; + self.run(&["disable", "--now", self.socket]).await?; self.verify(DetectedStartupMode::Disabled).await } }