use std::path::{Path, PathBuf};
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DaemonActive {
pub project: PathBuf,
pub pid: u32,
pub socket: PathBuf,
}
pub fn active_file_path() -> PathBuf {
dirs::cache_dir()
.unwrap_or_else(|| PathBuf::from(".cache"))
.join("tldr")
.join("daemon-active.json")
}
pub fn write_active(project: &Path, pid: u32, socket: &Path) -> std::io::Result<()> {
let path = active_file_path();
if let Some(parent) = path.parent() {
std::fs::create_dir_all(parent)?;
}
let record = DaemonActive {
project: project.to_path_buf(),
pid,
socket: socket.to_path_buf(),
};
let json = serde_json::to_string_pretty(&record).map_err(std::io::Error::other)?;
let tmp = path.with_extension("json.tmp");
std::fs::write(&tmp, json)?;
std::fs::rename(&tmp, &path)?;
Ok(())
}
pub fn read_active() -> Option<DaemonActive> {
let path = active_file_path();
let content = std::fs::read_to_string(&path).ok()?;
let parsed: DaemonActive = serde_json::from_str(&content).ok()?;
if !is_pid_alive(parsed.pid) {
return None;
}
Some(parsed)
}
pub fn remove_active() -> std::io::Result<()> {
match std::fs::remove_file(active_file_path()) {
Ok(()) => Ok(()),
Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(()),
Err(e) => Err(e),
}
}
#[cfg(unix)]
fn is_pid_alive(pid: u32) -> bool {
let rc = unsafe { libc::kill(pid as i32, 0) };
if rc == 0 {
return true;
}
matches!(
std::io::Error::last_os_error().raw_os_error(),
Some(libc::EPERM)
)
}
#[cfg(not(unix))]
fn is_pid_alive(_pid: u32) -> bool {
true
}
#[cfg(test)]
mod tests {
use super::*;
use tempfile::TempDir;
#[test]
fn write_then_read_round_trips() {
let tmp = TempDir::new().expect("tempdir");
let cache_root = tmp.path().to_path_buf();
let project = tmp.path().join("project");
std::fs::create_dir_all(&project).unwrap();
let socket = tmp.path().join("tldr-deadbeef.sock");
let record = DaemonActive {
project: project.clone(),
pid: std::process::id(),
socket: socket.clone(),
};
let json = serde_json::to_string(&record).unwrap();
let parsed: DaemonActive = serde_json::from_str(&json).unwrap();
assert_eq!(parsed.project, project);
assert_eq!(parsed.pid, std::process::id());
assert_eq!(parsed.socket, socket);
assert!(cache_root.exists());
}
#[cfg(unix)]
#[test]
fn pid_zero_is_not_alive() {
let mut child = std::process::Command::new("true")
.spawn()
.expect("spawn true");
let pid = child.id();
let _ = child.wait();
assert!(!is_pid_alive(pid), "reaped child PID should not be alive");
}
#[cfg(unix)]
#[test]
fn current_process_is_alive() {
assert!(is_pid_alive(std::process::id()));
}
}