centralize: daemon syncs state with config, cleanup orphans

- rss daemon kills and cleans services not in config (rename-safe)
- rss list also prunes orphaned state
- rss start checks for already running before spawning
- State: add supervise_dir/service_dir/list_pid_names helpers
This commit is contained in:
Niko Marmeladkov 2026-07-03 11:20:36 +03:00
parent 74b9539149
commit a7ce8405d1
Signed by untrusted user who does not match committer: Niko
GPG key ID: E3B955F9442D44E3
2 changed files with 78 additions and 4 deletions

View file

@ -74,6 +74,12 @@ fn cmd_start(name: &str, config: &Config, config_path: &str) {
eprintln!("error: service '{}' not found in config", name);
std::process::exit(1);
}
let state = State::new();
let svc_pid = state.service_pid_path(name);
if State::read_pid(&svc_pid).map_or(false, |p| process_running(p)) {
eprintln!("error: '{}' is already running", name);
std::process::exit(1);
}
let self_exe = std::env::current_exe().unwrap_or_else(|_| {
eprintln!("error: cannot determine binary path");
std::process::exit(1);
@ -251,6 +257,7 @@ fn cmd_disable(name: &str, state: &State) {
}
fn cmd_list(config: &Config, state: &State) {
cleanup_orphans(config, state);
let mut names: Vec<&String> = config.service.keys().collect();
names.sort();
@ -292,7 +299,55 @@ fn cmd_list(config: &Config, state: &State) {
}
}
fn cleanup_orphans(config: &Config, state: &State) {
for name in State::list_pid_names(&state.supervise_dir()) {
if !config.service.contains_key(&name) {
stop_orphan(&name, state);
}
}
for name in State::list_pid_names(&state.service_dir()) {
if !config.service.contains_key(&name) {
State::remove_pid(&state.service_pid_path(&name));
}
}
}
fn stop_orphan(name: &str, state: &State) {
let svc = state.service_pid_path(name);
let sup = state.supervise_pid_path(name);
if let Some(pid) = State::read_pid(&svc) {
if process_running(pid) {
let _ = nix::sys::signal::kill(
nix::unistd::Pid::from_raw(pid as i32),
nix::sys::signal::Signal::SIGTERM,
);
wait_for_exit(pid, 5);
if process_running(pid) {
let _ = nix::sys::signal::kill(
nix::unistd::Pid::from_raw(pid as i32),
nix::sys::signal::Signal::SIGKILL,
);
wait_for_exit(pid, 3);
}
}
}
if let Some(pid) = State::read_pid(&sup) {
if process_running(pid) {
let _ = nix::sys::signal::kill(
nix::unistd::Pid::from_raw(pid as i32),
nix::sys::signal::Signal::SIGKILL,
);
wait_for_exit(pid, 3);
}
}
State::remove_pid(&svc);
State::remove_pid(&sup);
}
fn cmd_daemon(config: &Config, state: &State, config_path: &str) {
cleanup_orphans(config, state);
let enabled = state.list_enabled().unwrap_or_default();
if enabled.is_empty() {
println!("no enabled services");
@ -300,15 +355,12 @@ fn cmd_daemon(config: &Config, state: &State, config_path: &str) {
}
for name in &enabled {
if !config.service.contains_key(name) {
continue;
}
let svc_pid_path = state.service_pid_path(name);
let already = State::read_pid(&svc_pid_path).map_or(false, |p| process_running(p));
if already {
continue;
}
println!("starting {}...", name);
println!("{}", name);
cmd_start(name, config, config_path);
}
}

View file

@ -26,10 +26,32 @@ impl State {
self.state_dir.join("logs").join(format!("{}.log", name))
}
pub fn supervise_dir(&self) -> PathBuf {
self.state_dir.join("supervise")
}
pub fn service_dir(&self) -> PathBuf {
self.state_dir.join("service")
}
pub fn enabled_path(&self, name: &str) -> PathBuf {
self.config_dir.join("enabled").join(name)
}
pub fn list_pid_names(dir: &PathBuf) -> Vec<String> {
let mut names = Vec::new();
if let Ok(entries) = std::fs::read_dir(dir) {
for entry in entries.flatten() {
let fname = entry.file_name();
let fname = fname.to_string_lossy().to_string();
if let Some(stripped) = fname.strip_suffix(".pid") {
names.push(stripped.to_string());
}
}
}
names
}
pub fn write_pid(path: &PathBuf) -> std::io::Result<()> {
Self::write_pid_with(path, std::process::id())
}