use anyhow::{Context, Result};
use std::path::{Path, PathBuf};
use std::sync::atomic::{AtomicI32, Ordering};
use std::sync::Arc;
use agent_orchestrator::state::InnerState;
static SIGTERM_SENDER_PID: AtomicI32 = AtomicI32::new(0);
static mut PREV_SIGTERM_ACTION: std::mem::MaybeUninit<libc::sigaction> =
std::mem::MaybeUninit::uninit();
extern "C" fn sigterm_sigaction_handler(
sig: libc::c_int,
info: *mut libc::siginfo_t,
ucontext: *mut libc::c_void,
) {
if !info.is_null() {
let sender_pid = unsafe {
#[cfg(target_os = "linux")]
{ (*info).si_pid() }
#[cfg(not(target_os = "linux"))]
{ (*info).si_pid }
};
SIGTERM_SENDER_PID.store(sender_pid, Ordering::SeqCst);
}
unsafe {
let prev = &*std::ptr::addr_of!(PREV_SIGTERM_ACTION).cast::<libc::sigaction>();
let handler = prev.sa_sigaction;
if handler == libc::SIG_DFL || handler == libc::SIG_IGN {
return;
}
if prev.sa_flags & libc::SA_SIGINFO != 0 {
let func: extern "C" fn(libc::c_int, *mut libc::siginfo_t, *mut libc::c_void) =
std::mem::transmute(handler);
func(sig, info, ucontext);
} else {
let func: extern "C" fn(libc::c_int) = std::mem::transmute(handler);
func(sig);
}
}
}
fn install_sigterm_siginfo_handler() -> Result<()> {
unsafe {
let mut sa: libc::sigaction = std::mem::zeroed();
sa.sa_sigaction = sigterm_sigaction_handler as *const () as usize;
sa.sa_flags = libc::SA_SIGINFO;
libc::sigemptyset(&mut sa.sa_mask);
let mut old_sa: libc::sigaction = std::mem::zeroed();
if libc::sigaction(libc::SIGTERM, &sa, &mut old_sa) != 0 {
return Err(anyhow::anyhow!(
"sigaction(SIGTERM) failed: {}",
std::io::Error::last_os_error()
));
}
std::ptr::addr_of_mut!(PREV_SIGTERM_ACTION)
.cast::<libc::sigaction>()
.write(old_sa);
}
Ok(())
}
pub fn sigterm_sender_pid() -> Option<i32> {
let pid = SIGTERM_SENDER_PID.load(Ordering::SeqCst);
if pid != 0 {
Some(pid)
} else {
None
}
}
pub fn socket_path(data_dir: &Path) -> PathBuf {
data_dir.join("orchestrator.sock")
}
pub fn pid_path(data_dir: &Path) -> PathBuf {
data_dir.join("daemon.pid")
}
pub fn write_pid_file(path: &Path) -> Result<()> {
if let Some(parent) = path.parent() {
std::fs::create_dir_all(parent).with_context(|| {
format!("failed to create PID file directory: {}", parent.display())
})?;
}
std::fs::write(path, std::process::id().to_string())
.with_context(|| format!("failed to write PID file: {}", path.display()))
}
pub fn read_pid_file(path: &Path) -> Option<u32> {
std::fs::read_to_string(path)
.ok()
.and_then(|s| s.trim().parse().ok())
}
#[cfg(unix)]
pub fn is_process_alive(pid: u32) -> bool {
nix::sys::signal::kill(nix::unistd::Pid::from_raw(pid as i32), None).is_ok()
}
#[cfg(unix)]
pub fn detect_stale_pid(pid_path: &Path) -> bool {
match read_pid_file(pid_path) {
Some(pid) => !is_process_alive(pid),
None => false,
}
}
#[cfg(unix)]
pub fn detect_running_daemon(pid_path: &Path) -> Option<u32> {
match read_pid_file(pid_path) {
Some(pid) if pid != std::process::id() && is_process_alive(pid) => Some(pid),
_ => None,
}
}
pub fn cleanup(socket_path: &Path, pid_path: &Path) {
let _ = std::fs::remove_file(socket_path);
let _ = std::fs::remove_file(pid_path);
}
pub async fn shutdown_signal(state: Arc<InnerState>) -> Result<()> {
let mut sigterm = tokio::signal::unix::signal(tokio::signal::unix::SignalKind::terminate())
.context("failed to install SIGTERM handler")?;
let mut sighup = tokio::signal::unix::signal(tokio::signal::unix::SignalKind::hangup())
.context("failed to install SIGHUP handler")?;
if let Err(e) = install_sigterm_siginfo_handler() {
tracing::warn!(error = %e, "failed to install SA_SIGINFO SIGTERM handler; sender PID will not be logged");
}
loop {
tokio::select! {
result = tokio::signal::ctrl_c() => {
if let Err(e) = result {
tracing::error!(error = %e, "ctrl_c handler failed");
}
tracing::info!("received SIGINT, shutting down");
state.daemon_runtime.request_shutdown();
break;
}
_ = sigterm.recv() => {
if let Some(sender) = sigterm_sender_pid() {
tracing::info!(sender_pid = sender, "received SIGTERM, shutting down");
} else {
tracing::info!("received SIGTERM, shutting down (sender PID unknown)");
}
state.daemon_runtime.request_shutdown();
break;
}
_ = sighup.recv() => {
tracing::info!("received SIGHUP, ignoring (daemon mode)");
}
}
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn detect_stale_pid_returns_true_for_dead_process() {
let dir = tempfile::tempdir().unwrap();
let pid_path = dir.path().join("daemon.pid");
std::fs::write(&pid_path, "2000000000").unwrap();
assert!(detect_stale_pid(&pid_path));
}
#[test]
fn detect_stale_pid_returns_false_for_current_process() {
let dir = tempfile::tempdir().unwrap();
let pid_path = dir.path().join("daemon.pid");
std::fs::write(&pid_path, std::process::id().to_string()).unwrap();
assert!(!detect_stale_pid(&pid_path));
}
#[test]
fn detect_stale_pid_returns_false_when_no_pid_file() {
let dir = tempfile::tempdir().unwrap();
let pid_path = dir.path().join("daemon.pid");
assert!(!detect_stale_pid(&pid_path));
}
#[test]
fn detect_running_daemon_returns_none_for_own_pid() {
let dir = tempfile::tempdir().unwrap();
let pid_path = dir.path().join("daemon.pid");
std::fs::write(&pid_path, std::process::id().to_string()).unwrap();
assert!(detect_running_daemon(&pid_path).is_none());
}
#[test]
fn detect_running_daemon_returns_none_for_dead_pid() {
let dir = tempfile::tempdir().unwrap();
let pid_path = dir.path().join("daemon.pid");
std::fs::write(&pid_path, "2000000000").unwrap();
assert!(detect_running_daemon(&pid_path).is_none());
}
#[test]
fn detect_running_daemon_returns_none_when_no_pid_file() {
let dir = tempfile::tempdir().unwrap();
let pid_path = dir.path().join("daemon.pid");
assert!(detect_running_daemon(&pid_path).is_none());
}
}