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:
Niko Marmeladkov 2026-08-18 18:22:17 +03:00
parent 21fc895b0e
commit 548b22a55f
Signed by untrusted user who does not match committer: Niko
GPG key ID: E3B955F9442D44E3
2 changed files with 48 additions and 3 deletions

View file

@ -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;

View file

@ -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()
}