use anyhow::{Context, Result};
use serde::{Deserialize, Serialize};
use std::path::{Path, PathBuf};
#[derive(Debug, Serialize, Deserialize)]
pub struct DaemonEntry {
pub pid: u32,
pub dir: String,
#[serde(default)]
pub socket_path: Option<String>,
}
fn registry_dir() -> Result<PathBuf> {
let dir = if let Ok(runtime) = std::env::var("XDG_RUNTIME_DIR") {
PathBuf::from(runtime).join("cryo")
} else {
let home = std::env::var("HOME").context("HOME not set")?;
PathBuf::from(home).join(".cryo").join("daemons")
};
std::fs::create_dir_all(&dir)?;
Ok(dir)
}
fn entry_filename(dir: &Path) -> String {
use std::hash::{Hash, Hasher};
let mut hasher = std::collections::hash_map::DefaultHasher::new();
dir.hash(&mut hasher);
format!("{:016x}.json", hasher.finish())
}
pub fn register(dir: &Path, socket_path: Option<&Path>) -> Result<()> {
let reg = registry_dir()?;
let entry = DaemonEntry {
pid: std::process::id(),
dir: dir.to_string_lossy().to_string(),
socket_path: socket_path.map(|p| p.to_string_lossy().to_string()),
};
let path = reg.join(entry_filename(dir));
std::fs::write(&path, serde_json::to_string(&entry)?)?;
Ok(())
}
pub fn unregister(dir: &Path) {
if let Ok(reg) = registry_dir() {
let path = reg.join(entry_filename(dir));
let _ = std::fs::remove_file(path);
}
}
pub fn list() -> Result<Vec<DaemonEntry>> {
let reg = registry_dir()?;
let mut alive = Vec::new();
let dir = match std::fs::read_dir(®) {
Ok(d) => d,
Err(_) => return Ok(alive),
};
for file in dir {
let file = file?;
if file.path().extension().is_none_or(|ext| ext != "json") {
continue;
}
let content = match std::fs::read_to_string(file.path()) {
Ok(c) => c,
Err(_) => continue,
};
let entry: DaemonEntry = match serde_json::from_str(&content) {
Ok(e) => e,
Err(_) => {
let _ = std::fs::remove_file(file.path());
continue;
}
};
if is_pid_alive(entry.pid) {
alive.push(entry);
} else {
let _ = std::fs::remove_file(file.path());
}
}
Ok(alive)
}
fn is_pid_alive(pid: u32) -> bool {
let ret = unsafe { libc::kill(pid as i32, 0) };
if ret == 0 {
return true;
}
let errno = std::io::Error::last_os_error().raw_os_error().unwrap_or(0);
errno == libc::EPERM
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_daemon_entry_has_socket_path() {
let entry = DaemonEntry {
pid: 1234,
dir: "/tmp/test".to_string(),
socket_path: Some("/tmp/test/.cryo/cryo.sock".to_string()),
};
let json = serde_json::to_string(&entry).unwrap();
assert!(json.contains("cryo.sock"));
}
}