1use anyhow::{Context, Result};
7use std::fs;
8use std::path::PathBuf;
9
10const PIDS_DIR: &str = ".chant/pids";
11const PROCESSES_DIR: &str = ".chant/processes";
12
13pub 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
22pub 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
30pub 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
50pub 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
62pub fn is_process_running(pid: u32) -> bool {
64 #[cfg(unix)]
65 {
66 use std::process::Command;
67
68 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 eprintln!("Warning: Process checking not implemented for this platform");
82 false
83 }
84}
85
86pub fn stop_process(pid: u32) -> Result<()> {
88 #[cfg(unix)]
89 {
90 use std::process::Command;
91
92 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
111pub 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 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
130pub 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
157pub 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
172pub 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}