From 7383445ea5f488321c8195e9177ab7d144df5850 Mon Sep 17 00:00:00 2001 From: Niko Marmeladkov Date: Fri, 3 Jul 2026 13:04:58 +0300 Subject: [PATCH] refactor paths: remove HOME dependency, use env/XDG/tmp fallback MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - socket/log/enabled paths resolved via state.rs with priority: RSS_SOCKET/RSS_STATE_DIR/RSS_CONFIG_DIR env → XDG_RUNTIME_DIR → ~/.local/share/rss → /tmp/rss-niko - config path: RSS_CONFIG → RSS_CONFIG_DIR/config.toml → ~/.config/rss → /etc/rss/config.toml - removed State struct; all functions are module-level - daemon chmods socket parent dir for group access - clean up stale PID file on config load failure --- contrib/rss.service | 1 + src/daemon.rs | 58 ++++++++++--------- src/ipc.rs | 13 +---- src/main.rs | 15 ++--- src/state.rs | 134 +++++++++++++++++++++++++------------------- 5 files changed, 116 insertions(+), 105 deletions(-) diff --git a/contrib/rss.service b/contrib/rss.service index 721f491..0ac311e 100644 --- a/contrib/rss.service +++ b/contrib/rss.service @@ -8,6 +8,7 @@ ExecStart=%h/.cargo/bin/rss daemon Restart=on-failure RestartSec=5 Environment=RSS_CONFIG=%h/.config/rss/config.toml +Environment=RSS_STATE_DIR=%h/.local/share/rss [Install] WantedBy=default.target diff --git a/src/daemon.rs b/src/daemon.rs index c701475..b20a222 100644 --- a/src/daemon.rs +++ b/src/daemon.rs @@ -1,12 +1,14 @@ use std::collections::HashMap; +use std::fs; use std::io::{BufRead, BufReader, Write}; +use std::os::unix::fs::PermissionsExt; use std::os::unix::net::{UnixListener, UnixStream}; use std::process::{Child, Command, Stdio}; use std::time::{Duration, SystemTime}; use std::sync::atomic::{AtomicBool, Ordering}; use crate::config::{Config, RestartPolicy, ServiceConfig}; -use crate::state::State; +use crate::state; static STOP: AtomicBool = AtomicBool::new(false); @@ -35,16 +37,24 @@ pub fn run(config_path: String) { } // Write daemon PID - let pid_path = crate::ipc::daemon_pid_path(); - State::write_pid(&pid_path).ok(); + let pid_path = state::daemon_pid_path(); + state::write_pid(&pid_path).ok(); // Ensure socket path is clean - let sock_path = crate::ipc::socket_path(); + let sock_path = state::socket_path(); if sock_path.exists() { - let _ = std::fs::remove_file(&sock_path); + let _ = fs::remove_file(&sock_path); } if let Some(parent) = sock_path.parent() { - let _ = std::fs::create_dir_all(parent); + fs::create_dir_all(parent).ok(); + // Make directory group-accessible (rwxrwx---) + if let Ok(dir) = fs::metadata(parent) { + let perm = dir.permissions(); + let mode = perm.mode(); + // set group rwx if owner has it + let group = (mode & 0o700) >> 3; + let _ = fs::set_permissions(parent, fs::Permissions::from_mode(mode | group)); + } } // Load config @@ -52,6 +62,7 @@ pub fn run(config_path: String) { Ok(c) => c, Err(e) => { eprintln!("error: config: {}", e); + let _ = fs::remove_file(&pid_path); return; } }; @@ -59,12 +70,11 @@ pub fn run(config_path: String) { let listener = UnixListener::bind(&sock_path).expect("bind socket"); listener.set_nonblocking(true).ok(); - let state = State::new(); let mut procs: HashMap = HashMap::new(); let mut last_mtime: Option = config_mtime(&config_path); // Start enabled services - start_enabled(&config, &state, &mut procs); + start_enabled(&config, &mut procs); // Main loop loop { @@ -81,7 +91,7 @@ pub fn run(config_path: String) { last_mtime = Some(mtime); let new_config = Config::load(&config_path); if let Ok(new_config) = new_config { - reconcile(&new_config, &state, &mut procs); + reconcile(&new_config, &mut procs); config = new_config; } } @@ -92,7 +102,7 @@ pub fn run(config_path: String) { // Accept socket connections while let Ok((stream, _)) = listener.accept() { - handle_client(stream, &config, &state, &mut procs, &config_path); + handle_client(&stream, &config, &mut procs, &config_path); } std::thread::sleep(Duration::from_millis(200)); @@ -103,8 +113,8 @@ fn config_mtime(path: &str) -> Option { std::fs::metadata(path).ok().and_then(|m| m.modified().ok()) } -fn start_enabled(config: &Config, state: &State, procs: &mut HashMap) { - if let Ok(enabled) = state.list_enabled() { +fn start_enabled(config: &Config, procs: &mut HashMap) { + if let Ok(enabled) = crate::state::list_enabled() { for name in &enabled { if let Some(cfg) = config.service.get(name) { if !procs.contains_key(name) { @@ -117,7 +127,7 @@ fn start_enabled(config: &Config, state: &State, procs: &mut HashMap) { +fn reconcile(new_config: &Config, procs: &mut HashMap) { let old_names: Vec = procs.keys().cloned().collect(); for name in &old_names { @@ -127,7 +137,7 @@ fn reconcile(new_config: &Config, state: &State, procs: &mut HashMap Option { - let log_path = format!( - "{}/.local/share/rss/logs/{}.log", - std::env::var("HOME").unwrap_or_default(), - name - ); - if let Some(parent) = std::path::Path::new(&log_path).parent() { + let log_path = crate::state::log_path(name); + if let Some(parent) = log_path.parent() { let _ = std::fs::create_dir_all(parent); } + let log_path = log_path.to_string_lossy().to_string(); let cmd = cfg.resolve_command(name, &log_path); let mut child = match Command::new("sh") @@ -326,14 +333,13 @@ fn check_children(procs: &mut HashMap) { } fn handle_client( - mut stream: UnixStream, + mut stream: &UnixStream, config: &Config, - state: &State, procs: &mut HashMap, _config_path: &str, ) { let mut line = String::new(); - let mut reader = BufReader::new(&stream); + let mut reader = BufReader::new(stream); if reader.read_line(&mut line).is_err() { return; } @@ -387,7 +393,7 @@ fn handle_client( "list" | "list " => { let mut out = Vec::new(); for (name, _cfg) in &config.service { - let enabled = if state.is_enabled(name) { "yes" } else { "no" }; + let enabled = if crate::state::is_enabled(name) { "yes" } else { "no" }; let info = if let Some(p) = procs.get(name) { format!("running {} pid={} enabled={}", name, p.pid, enabled) } else { @@ -399,7 +405,7 @@ fn handle_client( } l if l.starts_with("status ") => { let name = &l[7..]; - let enabled = if state.is_enabled(name) { "yes" } else { "no" }; + let enabled = if crate::state::is_enabled(name) { "yes" } else { "no" }; if let Some(p) = procs.get(name) { format!("running {} pid={} enabled={}", name, p.pid, enabled) } else if config.service.contains_key(name) { @@ -411,7 +417,7 @@ fn handle_client( "status" => { let mut out = Vec::new(); for (name, _) in &config.service { - let enabled = if state.is_enabled(name) { "yes" } else { "no" }; + let enabled = if crate::state::is_enabled(name) { "yes" } else { "no" }; let info = if let Some(p) = procs.get(name) { format!("running {} pid={} enabled={}", name, p.pid, enabled) } else { diff --git a/src/ipc.rs b/src/ipc.rs index 9d7279c..cac9674 100644 --- a/src/ipc.rs +++ b/src/ipc.rs @@ -1,19 +1,8 @@ use std::io::{BufRead, BufReader, Write}; use std::os::unix::net::UnixStream; -use std::path::PathBuf; - -pub fn socket_path() -> PathBuf { - let home = std::env::var("HOME").expect("HOME not set"); - PathBuf::from(format!("{}/.local/share/rss/rss.sock", home)) -} - -pub fn daemon_pid_path() -> PathBuf { - let home = std::env::var("HOME").expect("HOME not set"); - PathBuf::from(format!("{}/.local/share/rss/daemon.pid", home)) -} pub fn send_cmd(cmd: &str) -> Result, String> { - let path = socket_path(); + let path = crate::state::socket_path(); if path.exists() { if UnixStream::connect(&path).is_err() { diff --git a/src/main.rs b/src/main.rs index c62921b..73140ea 100644 --- a/src/main.rs +++ b/src/main.rs @@ -8,7 +8,7 @@ mod supervisor; use clap::Parser; use cli::{Cli, Commands}; -use state::State; + fn main() { let cli = Cli::parse(); @@ -40,8 +40,7 @@ fn resolve_config_path(custom: Option<&str>) -> String { if let Ok(path) = std::env::var("RSS_CONFIG") { return path; } - let home = std::env::var("HOME").expect("HOME not set"); - format!("{}/.config/rss/config.toml", home) + state::config_dir().join("config.toml").to_string_lossy().to_string() } fn cmd_start(name: &str) { @@ -128,8 +127,7 @@ fn cmd_logs(name: &str, mut lines: Option, follow: bool) { lines = Some(10); } - let state = State::new(); - let log_path = state.log_path(name); + let log_path = state::log_path(name); if !log_path.exists() { eprintln!("error: no logs for '{}'", name); @@ -143,8 +141,6 @@ fn cmd_logs(name: &str, mut lines: Option, follow: bool) { } fn cmd_enable(name: &str, now: bool) { - let state = State::new(); - let config_path = resolve_config_path(None); let config = match config::Config::load(&config_path) { Ok(c) => c, @@ -159,7 +155,7 @@ fn cmd_enable(name: &str, now: bool) { return; } - if let Err(e) = state.enable(name) { + if let Err(e) = state::enable(name) { eprintln!("error: enabling '{}': {}", name, e); return; } @@ -172,7 +168,6 @@ fn cmd_enable(name: &str, now: bool) { } fn cmd_disable(name: &str) { - let state = State::new(); - state.disable(name); + state::disable(name); println!("disabled {}", name); } diff --git a/src/state.rs b/src/state.rs index d3716c8..94194f4 100644 --- a/src/state.rs +++ b/src/state.rs @@ -1,64 +1,84 @@ use std::path::PathBuf; -pub struct State { - pub state_dir: PathBuf, - config_dir: PathBuf, +pub fn runtime_dir() -> PathBuf { + if let Ok(dir) = std::env::var("RSS_STATE_DIR") { + return PathBuf::from(dir); + } + if let Ok(dir) = std::env::var("XDG_RUNTIME_DIR") { + return PathBuf::from(dir).join("rss"); + } + if let Ok(home) = std::env::var("HOME") { + return PathBuf::from(home).join(".local/share/rss"); + } + let user = std::env::var("USER").unwrap_or_else(|_| "unknown".into()); + PathBuf::from(format!("/tmp/rss-{}", user)) } -impl State { - pub fn new() -> Self { - let home = std::env::var("HOME").expect("HOME not set"); - State { - state_dir: PathBuf::from(format!("{}/.local/share/rss", home)), - config_dir: PathBuf::from(format!("{}/.config/rss", home)), - } +pub fn config_dir() -> PathBuf { + if let Ok(dir) = std::env::var("RSS_CONFIG_DIR") { + return PathBuf::from(dir); } - - pub fn log_path(&self, name: &str) -> PathBuf { - self.state_dir.join("logs").join(format!("{}.log", name)) - } - - pub fn enabled_path(&self, name: &str) -> PathBuf { - self.config_dir.join("enabled").join(name) - } - - pub fn write_pid(path: &PathBuf) -> std::io::Result<()> { - if let Some(parent) = path.parent() { - std::fs::create_dir_all(parent)?; - } - std::fs::write(path, format!("{}", std::process::id())) - } - - pub fn is_enabled(&self, name: &str) -> bool { - self.enabled_path(name).exists() - } - - pub fn enable(&self, name: &str) -> std::io::Result<()> { - let path = self.enabled_path(name); - if let Some(parent) = path.parent() { - std::fs::create_dir_all(parent)?; - } - std::fs::write(&path, "")?; - Ok(()) - } - - pub fn disable(&self, name: &str) { - let _ = std::fs::remove_file(self.enabled_path(name)); - } - - pub fn list_enabled(&self) -> std::io::Result> { - let dir = self.config_dir.join("enabled"); - if !dir.exists() { - return Ok(Vec::new()); - } - let mut names = Vec::new(); - for entry in std::fs::read_dir(dir)? { - let entry = entry?; - if let Some(name) = entry.file_name().to_str() { - names.push(name.to_string()); - } - } - names.sort(); - Ok(names) + if let Ok(home) = std::env::var("HOME") { + return PathBuf::from(home).join(".config/rss"); } + PathBuf::from("/etc/rss") +} + +pub fn socket_path() -> PathBuf { + if let Ok(p) = std::env::var("RSS_SOCKET") { + return PathBuf::from(p); + } + runtime_dir().join("rss.sock") +} + +pub fn daemon_pid_path() -> PathBuf { + runtime_dir().join("daemon.pid") +} + +pub fn log_path(name: &str) -> PathBuf { + runtime_dir().join("logs").join(format!("{}.log", name)) +} + +pub fn enabled_path(name: &str) -> PathBuf { + config_dir().join("enabled").join(name) +} + +pub fn write_pid(path: &PathBuf) -> std::io::Result<()> { + if let Some(parent) = path.parent() { + std::fs::create_dir_all(parent)?; + } + std::fs::write(path, format!("{}", std::process::id())) +} + +pub fn is_enabled(name: &str) -> bool { + enabled_path(name).exists() +} + +pub fn enable(name: &str) -> std::io::Result<()> { + let path = enabled_path(name); + if let Some(parent) = path.parent() { + std::fs::create_dir_all(parent)?; + } + std::fs::write(&path, "")?; + Ok(()) +} + +pub fn disable(name: &str) { + let _ = std::fs::remove_file(enabled_path(name)); +} + +pub fn list_enabled() -> std::io::Result> { + let dir = config_dir().join("enabled"); + if !dir.exists() { + return Ok(Vec::new()); + } + let mut names = Vec::new(); + for entry in std::fs::read_dir(dir)? { + let entry = entry?; + if let Some(name) = entry.file_name().to_str() { + names.push(name.to_string()); + } + } + names.sort(); + Ok(names) }