fix(daemon): prevent double-start and process-group leaks
- flock-lock the daemon pid file so a second daemon refuses to start, avoiding two supervisors spawning the same services - put each service in its own process group (setpgid) and kill the whole group on stop/force-kill, so pipelines/background children of sh -c no longer leak as orphans - SIGKILL leftover group members when a service exits unexpectedly
This commit is contained in:
parent
21fc895b0e
commit
548b22a55f
2 changed files with 48 additions and 3 deletions
|
|
@ -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<Proc> {
|
|||
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<String, Proc>) {
|
|||
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<String, Proc>) {
|
|||
// 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<String, Proc>) {
|
|||
|
||||
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;
|
||||
|
|
|
|||
20
src/state.rs
20
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<std::fs::File> {
|
||||
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()
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue