use std::fs;
use std::io;
use std::os::unix::io::AsRawFd;
use std::path::Path;
use std::process;
use std::thread;
use std::time::Duration;
pub const SYSTEM_PID_FILE: &str = "/run/pwr/pwr-server.pid";
pub fn user_pid_file() -> std::path::PathBuf {
crate::config::user_runtime_dir().join("pwr-server.pid")
}
pub fn pid_file_for_config(config_path: &Path) -> std::path::PathBuf {
let system_base = crate::config::system_config_dir();
if config_path.starts_with(&system_base) {
std::path::PathBuf::from(SYSTEM_PID_FILE)
} else {
user_pid_file()
}
}
pub fn daemonize(pid_file: &Path) -> Result<(), String> {
match unsafe { libc::fork() } {
-1 => return Err(format!("First fork failed: {}", io::Error::last_os_error())),
0 => {} _ => process::exit(0), }
if unsafe { libc::setsid() } == -1 {
return Err(format!("setsid failed: {}", io::Error::last_os_error()));
}
match unsafe { libc::fork() } {
-1 => return Err(format!("Second fork failed: {}", io::Error::last_os_error())),
0 => {} _ => process::exit(0), }
if unsafe { libc::chdir(b"/\0".as_ptr() as *const _) } == -1 {
return Err(format!("chdir(/) failed: {}", io::Error::last_os_error()));
}
unsafe {
libc::umask(0);
}
redirect_stdio_to_devnull()?;
write_pid_file(pid_file)?;
Ok(())
}
fn redirect_stdio_to_devnull() -> Result<(), String> {
let devnull = fs::OpenOptions::new()
.read(true)
.write(true)
.open("/dev/null")
.map_err(|e| format!("Cannot open /dev/null: {}", e))?;
let devnull_fd = devnull.as_raw_fd();
for fd in [libc::STDIN_FILENO, libc::STDOUT_FILENO, libc::STDERR_FILENO] {
if fd != devnull_fd {
if unsafe { libc::dup2(devnull_fd, fd) } == -1 {
return Err(format!(
"Cannot redirect fd {} to /dev/null: {}",
fd,
io::Error::last_os_error()
));
}
}
}
Ok(())
}
fn write_pid_file(path: &Path) -> Result<(), String> {
if let Some(parent) = path.parent() {
fs::create_dir_all(parent)
.map_err(|e| format!("Cannot create PID directory {}: {}", parent.display(), e))?;
}
let pid = unsafe { libc::getpid() };
let contents = format!("{}\n", pid);
fs::write(path, &contents)
.map_err(|e| format!("Cannot write PID file {}: {}", path.display(), e))?;
Ok(())
}
pub fn stop_daemon(pid_file: &Path) -> Result<String, String> {
let pid = read_pid_file(pid_file)?;
if !is_process_running(pid) {
remove_pid_file(pid_file)?;
return Err(format!(
"PID {} is not running (stale PID file removed)",
pid
));
}
send_signal(pid, libc::SIGTERM)?;
let grace = Duration::from_secs(5);
let poll = Duration::from_millis(100);
let deadline = std::time::Instant::now() + grace;
let mut clean_exit = false;
while std::time::Instant::now() < deadline {
if !is_process_running(pid) {
clean_exit = true;
break;
}
thread::sleep(poll);
}
if clean_exit {
remove_pid_file(pid_file)?;
return Ok(format!("pwr-server (PID {}) stopped gracefully", pid));
}
send_signal(pid, libc::SIGKILL)?;
thread::sleep(Duration::from_millis(200));
if is_process_running(pid) {
return Err(format!(
"Failed to kill pwr-server (PID {}) — process did not respond to SIGKILL",
pid
));
}
remove_pid_file(pid_file)?;
Ok(format!(
"pwr-server (PID {}) killed (did not respond to SIGTERM within {}s)",
pid,
grace.as_secs()
))
}
fn read_pid_file(path: &Path) -> Result<libc::pid_t, String> {
let contents = fs::read_to_string(path)
.map_err(|e| format!("Cannot read PID file {}: {}", path.display(), e))?;
let pid: libc::pid_t = contents
.trim()
.parse()
.map_err(|e| format!("Invalid PID in {}: {}", path.display(), e))?;
if pid <= 0 {
return Err(format!("Invalid PID ({}) in {}", pid, path.display()));
}
Ok(pid)
}
fn is_process_running(pid: libc::pid_t) -> bool {
unsafe { libc::kill(pid, 0) == 0 }
}
fn send_signal(pid: libc::pid_t, signal: libc::c_int) -> Result<(), String> {
if unsafe { libc::kill(pid, signal) } == -1 {
return Err(format!(
"Cannot send signal {} to PID {}: {}",
signal,
pid,
io::Error::last_os_error()
));
}
Ok(())
}
fn remove_pid_file(path: &Path) -> Result<(), String> {
match fs::remove_file(path) {
Ok(()) => Ok(()),
Err(e) if e.kind() == io::ErrorKind::NotFound => Ok(()),
Err(e) => Err(format!(
"Cannot remove PID file {}: {}",
path.display(),
e
)),
}
}