use std::fs;
use std::io::Write;
use std::path::{Path, PathBuf};
use anyhow::{Context, Result};
pub fn pid_path(project_root: &Path) -> PathBuf {
project_root.join(".code-graph").join("daemon.pid")
}
pub fn socket_path(project_root: &Path) -> PathBuf {
project_root.join(".code-graph").join("daemon.sock")
}
pub fn log_path(project_root: &Path) -> PathBuf {
project_root.join(".code-graph").join("daemon.log")
}
pub fn write_pid_file(project_root: &Path) -> Result<()> {
let path = pid_path(project_root);
if let Some(parent) = path.parent() {
fs::create_dir_all(parent)
.with_context(|| format!("failed to create directory {}", parent.display()))?;
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
let perms = fs::Permissions::from_mode(0o700);
fs::set_permissions(parent, perms)
.with_context(|| format!("failed to set permissions on {}", parent.display()))?;
}
}
let pid = std::process::id();
let mut opts = fs::OpenOptions::new();
opts.write(true).create(true).truncate(true);
#[cfg(unix)]
{
use std::os::unix::fs::OpenOptionsExt;
opts.mode(0o600);
}
let mut file = opts
.open(&path)
.with_context(|| format!("failed to write PID file {}", path.display()))?;
write!(file, "{}", pid)
.with_context(|| format!("failed to write PID to {}", path.display()))?;
Ok(())
}
pub fn read_pid_file(project_root: &Path) -> Option<u32> {
let path = pid_path(project_root);
let contents = fs::read_to_string(&path).ok()?;
contents.trim().parse::<u32>().ok()
}
pub fn remove_pid_file(project_root: &Path) -> Result<()> {
let path = pid_path(project_root);
match fs::remove_file(&path) {
Ok(()) => Ok(()),
Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(()),
Err(e) => Err(e).with_context(|| format!("failed to remove PID file {}", path.display())),
}
}
pub fn remove_socket_file(project_root: &Path) -> Result<()> {
let path = socket_path(project_root);
match fs::remove_file(&path) {
Ok(()) => Ok(()),
Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(()),
Err(e) => {
Err(e).with_context(|| format!("failed to remove socket file {}", path.display()))
}
}
}
pub fn is_daemon_running(project_root: &Path) -> bool {
let Some(pid) = read_pid_file(project_root) else {
return false;
};
process_is_alive(pid)
}
#[cfg(unix)]
fn process_is_alive(pid: u32) -> bool {
unsafe { libc::kill(pid as libc::pid_t, 0) == 0 }
}
#[cfg(not(unix))]
fn process_is_alive(_pid: u32) -> bool {
true
}
#[cfg(test)]
mod tests {
use super::*;
use tempfile::TempDir;
#[test]
fn pid_file_roundtrip() {
let tmp = TempDir::new().unwrap();
let root = tmp.path();
assert!(read_pid_file(root).is_none());
write_pid_file(root).unwrap();
let pid = read_pid_file(root);
assert!(pid.is_some());
assert_eq!(pid.unwrap(), std::process::id());
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
let meta = std::fs::metadata(pid_path(root)).unwrap();
assert_eq!(meta.permissions().mode() & 0o777, 0o600);
}
remove_pid_file(root).unwrap();
assert!(read_pid_file(root).is_none());
}
#[test]
fn remove_pid_file_idempotent() {
let tmp = TempDir::new().unwrap();
let root = tmp.path();
remove_pid_file(root).unwrap();
remove_pid_file(root).unwrap();
}
#[test]
fn remove_socket_file_idempotent() {
let tmp = TempDir::new().unwrap();
let root = tmp.path();
remove_socket_file(root).unwrap();
remove_socket_file(root).unwrap();
}
#[test]
fn is_daemon_running_with_no_pid_file() {
let tmp = TempDir::new().unwrap();
assert!(!is_daemon_running(tmp.path()));
}
#[test]
fn is_daemon_running_with_current_process() {
let tmp = TempDir::new().unwrap();
let root = tmp.path();
write_pid_file(root).unwrap();
assert!(is_daemon_running(root));
remove_pid_file(root).unwrap();
}
#[test]
fn is_daemon_running_with_dead_pid() {
let tmp = TempDir::new().unwrap();
let root = tmp.path();
let path = pid_path(root);
std::fs::create_dir_all(path.parent().unwrap()).unwrap();
std::fs::write(&path, "99999").unwrap();
let _ = is_daemon_running(root);
}
#[test]
fn socket_path_is_correct() {
let root = Path::new("/projects/myapp");
assert_eq!(
socket_path(root),
PathBuf::from("/projects/myapp/.code-graph/daemon.sock")
);
}
#[test]
fn pid_path_is_correct() {
let root = Path::new("/projects/myapp");
assert_eq!(
pid_path(root),
PathBuf::from("/projects/myapp/.code-graph/daemon.pid")
);
}
#[test]
fn read_pid_file_with_invalid_content() {
let tmp = TempDir::new().unwrap();
let root = tmp.path();
let path = pid_path(root);
std::fs::create_dir_all(path.parent().unwrap()).unwrap();
std::fs::write(&path, "not-a-number").unwrap();
assert!(read_pid_file(root).is_none());
}
}