diff --git a/src/daemon.rs b/src/daemon.rs index d52bf38..c1c4832 100644 --- a/src/daemon.rs +++ b/src/daemon.rs @@ -39,6 +39,20 @@ pub fn run(config_path: String) { let _ = nix::sys::signal::sigaction(nix::sys::signal::Signal::SIGINT, &sig_action); } + // Single-instance guard: fail if another daemon already holds the lock. + // This prevents two supervisors from double-starting the same services. + let _daemon_lock = match state::lock_daemon() { + Ok(f) => f, + Err(e) => { + eprintln!( + "error: another daemon is already running (lock {:?}): {}", + state::daemon_pid_path(), + e + ); + return; + } + }; + // Write daemon PID let pid_path = state::daemon_pid_path(); if let Err(e) = state::write_pid(&pid_path) { @@ -185,6 +199,9 @@ fn spawn(name: &str, cfg: &ServiceConfig) -> Option { libc::_exit(1); } libc::prctl(libc::PR_SET_PDEATHSIG, libc::SIGTERM); + // Put the service in its own process group so we can kill the + // whole tree (sh + grandchildren) instead of leaking orphans. + libc::setpgid(0, 0); Ok(()) }); } @@ -246,9 +263,10 @@ fn stop(name: &str, procs: &mut HashMap) { let _ = Command::new("sh").arg("-c").arg(&cmd).output(); } - // signal + // signal the whole process group (pid negative) so sh and any + // grandchildren all get it — otherwise background/pipeline children leak let sig = crate::supervisor::parse_signal(&p.cfg.stop_signal); - let _ = nix::sys::signal::kill(nix::unistd::Pid::from_raw(pid as i32), sig); + let _ = nix::sys::signal::kill(nix::unistd::Pid::from_raw(-(pid as i32)), sig); // wait let deadline = std::time::Instant::now() + Duration::from_secs(p.cfg.timeout_stop); @@ -262,7 +280,7 @@ fn stop(name: &str, procs: &mut HashMap) { // force kill if let Ok(None) = procs.get_mut(name).unwrap().child.try_wait() { let _ = nix::sys::signal::kill( - nix::unistd::Pid::from_raw(pid as i32), + nix::unistd::Pid::from_raw(-(pid as i32)), nix::sys::signal::Signal::SIGKILL, ); let _ = procs.get_mut(name).unwrap().child.wait(); @@ -298,6 +316,13 @@ fn check_children(procs: &mut HashMap) { append_log(&log_path, &format!("O: exited with {}", code)); + // Clean up anything still running in the dead service's process + // group (e.g. background/pipeline children of `sh -c`). + let _ = nix::sys::signal::kill( + nix::unistd::Pid::from_raw(-(p.pid as i32)), + nix::sys::signal::Signal::SIGKILL, + ); + if p.cfg.oneshot { dead.push(name.clone()); continue; diff --git a/src/state.rs b/src/state.rs index a391741..e9fad36 100644 --- a/src/state.rs +++ b/src/state.rs @@ -1,3 +1,4 @@ +use std::os::unix::io::AsRawFd; use std::path::PathBuf; pub fn runtime_dir() -> PathBuf { @@ -105,6 +106,25 @@ pub fn write_pid(path: &PathBuf) -> std::io::Result<()> { std::fs::write(path, format!("{}", std::process::id())) } +/// Acquire an exclusive lock on the daemon pid file. +/// Held for the lifetime of the returned `File`; a second daemon fails here. +pub fn lock_daemon() -> std::io::Result { + let path = daemon_pid_path(); + if let Some(parent) = path.parent() { + std::fs::create_dir_all(parent)?; + } + let f = std::fs::OpenOptions::new() + .create(true) + .read(true) + .write(true) + .open(&path)?; + let rc = unsafe { libc::flock(f.as_raw_fd(), libc::LOCK_EX | libc::LOCK_NB) }; + if rc != 0 { + return Err(std::io::Error::last_os_error()); + } + Ok(f) +} + pub fn is_enabled(name: &str) -> bool { enabled_path(name).exists() }