use std::env;
#[cfg(windows)]
use std::net::{Ipv4Addr, SocketAddrV4};
use std::path::{Path, PathBuf};
use serde::{Deserialize, Serialize};
use sha2::{Digest, Sha256};
use crate::directory::get_code_graph_dir;
pub const POSIX_SOCKET_PATH_LIMIT: usize = 100;
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct DaemonLockInfo {
pub pid: u32,
pub version: String,
pub socket_path: String,
pub started_at: i64,
}
pub fn project_hash(project_root: impl AsRef<Path>) -> String {
let resolved = project_root
.as_ref()
.canonicalize()
.unwrap_or_else(|_| project_root.as_ref().to_path_buf());
let mut hasher = Sha256::new();
hasher.update(resolved.to_string_lossy().as_bytes());
let digest = hasher.finalize();
hex_prefix(&digest, 16)
}
pub fn get_daemon_socket_path(project_root: impl AsRef<Path>) -> PathBuf {
let project_root = project_root.as_ref();
if cfg!(windows) {
return PathBuf::from(format!(
r"\\.\pipe\rustcodegraph-{}",
project_hash(project_root)
));
}
let in_project = get_code_graph_dir(project_root).join("daemon.sock");
if in_project.to_string_lossy().len() <= POSIX_SOCKET_PATH_LIMIT {
return in_project;
}
env::temp_dir().join(format!("rustcodegraph-{}.sock", project_hash(project_root)))
}
pub fn get_daemon_pid_path(project_root: impl AsRef<Path>) -> PathBuf {
get_code_graph_dir(project_root).join("daemon.pid")
}
#[cfg(windows)]
pub fn daemon_loopback_addr(socket_path: impl AsRef<Path>) -> SocketAddrV4 {
const LOOPBACK_PORT_BASE: u16 = 30_000;
const LOOPBACK_PORT_SPAN: u16 = 30_000;
let mut hasher = Sha256::new();
hasher.update(socket_path.as_ref().to_string_lossy().as_bytes());
let digest = hasher.finalize();
let offset = u16::from_be_bytes([digest[0], digest[1]]) % LOOPBACK_PORT_SPAN;
SocketAddrV4::new(Ipv4Addr::LOCALHOST, LOOPBACK_PORT_BASE + offset)
}
pub fn encode_lock_info(info: &DaemonLockInfo) -> String {
match serde_json::to_string_pretty(info) {
Ok(mut json) => {
json.push('\n');
json
}
Err(_) => String::new(),
}
}
pub fn decode_lock_info(raw: &str) -> Option<DaemonLockInfo> {
let trimmed = raw.trim();
if trimmed.is_empty() {
return None;
}
if let Ok(parsed) = serde_json::from_str::<DaemonLockInfo>(trimmed)
&& parsed.pid > 0
&& !parsed.version.is_empty()
&& !parsed.socket_path.is_empty()
{
return Some(parsed);
}
trimmed
.parse::<u32>()
.ok()
.filter(|pid| *pid > 0)
.map(|pid| DaemonLockInfo {
pid,
version: "unknown".to_string(),
socket_path: String::new(),
started_at: 0,
})
}
fn hex_prefix(bytes: &[u8], chars: usize) -> String {
const HEX: &[u8; 16] = b"0123456789abcdef";
let mut out = String::with_capacity(chars);
for byte in bytes {
if out.len() >= chars {
break;
}
out.push(HEX[(byte >> 4) as usize] as char);
if out.len() >= chars {
break;
}
out.push(HEX[(byte & 0x0f) as usize] as char);
}
out
}