#[cfg(not(unix))]
use anyhow::anyhow;
use anyhow::{Context, Result};
use std::fs;
use std::path::Path;
use std::time::Duration;
#[cfg(unix)]
use nix::sys::signal::{self, Signal};
#[cfg(unix)]
use nix::unistd::Pid;
pub fn write_pid_file(path: &Path, pid: u32) -> Result<()> {
crate::core::fs::write_sensitive_file(path, pid.to_string())
.with_context(|| format!("Failed to write PID file: {:?}", path))
}
pub fn read_pid_file(path: &Path) -> Result<Option<u32>> {
if !path.exists() {
return Ok(None);
}
let content =
fs::read_to_string(path).with_context(|| format!("Failed to read PID file: {:?}", path))?;
let pid: u32 = content
.trim()
.parse()
.with_context(|| format!("Invalid PID in file: {}", content.trim()))?;
Ok(Some(pid))
}
#[cfg(unix)]
pub fn is_process_running(pid: u32) -> bool {
signal::kill(Pid::from_raw(pid as i32), None).is_ok()
}
#[cfg(not(unix))]
pub fn is_process_running(_pid: u32) -> bool {
false
}
#[cfg(unix)]
pub fn socket_is_connectable(path: &Path) -> bool {
std::os::unix::net::UnixStream::connect(path).is_ok()
}
#[cfg(not(unix))]
pub fn socket_is_connectable(_path: &Path) -> bool {
false
}
#[cfg(unix)]
pub fn spawn_detached(args: &[&str], log_path: &Path) -> Result<u32> {
use std::os::unix::process::CommandExt;
use std::process::Command;
let exe = std::env::current_exe().context("Failed to get current executable path")?;
let log_file = fs::File::create(log_path)
.with_context(|| format!("Failed to create log file: {:?}", log_path))?;
let log_file_err = log_file
.try_clone()
.context("Failed to clone log file handle")?;
let mut cmd = Command::new(&exe);
for arg in args {
cmd.arg(arg);
}
cmd.stdout(log_file).stderr(log_file_err);
unsafe {
cmd.pre_exec(|| {
nix::unistd::setsid().map_err(std::io::Error::other)?;
Ok(())
});
}
let child = cmd.spawn().context("Failed to spawn daemon process")?;
Ok(child.id())
}
#[cfg(not(unix))]
pub fn spawn_detached(_args: &[&str], _log_path: &Path) -> Result<u32> {
Err(anyhow!(
"Daemonization not supported on this platform. Use --foreground."
))
}
#[cfg(unix)]
pub fn terminate_process(pid: u32, timeout: Duration) -> Result<()> {
signal::kill(Pid::from_raw(pid as i32), Signal::SIGTERM)
.with_context(|| format!("Failed to send SIGTERM to PID {}", pid))?;
let start = std::time::Instant::now();
while start.elapsed() < timeout {
if !is_process_running(pid) {
return Ok(());
}
std::thread::sleep(Duration::from_millis(100));
}
if is_process_running(pid) {
eprintln!("Process did not terminate gracefully, sending SIGKILL...");
let _ = signal::kill(Pid::from_raw(pid as i32), Signal::SIGKILL);
}
Ok(())
}
#[cfg(not(unix))]
pub fn terminate_process(_pid: u32, _timeout: Duration) -> Result<()> {
Err(anyhow!("Stopping agent not supported on this platform"))
}
pub fn cleanup_stale_files(paths: &[&Path]) {
for path in paths {
let _ = fs::remove_file(path);
}
}
#[cfg(test)]
mod tests {
use super::*;
use tempfile::TempDir;
#[test]
fn write_and_read_pid_file_round_trip() {
let dir = TempDir::new().unwrap();
let pid_path = dir.path().join("test.pid");
write_pid_file(&pid_path, 42).unwrap();
let read_back = read_pid_file(&pid_path).unwrap();
assert_eq!(read_back, Some(42));
}
#[test]
fn read_pid_file_missing_returns_none() {
let dir = TempDir::new().unwrap();
let pid_path = dir.path().join("nonexistent.pid");
assert_eq!(read_pid_file(&pid_path).unwrap(), None);
}
#[test]
fn read_pid_file_invalid_content_errors() {
let dir = TempDir::new().unwrap();
let pid_path = dir.path().join("bad.pid");
fs::write(&pid_path, "not-a-number").unwrap();
assert!(read_pid_file(&pid_path).is_err());
}
#[test]
fn cleanup_stale_files_removes_existing() {
let dir = TempDir::new().unwrap();
let f1 = dir.path().join("a");
let f2 = dir.path().join("b");
fs::write(&f1, "x").unwrap();
fs::write(&f2, "y").unwrap();
cleanup_stale_files(&[&f1, &f2]);
assert!(!f1.exists());
assert!(!f2.exists());
}
#[test]
fn cleanup_stale_files_ignores_missing() {
let dir = TempDir::new().unwrap();
let missing = dir.path().join("gone");
cleanup_stale_files(&[&missing]);
}
#[cfg(unix)]
#[test]
fn is_process_running_detects_current_process() {
assert!(is_process_running(std::process::id()));
}
#[cfg(unix)]
#[test]
fn is_process_running_false_for_nonexistent() {
assert!(!is_process_running(999_999_999));
}
}