Skip to main content

chant/
lock.rs

1//! Lock file operations for spec execution tracking
2//!
3//! Provides functionality to create, read, remove, and check lock files
4//! that track which processes are currently working on specs.
5
6use anyhow::Result;
7use std::fs;
8use std::path::{Path, PathBuf};
9
10use crate::paths::LOCKS_DIR;
11
12/// RAII guard that automatically removes lock file on drop
13pub struct LockGuard {
14    spec_id: String,
15}
16
17impl LockGuard {
18    /// Create a new lock guard and lock file
19    pub fn new(spec_id: &str) -> Result<Self> {
20        create_lock(spec_id)?;
21        Ok(Self {
22            spec_id: spec_id.to_string(),
23        })
24    }
25}
26
27impl Drop for LockGuard {
28    fn drop(&mut self) {
29        if let Err(e) = remove_lock(&self.spec_id) {
30            eprintln!(
31                "Warning: Failed to remove lock file for spec {}: {}",
32                self.spec_id, e
33            );
34        }
35    }
36}
37
38/// Create a lock file for a spec with the current process ID
39pub fn create_lock(spec_id: &str) -> Result<PathBuf> {
40    let lock_path = get_lock_path(spec_id);
41    fs::create_dir_all(LOCKS_DIR)?;
42    fs::write(&lock_path, format!("{}", std::process::id()))?;
43    Ok(lock_path)
44}
45
46/// Remove a lock file for a spec
47pub fn remove_lock(spec_id: &str) -> Result<()> {
48    let lock_path = get_lock_path(spec_id);
49    if lock_path.exists() {
50        fs::remove_file(&lock_path)?;
51    }
52    Ok(())
53}
54
55/// Read the PID from a lock file
56pub fn read_lock(spec_id: &str) -> Result<Option<u32>> {
57    let lock_path = get_lock_path(spec_id);
58
59    if !lock_path.exists() {
60        return Ok(None);
61    }
62
63    let content = fs::read_to_string(&lock_path)?;
64    let pid: u32 = content.trim().parse()?;
65    Ok(Some(pid))
66}
67
68/// Check if a spec has an active lock file with a running process
69pub fn is_locked(spec_id: &str) -> bool {
70    let lock_path = get_lock_path(spec_id);
71    if !lock_path.exists() {
72        return false;
73    }
74
75    // Verify PID is actually running
76    match read_lock(spec_id) {
77        Ok(Some(pid)) => is_process_alive(pid),
78        _ => false,
79    }
80}
81
82/// Check if a process with the given PID is alive
83fn is_process_alive(pid: u32) -> bool {
84    #[cfg(unix)]
85    {
86        use nix::sys::signal::{kill, Signal};
87        use nix::unistd::Pid;
88
89        // Signal 0 checks if process exists without sending a signal
90        kill(Pid::from_raw(pid as i32), Signal::try_from(0).ok()).is_ok()
91    }
92    #[cfg(not(unix))]
93    {
94        // On Windows, use a basic check via std::process
95        std::process::Command::new("tasklist")
96            .args(["/FI", &format!("PID eq {}", pid), "/NH"])
97            .output()
98            .map(|o| !String::from_utf8_lossy(&o.stdout).contains("No tasks"))
99            .unwrap_or(false)
100    }
101}
102
103/// Get the path to a spec's lock file
104fn get_lock_path(spec_id: &str) -> PathBuf {
105    Path::new(LOCKS_DIR).join(format!("{}.lock", spec_id))
106}