use std::fs;
use std::io;
use std::path::PathBuf;
use std::process;
#[cfg(unix)]
use std::os::unix::fs::{DirBuilderExt, OpenOptionsExt};
fn pid_dir() -> PathBuf {
let base = if let Ok(xdg) = std::env::var("XDG_RUNTIME_DIR") {
PathBuf::from(xdg)
} else {
let home = std::env::var("HOME").unwrap_or_else(|_| "/tmp".to_string());
PathBuf::from(home).join(".local").join("state")
};
base.join("semantic-diff")
}
fn ensure_pid_dir() -> io::Result<PathBuf> {
let dir = pid_dir();
if !dir.exists() {
#[cfg(unix)]
{
fs::DirBuilder::new()
.recursive(true)
.mode(0o700)
.create(&dir)?;
}
#[cfg(not(unix))]
{
fs::create_dir_all(&dir)?;
}
}
Ok(dir)
}
pub fn pid_file_path() -> PathBuf {
pid_dir().join("semantic-diff.pid")
}
pub fn log_file_path() -> PathBuf {
pid_dir().join("semantic-diff.log")
}
pub fn write_pid_file() -> io::Result<()> {
let dir = ensure_pid_dir()?;
let pid_path = dir.join("semantic-diff.pid");
let tmp_path = dir.join(".semantic-diff.pid.tmp");
let _ = fs::remove_file(&tmp_path);
{
let mut opts = fs::OpenOptions::new();
opts.write(true).create_new(true);
#[cfg(unix)]
opts.mode(0o600);
let mut file = opts.open(&tmp_path)?;
io::Write::write_all(&mut file, process::id().to_string().as_bytes())?;
}
fs::rename(&tmp_path, &pid_path)?;
Ok(())
}
pub fn remove_pid_file() {
let _ = fs::remove_file(pid_file_path());
}
fn validate_pid_ownership(pid: u32) -> bool {
if pid == 0 {
return false;
}
#[cfg(target_os = "macos")]
{
let output = std::process::Command::new("ps")
.args(["-p", &pid.to_string(), "-o", "comm="])
.output();
match output {
Ok(out) => {
let comm = String::from_utf8_lossy(&out.stdout);
comm.contains("semantic-diff")
}
Err(_) => false,
}
}
#[cfg(target_os = "linux")]
{
let comm_path = format!("/proc/{}/comm", pid);
match fs::read_to_string(&comm_path) {
Ok(comm) => comm.trim().contains("semantic-diff"),
Err(_) => false,
}
}
#[cfg(not(any(target_os = "macos", target_os = "linux")))]
{
true
}
}
#[allow(dead_code)]
pub fn read_pid() -> Option<u32> {
let pid_path = pid_file_path();
let pid: u32 = fs::read_to_string(pid_path).ok()?.trim().parse().ok()?;
if validate_pid_ownership(pid) {
Some(pid)
} else {
None
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::env;
#[test]
fn pid_dir_uses_xdg_runtime_dir_when_set() {
let test_dir = "/tmp/test-xdg-signal";
env::set_var("XDG_RUNTIME_DIR", test_dir);
let dir = pid_dir();
env::remove_var("XDG_RUNTIME_DIR");
assert!(
dir.starts_with(test_dir),
"pid_dir() should start with XDG_RUNTIME_DIR, got {:?}",
dir
);
assert!(
dir.ends_with("semantic-diff"),
"pid_dir() should end with 'semantic-diff', got {:?}",
dir
);
}
#[test]
fn pid_dir_falls_back_to_home_local_state() {
let saved = env::var("XDG_RUNTIME_DIR").ok();
env::remove_var("XDG_RUNTIME_DIR");
let dir = pid_dir();
if let Some(v) = saved {
env::set_var("XDG_RUNTIME_DIR", v);
}
assert!(
dir.ends_with("semantic-diff"),
"pid_dir() fallback should end with 'semantic-diff', got {:?}",
dir
);
}
#[test]
fn write_pid_file_creates_file_with_correct_pid() {
let test_dir = tempfile::tempdir().unwrap();
let dir = test_dir.path().join("semantic-diff");
fs::create_dir_all(&dir).unwrap();
let pid_path = dir.join("semantic-diff.pid");
let tmp_path = dir.join(".semantic-diff.pid.tmp");
let _ = fs::remove_file(&tmp_path);
{
let mut file = fs::OpenOptions::new()
.write(true)
.create_new(true)
.open(&tmp_path)
.unwrap();
io::Write::write_all(&mut file, process::id().to_string().as_bytes()).unwrap();
}
fs::rename(&tmp_path, &pid_path).unwrap();
let content = fs::read_to_string(&pid_path).unwrap();
assert_eq!(
content.trim(),
process::id().to_string(),
"PID file should contain current PID"
);
}
#[test]
fn read_pid_returns_none_for_nonexistent_file() {
let test_dir = tempfile::tempdir().unwrap();
env::set_var("XDG_RUNTIME_DIR", test_dir.path());
let result = read_pid();
env::remove_var("XDG_RUNTIME_DIR");
assert_eq!(result, None, "read_pid should return None when file doesn't exist");
}
#[test]
fn validate_pid_ownership_returns_false_for_invalid_pids() {
assert!(!validate_pid_ownership(0), "PID 0 should be invalid");
assert!(
!validate_pid_ownership(999_999_999),
"Very large PID should be invalid (process unlikely to exist)"
);
}
#[test]
fn atomic_write_creates_file_after_write() {
let test_dir = tempfile::tempdir().unwrap();
let dir = test_dir.path().join("semantic-diff");
fs::create_dir_all(&dir).unwrap();
let pid_path = dir.join("semantic-diff.pid");
let tmp_path = dir.join(".semantic-diff.pid.tmp");
let _ = fs::remove_file(&tmp_path);
fs::write(&tmp_path, "12345").unwrap();
fs::rename(&tmp_path, &pid_path).unwrap();
assert!(pid_path.exists(), "PID file should exist after atomic write");
assert!(!tmp_path.exists(), "Temp file should not exist after atomic write");
}
}