1use anyhow::{Context, Result};
7use std::fs;
8use std::path::PathBuf;
9
10const PIDS_DIR: &str = ".chant/pids";
11
12pub 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
21pub 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
29pub 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
49pub 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
61pub fn is_process_running(pid: u32) -> bool {
63 #[cfg(unix)]
64 {
65 use std::process::Command;
66
67 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 eprintln!("Warning: Process checking not implemented for this platform");
81 false
82 }
83}
84
85pub fn stop_process(pid: u32) -> Result<()> {
87 #[cfg(unix)]
88 {
89 use std::process::Command;
90
91 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
110pub 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 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
129pub 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
156pub 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}