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        let _ = remove_lock(&self.spec_id);
30    }
31}
32
33/// Create a lock file for a spec with the current process ID
34pub fn create_lock(spec_id: &str) -> Result<PathBuf> {
35    let lock_path = get_lock_path(spec_id);
36    fs::create_dir_all(LOCKS_DIR)?;
37    fs::write(&lock_path, format!("{}", std::process::id()))?;
38    Ok(lock_path)
39}
40
41/// Remove a lock file for a spec
42pub fn remove_lock(spec_id: &str) -> Result<()> {
43    let lock_path = get_lock_path(spec_id);
44    if lock_path.exists() {
45        fs::remove_file(&lock_path)?;
46    }
47    Ok(())
48}
49
50/// Read the PID from a lock file
51pub fn read_lock(spec_id: &str) -> Result<Option<u32>> {
52    let lock_path = get_lock_path(spec_id);
53
54    if !lock_path.exists() {
55        return Ok(None);
56    }
57
58    let content = fs::read_to_string(&lock_path)?;
59    let pid: u32 = content.trim().parse()?;
60    Ok(Some(pid))
61}
62
63/// Check if a spec has an active lock file with a running process
64pub fn is_locked(spec_id: &str) -> bool {
65    let lock_path = get_lock_path(spec_id);
66    if !lock_path.exists() {
67        return false;
68    }
69
70    // Verify PID is actually running
71    match read_lock(spec_id) {
72        Ok(Some(pid)) => is_process_alive(pid),
73        _ => false,
74    }
75}
76
77/// Check if a process with the given PID is alive
78fn is_process_alive(pid: u32) -> bool {
79    #[cfg(unix)]
80    {
81        use nix::sys::signal::{kill, Signal};
82        use nix::unistd::Pid;
83
84        // Signal 0 checks if process exists without sending a signal
85        kill(Pid::from_raw(pid as i32), Signal::try_from(0).ok()).is_ok()
86    }
87    #[cfg(not(unix))]
88    {
89        // On Windows, use a basic check via std::process
90        std::process::Command::new("tasklist")
91            .args(["/FI", &format!("PID eq {}", pid), "/NH"])
92            .output()
93            .map(|o| !String::from_utf8_lossy(&o.stdout).contains("No tasks"))
94            .unwrap_or(false)
95    }
96}
97
98/// Get the path to a spec's lock file
99fn get_lock_path(spec_id: &str) -> PathBuf {
100    Path::new(LOCKS_DIR).join(format!("{}.lock", spec_id))
101}