use std::fs::{File, OpenOptions};
use std::io::{Read, Seek, SeekFrom, Write};
use std::path::{Path, PathBuf};
use crate::commands::daemon::error::{DaemonError, DaemonResult};
pub fn compute_hash(project: &Path) -> String {
let project_str = project
.canonicalize()
.unwrap_or_else(|_| project.to_path_buf())
.to_string_lossy()
.to_string();
let digest = md5::compute(project_str.as_bytes());
format!("{:x}", digest)[..8].to_string()
}
pub fn compute_pid_path(project: &Path) -> PathBuf {
let hash = compute_hash(project);
let tmp_dir = std::env::temp_dir();
tmp_dir.join(format!("tldr-{}.pid", hash))
}
#[cfg(unix)]
pub fn compute_socket_path(project: &Path) -> PathBuf {
let hash = compute_hash(project);
let tmp_dir = std::env::temp_dir();
tmp_dir.join(format!("tldr-{}.sock", hash))
}
#[cfg(windows)]
pub fn compute_tcp_port(project: &Path) -> u16 {
let hash = compute_hash(project);
let hash_int = u64::from_str_radix(&hash, 16).unwrap_or(0);
49152 + (hash_int % 10000) as u16
}
#[cfg(not(unix))]
pub fn compute_socket_path(project: &Path) -> PathBuf {
let hash = compute_hash(project);
let tmp_dir = std::env::temp_dir();
tmp_dir.join(format!("tldr-{}.sock", hash))
}
pub struct PidGuard {
_file: File,
path: PathBuf,
pid: u32,
}
impl PidGuard {
pub fn pid(&self) -> u32 {
self.pid
}
pub fn path(&self) -> &Path {
&self.path
}
}
impl Drop for PidGuard {
fn drop(&mut self) {
let _ = std::fs::remove_file(&self.path);
}
}
#[cfg(unix)]
pub fn is_process_running(pid: u32) -> bool {
unsafe { libc::kill(pid as i32, 0) == 0 }
}
#[cfg(windows)]
pub fn is_process_running(pid: u32) -> bool {
use windows_sys::Win32::Foundation::CloseHandle;
use windows_sys::Win32::System::Threading::{OpenProcess, PROCESS_QUERY_LIMITED_INFORMATION};
unsafe {
let handle = OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, 0, pid);
if handle == 0 {
return false;
}
CloseHandle(handle);
true
}
}
pub fn try_acquire_lock(pid_path: &Path) -> DaemonResult<PidGuard> {
if let Some(parent) = pid_path.parent() {
std::fs::create_dir_all(parent)?;
}
let file = OpenOptions::new()
.read(true)
.write(true)
.create(true)
.truncate(false) .open(pid_path)?;
match try_lock_file(&file) {
Ok(()) => {
let our_pid = std::process::id();
let mut file = file;
file.set_len(0)?;
file.seek(SeekFrom::Start(0))?;
writeln!(file, "{}", our_pid)?;
file.sync_all()?;
Ok(PidGuard {
_file: file,
path: pid_path.to_path_buf(),
pid: our_pid,
})
}
Err(_) => {
let existing_pid = read_pid_from_file(&file).unwrap_or(0);
if existing_pid > 0 && is_process_running(existing_pid) {
Err(DaemonError::AlreadyRunning { pid: existing_pid })
} else {
Err(DaemonError::StalePidFile { pid: existing_pid })
}
}
}
}
fn read_pid_from_file(file: &File) -> Option<u32> {
let mut file = file;
let mut content = String::new();
if file.seek(SeekFrom::Start(0)).is_err() {
return None;
}
if file.read_to_string(&mut content).is_err() {
return None;
}
content.trim().parse().ok()
}
#[cfg(unix)]
fn try_lock_file(file: &File) -> Result<(), std::io::Error> {
use std::os::unix::io::AsRawFd;
let fd = file.as_raw_fd();
let result = unsafe { libc::flock(fd, libc::LOCK_EX | libc::LOCK_NB) };
if result == 0 {
Ok(())
} else {
Err(std::io::Error::last_os_error())
}
}
#[cfg(windows)]
fn try_lock_file(file: &File) -> Result<(), std::io::Error> {
use std::os::windows::io::AsRawHandle;
use windows_sys::Win32::Foundation::HANDLE;
use windows_sys::Win32::Storage::FileSystem::{
LockFileEx, LOCKFILE_EXCLUSIVE_LOCK, LOCKFILE_FAIL_IMMEDIATELY,
};
use windows_sys::Win32::System::IO::OVERLAPPED;
let handle = file.as_raw_handle() as HANDLE;
let mut overlapped: OVERLAPPED = unsafe { std::mem::zeroed() };
let result = unsafe {
LockFileEx(
handle,
LOCKFILE_EXCLUSIVE_LOCK | LOCKFILE_FAIL_IMMEDIATELY,
0,
1, 0,
&mut overlapped,
)
};
if result != 0 {
Ok(())
} else {
Err(std::io::Error::last_os_error())
}
}
pub fn check_stale_pid(pid_path: &Path) -> DaemonResult<bool> {
let content = match std::fs::read_to_string(pid_path) {
Ok(c) => c,
Err(e) if e.kind() == std::io::ErrorKind::NotFound => return Ok(false),
Err(e) => return Err(DaemonError::Io(e)),
};
let pid: u32 = match content.trim().parse() {
Ok(p) => p,
Err(_) => return Ok(true), };
Ok(!is_process_running(pid))
}
pub fn cleanup_stale_pid(pid_path: &Path) -> DaemonResult<bool> {
if check_stale_pid(pid_path)? {
std::fs::remove_file(pid_path)?;
Ok(true)
} else {
Ok(false)
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::path::PathBuf;
use tempfile::TempDir;
#[test]
fn test_compute_hash_deterministic() {
let project = PathBuf::from("/test/project");
let hash1 = compute_hash(&project);
let hash2 = compute_hash(&project);
assert_eq!(hash1, hash2);
assert_eq!(hash1.len(), 8);
}
#[test]
fn test_compute_hash_different_projects() {
let project1 = PathBuf::from("/test/project1");
let project2 = PathBuf::from("/test/project2");
let hash1 = compute_hash(&project1);
let hash2 = compute_hash(&project2);
assert_ne!(hash1, hash2);
}
#[test]
fn test_compute_pid_path_format() {
let project = PathBuf::from("/test/project");
let pid_path = compute_pid_path(&project);
let filename = pid_path.file_name().unwrap().to_str().unwrap();
assert!(filename.starts_with("tldr-"));
assert!(filename.ends_with(".pid"));
}
#[test]
fn test_compute_socket_path_format() {
let project = PathBuf::from("/test/project");
let socket_path = compute_socket_path(&project);
let filename = socket_path.file_name().unwrap().to_str().unwrap();
assert!(filename.starts_with("tldr-"));
assert!(filename.ends_with(".sock"));
}
#[test]
fn test_pid_and_socket_share_hash() {
let project = PathBuf::from("/test/project");
let pid_path = compute_pid_path(&project);
let socket_path = compute_socket_path(&project);
let pid_name = pid_path.file_name().unwrap().to_str().unwrap();
let socket_name = socket_path.file_name().unwrap().to_str().unwrap();
let pid_hash = &pid_name[5..13];
let socket_hash = &socket_name[5..13];
assert_eq!(pid_hash, socket_hash);
}
#[test]
fn test_try_acquire_lock_success() {
let temp = TempDir::new().unwrap();
let pid_path = temp.path().join("test.pid");
let guard = try_acquire_lock(&pid_path).unwrap();
let content = std::fs::read_to_string(&pid_path).unwrap();
let written_pid: u32 = content.trim().parse().unwrap();
assert_eq!(written_pid, std::process::id());
assert_eq!(guard.pid(), std::process::id());
}
#[test]
fn test_try_acquire_lock_already_locked() {
let temp = TempDir::new().unwrap();
let pid_path = temp.path().join("test.pid");
let _guard1 = try_acquire_lock(&pid_path).unwrap();
let result = try_acquire_lock(&pid_path);
assert!(result.is_err());
match result {
Err(DaemonError::AlreadyRunning { pid }) => {
assert_eq!(pid, std::process::id());
}
_ => panic!("Expected AlreadyRunning error"),
}
}
#[test]
fn test_guard_cleanup_on_drop() {
let temp = TempDir::new().unwrap();
let pid_path = temp.path().join("test.pid");
{
let _guard = try_acquire_lock(&pid_path).unwrap();
assert!(pid_path.exists());
}
assert!(!pid_path.exists());
}
#[test]
fn test_is_process_running_self() {
let our_pid = std::process::id();
assert!(is_process_running(our_pid));
}
#[test]
fn test_is_process_running_nonexistent() {
assert!(!is_process_running(4194304));
}
#[test]
fn test_check_stale_pid_nonexistent_file() {
let temp = TempDir::new().unwrap();
let pid_path = temp.path().join("nonexistent.pid");
let result = check_stale_pid(&pid_path).unwrap();
assert!(!result); }
#[test]
fn test_check_stale_pid_running_process() {
let temp = TempDir::new().unwrap();
let pid_path = temp.path().join("test.pid");
std::fs::write(&pid_path, format!("{}", std::process::id())).unwrap();
let result = check_stale_pid(&pid_path).unwrap();
assert!(!result); }
#[test]
fn test_check_stale_pid_dead_process() {
let temp = TempDir::new().unwrap();
let pid_path = temp.path().join("test.pid");
std::fs::write(&pid_path, "4194304").unwrap();
let result = check_stale_pid(&pid_path).unwrap();
assert!(result); }
#[test]
fn test_cleanup_stale_pid() {
let temp = TempDir::new().unwrap();
let pid_path = temp.path().join("test.pid");
std::fs::write(&pid_path, "4194304").unwrap();
assert!(pid_path.exists());
let cleaned = cleanup_stale_pid(&pid_path).unwrap();
assert!(cleaned);
assert!(!pid_path.exists());
}
#[test]
fn test_cleanup_stale_pid_not_stale() {
let temp = TempDir::new().unwrap();
let pid_path = temp.path().join("test.pid");
std::fs::write(&pid_path, format!("{}", std::process::id())).unwrap();
let cleaned = cleanup_stale_pid(&pid_path).unwrap();
assert!(!cleaned);
assert!(pid_path.exists());
}
#[cfg(windows)]
#[test]
fn test_compute_tcp_port_range() {
let project = PathBuf::from("/test/project");
let port = compute_tcp_port(&project);
assert!(port >= 49152);
assert!(port < 59152);
}
#[cfg(windows)]
#[test]
fn test_compute_tcp_port_deterministic() {
let project = PathBuf::from("/test/project");
let port1 = compute_tcp_port(&project);
let port2 = compute_tcp_port(&project);
assert_eq!(port1, port2);
}
}