tldr_cli/commands/daemon/
pid.rs1use std::fs::{File, OpenOptions};
23use std::io::{Read, Seek, SeekFrom, Write};
24use std::path::{Path, PathBuf};
25
26use crate::commands::daemon::error::{DaemonError, DaemonResult};
27
28pub fn compute_hash(project: &Path) -> String {
37 let project_str = project
39 .canonicalize()
40 .unwrap_or_else(|_| project.to_path_buf())
41 .to_string_lossy()
42 .to_string();
43
44 let digest = md5::compute(project_str.as_bytes());
45
46 format!("{:x}", digest)[..8].to_string()
48}
49
50pub fn compute_pid_path(project: &Path) -> PathBuf {
55 let hash = compute_hash(project);
56 let tmp_dir = std::env::temp_dir();
57 tmp_dir.join(format!("tldr-{}.pid", hash))
58}
59
60#[cfg(unix)]
65pub fn compute_socket_path(project: &Path) -> PathBuf {
66 let hash = compute_hash(project);
67 let tmp_dir = std::env::temp_dir();
68 tmp_dir.join(format!("tldr-{}.sock", hash))
69}
70
71#[cfg(windows)]
76pub fn compute_tcp_port(project: &Path) -> u16 {
77 let hash = compute_hash(project);
78 let hash_int = u64::from_str_radix(&hash, 16).unwrap_or(0);
79 49152 + (hash_int % 10000) as u16
80}
81
82#[cfg(not(unix))]
84pub fn compute_socket_path(project: &Path) -> PathBuf {
85 let hash = compute_hash(project);
87 let tmp_dir = std::env::temp_dir();
88 tmp_dir.join(format!("tldr-{}.sock", hash))
89}
90
91pub struct PidGuard {
102 _file: File,
104 path: PathBuf,
106 pid: u32,
108}
109
110impl PidGuard {
111 pub fn pid(&self) -> u32 {
113 self.pid
114 }
115
116 pub fn path(&self) -> &Path {
118 &self.path
119 }
120}
121
122impl Drop for PidGuard {
123 fn drop(&mut self) {
124 let _ = std::fs::remove_file(&self.path);
127
128 }
130}
131
132#[cfg(unix)]
142pub fn is_process_running(pid: u32) -> bool {
143 unsafe { libc::kill(pid as i32, 0) == 0 }
146}
147
148#[cfg(windows)]
149pub fn is_process_running(pid: u32) -> bool {
150 use windows_sys::Win32::Foundation::CloseHandle;
151 use windows_sys::Win32::System::Threading::{OpenProcess, PROCESS_QUERY_LIMITED_INFORMATION};
152
153 unsafe {
154 let handle = OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, 0, pid);
155 if handle == 0 {
156 return false;
157 }
158 CloseHandle(handle);
159 true
160 }
161}
162
163pub fn try_acquire_lock(pid_path: &Path) -> DaemonResult<PidGuard> {
184 if let Some(parent) = pid_path.parent() {
186 std::fs::create_dir_all(parent)?;
187 }
188
189 let file = OpenOptions::new()
191 .read(true)
192 .write(true)
193 .create(true)
194 .truncate(false) .open(pid_path)?;
196
197 match try_lock_file(&file) {
200 Ok(()) => {
201 let our_pid = std::process::id();
203
204 let mut file = file;
206 file.set_len(0)?;
207 file.seek(SeekFrom::Start(0))?;
208 writeln!(file, "{}", our_pid)?;
209 file.sync_all()?;
210
211 Ok(PidGuard {
212 _file: file,
213 path: pid_path.to_path_buf(),
214 pid: our_pid,
215 })
216 }
217 Err(_) => {
218 let existing_pid = read_pid_from_file(&file).unwrap_or(0);
221
222 if existing_pid > 0 && is_process_running(existing_pid) {
224 Err(DaemonError::AlreadyRunning { pid: existing_pid })
225 } else {
226 Err(DaemonError::StalePidFile { pid: existing_pid })
229 }
230 }
231 }
232}
233
234fn read_pid_from_file(file: &File) -> Option<u32> {
236 let mut file = file;
237 let mut content = String::new();
238
239 if file.seek(SeekFrom::Start(0)).is_err() {
241 return None;
242 }
243
244 if file.read_to_string(&mut content).is_err() {
245 return None;
246 }
247
248 content.trim().parse().ok()
249}
250
251#[cfg(unix)]
257fn try_lock_file(file: &File) -> Result<(), std::io::Error> {
258 use std::os::unix::io::AsRawFd;
259
260 let fd = file.as_raw_fd();
261 let result = unsafe { libc::flock(fd, libc::LOCK_EX | libc::LOCK_NB) };
262
263 if result == 0 {
264 Ok(())
265 } else {
266 Err(std::io::Error::last_os_error())
267 }
268}
269
270#[cfg(windows)]
271fn try_lock_file(file: &File) -> Result<(), std::io::Error> {
272 use std::os::windows::io::AsRawHandle;
273 use windows_sys::Win32::Foundation::HANDLE;
274 use windows_sys::Win32::Storage::FileSystem::{
275 LockFileEx, LOCKFILE_EXCLUSIVE_LOCK, LOCKFILE_FAIL_IMMEDIATELY,
276 };
277 use windows_sys::Win32::System::IO::OVERLAPPED;
278
279 let handle = file.as_raw_handle() as HANDLE;
280
281 let mut overlapped: OVERLAPPED = unsafe { std::mem::zeroed() };
282
283 let result = unsafe {
284 LockFileEx(
285 handle,
286 LOCKFILE_EXCLUSIVE_LOCK | LOCKFILE_FAIL_IMMEDIATELY,
287 0,
288 1, 0,
290 &mut overlapped,
291 )
292 };
293
294 if result != 0 {
295 Ok(())
296 } else {
297 Err(std::io::Error::last_os_error())
298 }
299}
300
301pub fn check_stale_pid(pid_path: &Path) -> DaemonResult<bool> {
310 let content = match std::fs::read_to_string(pid_path) {
312 Ok(c) => c,
313 Err(e) if e.kind() == std::io::ErrorKind::NotFound => return Ok(false),
314 Err(e) => return Err(DaemonError::Io(e)),
315 };
316
317 let pid: u32 = match content.trim().parse() {
319 Ok(p) => p,
320 Err(_) => return Ok(true), };
322
323 Ok(!is_process_running(pid))
325}
326
327pub fn cleanup_stale_pid(pid_path: &Path) -> DaemonResult<bool> {
333 if check_stale_pid(pid_path)? {
334 std::fs::remove_file(pid_path)?;
335 Ok(true)
336 } else {
337 Ok(false)
338 }
339}
340
341#[cfg(test)]
346mod tests {
347 use super::*;
348 use std::path::PathBuf;
349 use tempfile::TempDir;
350
351 #[test]
352 fn test_compute_hash_deterministic() {
353 let project = PathBuf::from("/test/project");
354 let hash1 = compute_hash(&project);
355 let hash2 = compute_hash(&project);
356 assert_eq!(hash1, hash2);
357 assert_eq!(hash1.len(), 8);
358 }
359
360 #[test]
361 fn test_compute_hash_different_projects() {
362 let project1 = PathBuf::from("/test/project1");
363 let project2 = PathBuf::from("/test/project2");
364 let hash1 = compute_hash(&project1);
365 let hash2 = compute_hash(&project2);
366 assert_ne!(hash1, hash2);
367 }
368
369 #[test]
370 fn test_compute_pid_path_format() {
371 let project = PathBuf::from("/test/project");
372 let pid_path = compute_pid_path(&project);
373
374 let filename = pid_path.file_name().unwrap().to_str().unwrap();
375 assert!(filename.starts_with("tldr-"));
376 assert!(filename.ends_with(".pid"));
377 }
378
379 #[test]
380 fn test_compute_socket_path_format() {
381 let project = PathBuf::from("/test/project");
382 let socket_path = compute_socket_path(&project);
383
384 let filename = socket_path.file_name().unwrap().to_str().unwrap();
385 assert!(filename.starts_with("tldr-"));
386 assert!(filename.ends_with(".sock"));
387 }
388
389 #[test]
390 fn test_pid_and_socket_share_hash() {
391 let project = PathBuf::from("/test/project");
392 let pid_path = compute_pid_path(&project);
393 let socket_path = compute_socket_path(&project);
394
395 let pid_name = pid_path.file_name().unwrap().to_str().unwrap();
397 let socket_name = socket_path.file_name().unwrap().to_str().unwrap();
398
399 let pid_hash = &pid_name[5..13];
401 let socket_hash = &socket_name[5..13];
403
404 assert_eq!(pid_hash, socket_hash);
405 }
406
407 #[test]
408 fn test_try_acquire_lock_success() {
409 let temp = TempDir::new().unwrap();
410 let pid_path = temp.path().join("test.pid");
411
412 let guard = try_acquire_lock(&pid_path).unwrap();
413
414 let content = std::fs::read_to_string(&pid_path).unwrap();
416 let written_pid: u32 = content.trim().parse().unwrap();
417 assert_eq!(written_pid, std::process::id());
418 assert_eq!(guard.pid(), std::process::id());
419 }
420
421 #[test]
422 fn test_try_acquire_lock_already_locked() {
423 let temp = TempDir::new().unwrap();
424 let pid_path = temp.path().join("test.pid");
425
426 let _guard1 = try_acquire_lock(&pid_path).unwrap();
428
429 let result = try_acquire_lock(&pid_path);
431 assert!(result.is_err());
432 match result {
433 Err(DaemonError::AlreadyRunning { pid }) => {
434 assert_eq!(pid, std::process::id());
435 }
436 _ => panic!("Expected AlreadyRunning error"),
437 }
438 }
439
440 #[test]
441 fn test_guard_cleanup_on_drop() {
442 let temp = TempDir::new().unwrap();
443 let pid_path = temp.path().join("test.pid");
444
445 {
446 let _guard = try_acquire_lock(&pid_path).unwrap();
447 assert!(pid_path.exists());
448 }
449
450 assert!(!pid_path.exists());
452 }
453
454 #[test]
455 fn test_is_process_running_self() {
456 let our_pid = std::process::id();
457 assert!(is_process_running(our_pid));
458 }
459
460 #[test]
461 fn test_is_process_running_nonexistent() {
462 assert!(!is_process_running(4194304));
465 }
466
467 #[test]
468 fn test_check_stale_pid_nonexistent_file() {
469 let temp = TempDir::new().unwrap();
470 let pid_path = temp.path().join("nonexistent.pid");
471
472 let result = check_stale_pid(&pid_path).unwrap();
473 assert!(!result); }
475
476 #[test]
477 fn test_check_stale_pid_running_process() {
478 let temp = TempDir::new().unwrap();
479 let pid_path = temp.path().join("test.pid");
480
481 std::fs::write(&pid_path, format!("{}", std::process::id())).unwrap();
483
484 let result = check_stale_pid(&pid_path).unwrap();
485 assert!(!result); }
487
488 #[test]
489 fn test_check_stale_pid_dead_process() {
490 let temp = TempDir::new().unwrap();
491 let pid_path = temp.path().join("test.pid");
492
493 std::fs::write(&pid_path, "4194304").unwrap();
495
496 let result = check_stale_pid(&pid_path).unwrap();
497 assert!(result); }
499
500 #[test]
501 fn test_cleanup_stale_pid() {
502 let temp = TempDir::new().unwrap();
503 let pid_path = temp.path().join("test.pid");
504
505 std::fs::write(&pid_path, "4194304").unwrap();
507 assert!(pid_path.exists());
508
509 let cleaned = cleanup_stale_pid(&pid_path).unwrap();
510 assert!(cleaned);
511 assert!(!pid_path.exists());
512 }
513
514 #[test]
515 fn test_cleanup_stale_pid_not_stale() {
516 let temp = TempDir::new().unwrap();
517 let pid_path = temp.path().join("test.pid");
518
519 std::fs::write(&pid_path, format!("{}", std::process::id())).unwrap();
521
522 let cleaned = cleanup_stale_pid(&pid_path).unwrap();
523 assert!(!cleaned);
524 assert!(pid_path.exists());
525 }
526
527 #[cfg(windows)]
528 #[test]
529 fn test_compute_tcp_port_range() {
530 let project = PathBuf::from("/test/project");
531 let port = compute_tcp_port(&project);
532 assert!(port >= 49152);
533 assert!(port < 59152);
534 }
535
536 #[cfg(windows)]
537 #[test]
538 fn test_compute_tcp_port_deterministic() {
539 let project = PathBuf::from("/test/project");
540 let port1 = compute_tcp_port(&project);
541 let port2 = compute_tcp_port(&project);
542 assert_eq!(port1, port2);
543 }
544}