Skip to main content

chant/
pid.rs

1//! PID tracking for running work processes
2//!
3//! Provides functionality to track and manage running agent processes
4//! associated with specs being worked on.
5
6use anyhow::{Context, Result};
7use std::fs;
8use std::path::PathBuf;
9
10const PIDS_DIR: &str = ".chant/pids";
11
12/// Ensure the PIDs directory exists
13pub fn ensure_pids_dir() -> Result<PathBuf> {
14    let pids_dir = PathBuf::from(PIDS_DIR);
15    if !pids_dir.exists() {
16        fs::create_dir_all(&pids_dir)?;
17    }
18    Ok(pids_dir)
19}
20
21/// Write a PID file for a spec
22pub fn write_pid_file(spec_id: &str, pid: u32) -> Result<()> {
23    let pids_dir = ensure_pids_dir()?;
24    let pid_file = pids_dir.join(format!("{}.pid", spec_id));
25    fs::write(&pid_file, pid.to_string())?;
26    Ok(())
27}
28
29/// Read PID from a spec's PID file
30pub fn read_pid_file(spec_id: &str) -> Result<Option<u32>> {
31    let pids_dir = PathBuf::from(PIDS_DIR);
32    let pid_file = pids_dir.join(format!("{}.pid", spec_id));
33
34    if !pid_file.exists() {
35        return Ok(None);
36    }
37
38    let content = fs::read_to_string(&pid_file)
39        .with_context(|| format!("Failed to read PID file: {}", pid_file.display()))?;
40
41    let pid: u32 = content
42        .trim()
43        .parse()
44        .with_context(|| format!("Invalid PID in file: {}", content))?;
45
46    Ok(Some(pid))
47}
48
49/// Remove PID file for a spec
50pub fn remove_pid_file(spec_id: &str) -> Result<()> {
51    let pids_dir = PathBuf::from(PIDS_DIR);
52    let pid_file = pids_dir.join(format!("{}.pid", spec_id));
53
54    if pid_file.exists() {
55        fs::remove_file(&pid_file)?;
56    }
57
58    Ok(())
59}
60
61/// Check if a process with the given PID is running
62pub fn is_process_running(pid: u32) -> bool {
63    #[cfg(unix)]
64    {
65        use std::process::Command;
66
67        // Use `kill -0` to check if process exists without actually killing it
68        Command::new("kill")
69            .args(["-0", &pid.to_string()])
70            .output()
71            .map(|output| output.status.success())
72            .unwrap_or(false)
73    }
74
75    #[cfg(not(unix))]
76    {
77        // On Windows, we could use tasklist or similar
78        // For now, assume it's not running if we can't check
79        // This is a limitation on non-Unix platforms
80        eprintln!("Warning: Process checking not implemented for this platform");
81        false
82    }
83}
84
85/// Stop a process with the given PID
86pub fn stop_process(pid: u32) -> Result<()> {
87    #[cfg(unix)]
88    {
89        use std::process::Command;
90
91        // Try graceful termination first (SIGTERM)
92        let status = Command::new("kill")
93            .args(["-TERM", &pid.to_string()])
94            .status()
95            .with_context(|| format!("Failed to send SIGTERM to process {}", pid))?;
96
97        if !status.success() {
98            anyhow::bail!("Failed to terminate process {}", pid);
99        }
100
101        Ok(())
102    }
103
104    #[cfg(not(unix))]
105    {
106        anyhow::bail!("Process termination not implemented for this platform");
107    }
108}
109
110/// Stop the work process for a spec
111pub fn stop_spec_work(spec_id: &str) -> Result<()> {
112    let pid = read_pid_file(spec_id)?;
113
114    if let Some(pid) = pid {
115        if is_process_running(pid) {
116            stop_process(pid)?;
117            remove_pid_file(spec_id)?;
118            Ok(())
119        } else {
120            // Process not running, clean up PID file
121            remove_pid_file(spec_id)?;
122            anyhow::bail!("Process {} is not running", pid)
123        }
124    } else {
125        anyhow::bail!("No PID file found for spec {}", spec_id)
126    }
127}
128
129/// List all specs with active PID files
130pub fn list_active_pids() -> Result<Vec<(String, u32, bool)>> {
131    let pids_dir = PathBuf::from(PIDS_DIR);
132
133    if !pids_dir.exists() {
134        return Ok(Vec::new());
135    }
136
137    let mut results = Vec::new();
138
139    for entry in fs::read_dir(&pids_dir)? {
140        let entry = entry?;
141        let path = entry.path();
142
143        if path.extension().and_then(|s| s.to_str()) == Some("pid") {
144            if let Some(spec_id) = path.file_stem().and_then(|s| s.to_str()) {
145                if let Ok(Some(pid)) = read_pid_file(spec_id) {
146                    let is_running = is_process_running(pid);
147                    results.push((spec_id.to_string(), pid, is_running));
148                }
149            }
150        }
151    }
152
153    Ok(results)
154}
155
156/// Clean up stale PID files (where process is no longer running)
157pub fn cleanup_stale_pids() -> Result<usize> {
158    let active_pids = list_active_pids()?;
159    let mut cleaned = 0;
160
161    for (spec_id, _pid, is_running) in active_pids {
162        if !is_running {
163            remove_pid_file(&spec_id)?;
164            cleaned += 1;
165        }
166    }
167
168    Ok(cleaned)
169}