refactor paths: remove HOME dependency, use env/XDG/tmp fallback
- 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
This commit is contained in:
parent
b069c527bd
commit
7383445ea5
5 changed files with 116 additions and 105 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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<String, Proc> = HashMap::new();
|
||||
let mut last_mtime: Option<SystemTime> = 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<SystemTime> {
|
|||
std::fs::metadata(path).ok().and_then(|m| m.modified().ok())
|
||||
}
|
||||
|
||||
fn start_enabled(config: &Config, state: &State, procs: &mut HashMap<String, Proc>) {
|
||||
if let Ok(enabled) = state.list_enabled() {
|
||||
fn start_enabled(config: &Config, procs: &mut HashMap<String, Proc>) {
|
||||
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<String, Pro
|
|||
}
|
||||
}
|
||||
|
||||
fn reconcile(new_config: &Config, state: &State, procs: &mut HashMap<String, Proc>) {
|
||||
fn reconcile(new_config: &Config, procs: &mut HashMap<String, Proc>) {
|
||||
let old_names: Vec<String> = procs.keys().cloned().collect();
|
||||
|
||||
for name in &old_names {
|
||||
|
|
@ -127,7 +137,7 @@ fn reconcile(new_config: &Config, state: &State, procs: &mut HashMap<String, Pro
|
|||
}
|
||||
|
||||
for (name, cfg) in &new_config.service {
|
||||
if !procs.contains_key(name) && state.is_enabled(name) {
|
||||
if !procs.contains_key(name) && crate::state::is_enabled(name) {
|
||||
if let Some(p) = spawn(name, cfg) {
|
||||
procs.insert(name.clone(), p);
|
||||
}
|
||||
|
|
@ -136,14 +146,11 @@ fn reconcile(new_config: &Config, state: &State, procs: &mut HashMap<String, Pro
|
|||
}
|
||||
|
||||
fn spawn(name: &str, cfg: &ServiceConfig) -> Option<Proc> {
|
||||
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<String, Proc>) {
|
|||
}
|
||||
|
||||
fn handle_client(
|
||||
mut stream: UnixStream,
|
||||
mut stream: &UnixStream,
|
||||
config: &Config,
|
||||
state: &State,
|
||||
procs: &mut HashMap<String, Proc>,
|
||||
_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 {
|
||||
|
|
|
|||
13
src/ipc.rs
13
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<Vec<String>, String> {
|
||||
let path = socket_path();
|
||||
let path = crate::state::socket_path();
|
||||
|
||||
if path.exists() {
|
||||
if UnixStream::connect(&path).is_err() {
|
||||
|
|
|
|||
15
src/main.rs
15
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<usize>, 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<usize>, 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);
|
||||
}
|
||||
|
|
|
|||
134
src/state.rs
134
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<Vec<String>> {
|
||||
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<Vec<String>> {
|
||||
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)
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue