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