1use crate::error::{GitWarpError, Result};
2use std::path::{Path, PathBuf};
3use sysinfo::{System, Pid, Process, ProcessRefreshKind};
4use std::time::Duration;
5
6#[derive(Debug, Clone)]
7pub struct ProcessInfo {
8 pub pid: u32,
9 pub name: String,
10 pub cmd: String,
11 pub working_dir: PathBuf,
12 pub cpu_usage: f32,
13 pub memory_usage: u64,
14 pub start_time: u64,
15}
16
17#[derive(Debug)]
18pub struct ProcessStats {
19 pub total_count: usize,
20 pub total_memory: u64,
21 pub total_cpu: f32,
22 pub high_cpu_count: usize,
23 pub processes: Vec<ProcessInfo>,
24}
25
26pub struct ProcessManager {
27 system: System,
28}
29
30impl ProcessManager {
31 pub fn new() -> Self {
32 let mut system = System::new();
33 system.refresh_all();
34 Self { system }
35 }
36
37 pub fn refresh(&mut self) {
39 self.system.refresh_processes_specifics(ProcessRefreshKind::new());
40 }
41
42 pub fn find_processes_in_directory<P: AsRef<Path>>(&mut self, path: P) -> Result<Vec<ProcessInfo>> {
44 let target_path = path.as_ref().canonicalize()
45 .unwrap_or_else(|_| path.as_ref().to_path_buf());
46
47 self.refresh();
48 let mut processes = Vec::new();
49
50 for (pid, process) in self.system.processes() {
51 if let Some(cwd) = process.cwd() {
52 if cwd.starts_with(&target_path) {
53 processes.push(ProcessInfo {
54 pid: pid.as_u32(),
55 name: process.name().to_string(),
56 cmd: process.cmd().join(" "),
57 working_dir: cwd.to_path_buf(),
58 cpu_usage: process.cpu_usage(),
59 memory_usage: process.memory(),
60 start_time: process.start_time(),
61 });
62 }
63 }
64 }
65
66 processes.sort_by(|a, b| b.cpu_usage.partial_cmp(&a.cpu_usage).unwrap_or(std::cmp::Ordering::Equal));
68
69 Ok(processes)
70 }
71
72 pub fn terminate_processes(&self, processes: &[ProcessInfo], auto_confirm: bool) -> Result<bool> {
74 if processes.is_empty() {
75 return Ok(true);
76 }
77
78 self.display_process_list(processes);
79
80 if !auto_confirm && !self.confirm_termination()? {
81 println!("ā Process termination cancelled");
82 return Ok(false);
83 }
84
85 let mut success_count = 0;
86 let mut failed_count = 0;
87
88 for process in processes {
89 println!("šŖ Terminating PID {}: {}", process.pid, process.name);
90
91 if self.terminate_single_process(process.pid) {
92 success_count += 1;
93 println!(" ā
Terminated successfully");
94 } else {
95 failed_count += 1;
96 println!(" ā Failed to terminate");
97 }
98 }
99
100 println!("\nš Process termination complete: {} succeeded, {} failed", success_count, failed_count);
101 Ok(failed_count == 0)
102 }
103
104 fn display_process_list(&self, processes: &[ProcessInfo]) {
105 println!("\nā ļø Found {} processes in worktree:", processes.len());
106 for process in processes {
107 let memory_mb = process.memory_usage / 1024 / 1024;
108 println!(" ⢠PID {}: {} (CPU: {:.1}%, Mem: {}MB)",
109 process.pid, process.name, process.cpu_usage, memory_mb);
110 println!(" Working dir: {}", process.working_dir.display());
111 if !process.cmd.is_empty() {
112 println!(" Command: {}", process.cmd);
113 }
114 }
115 }
116
117 fn confirm_termination(&self) -> Result<bool> {
118 println!("\nā Terminate these processes? [y/N]: ");
119 use std::io::{self, Write};
120 io::stdout().flush()?;
121
122 let mut input = String::new();
123 io::stdin().read_line(&mut input)?;
124
125 Ok(input.trim().to_lowercase().starts_with('y'))
126 }
127
128 fn terminate_single_process(&self, pid: u32) -> bool {
130 #[cfg(unix)]
131 {
132 use std::process::Command;
133
134 let graceful_result = Command::new("kill")
136 .arg("-TERM")
137 .arg(pid.to_string())
138 .output();
139
140 match graceful_result {
141 Ok(output) if output.status.success() => {
142 std::thread::sleep(Duration::from_millis(2000));
144
145 let check_result = Command::new("kill")
147 .arg("-0")
148 .arg(pid.to_string())
149 .output();
150
151 match check_result {
152 Ok(output) if output.status.success() => {
153 let force_result = Command::new("kill")
155 .arg("-KILL")
156 .arg(pid.to_string())
157 .output();
158 force_result.map(|o| o.status.success()).unwrap_or(false)
159 }
160 _ => true, }
162 }
163 _ => {
164 let force_result = Command::new("kill")
166 .arg("-KILL")
167 .arg(pid.to_string())
168 .output();
169 force_result.map(|o| o.status.success()).unwrap_or(false)
170 }
171 }
172 }
173
174 #[cfg(windows)]
175 {
176 use std::process::Command;
177
178 let result = Command::new("taskkill")
179 .arg("/PID")
180 .arg(pid.to_string())
181 .arg("/F")
182 .output();
183
184 result.map(|o| o.status.success()).unwrap_or(false)
185 }
186
187 #[cfg(not(any(unix, windows)))]
188 {
189 false
190 }
191 }
192
193 pub fn has_processes_in_directory<P: AsRef<Path>>(&mut self, path: P) -> Result<bool> {
195 let processes = self.find_processes_in_directory(path)?;
196 Ok(!processes.is_empty())
197 }
198
199 pub fn get_directory_process_stats<P: AsRef<Path>>(&mut self, path: P) -> Result<ProcessStats> {
201 let processes = self.find_processes_in_directory(path)?;
202
203 let total_count = processes.len();
204 let total_memory = processes.iter().map(|p| p.memory_usage).sum::<u64>();
205 let total_cpu = processes.iter().map(|p| p.cpu_usage).sum::<f32>();
206 let high_cpu_count = processes.iter().filter(|p| p.cpu_usage > 10.0).count();
207
208 Ok(ProcessStats {
209 total_count,
210 total_memory,
211 total_cpu,
212 high_cpu_count,
213 processes,
214 })
215 }
216
217 pub fn kill_directory_processes<P: AsRef<Path>>(&mut self, path: P, auto_confirm: bool) -> Result<bool> {
219 let processes = self.find_processes_in_directory(path)?;
220
221 if processes.is_empty() {
222 println!("⨠No processes found in directory");
223 return Ok(true);
224 }
225
226 self.terminate_processes(&processes, auto_confirm)
227 }
228}
229
230#[cfg(test)]
231mod tests {
232 use super::*;
233 use tempfile::tempdir;
234
235 #[test]
236 fn test_process_manager_creation() {
237 let manager = ProcessManager::new();
238 }
240
241 #[test]
242 fn test_find_processes_empty_directory() {
243 let temp_dir = tempdir().unwrap();
244 let mut manager = ProcessManager::new();
245
246 let result = manager.find_processes_in_directory(temp_dir.path());
247 assert!(result.is_ok());
248 }
250
251 #[test]
252 fn test_process_info_fields() {
253 let process = ProcessInfo {
254 pid: 12345,
255 name: "test_process".to_string(),
256 cmd: "test command".to_string(),
257 working_dir: PathBuf::from("/test"),
258 cpu_usage: 5.5,
259 memory_usage: 1024 * 1024, start_time: 1234567890,
261 };
262
263 assert_eq!(process.pid, 12345);
264 assert_eq!(process.name, "test_process");
265 assert_eq!(process.cpu_usage, 5.5);
266 assert_eq!(process.memory_usage, 1024 * 1024);
267 }
268
269 #[test]
270 fn test_has_processes_in_directory() {
271 let temp_dir = tempdir().unwrap();
272 let mut manager = ProcessManager::new();
273
274 let result = manager.has_processes_in_directory(temp_dir.path());
275 assert!(result.is_ok());
276 }
277
278 #[test]
279 fn test_process_stats() {
280 let processes = vec![
281 ProcessInfo {
282 pid: 1,
283 name: "proc1".to_string(),
284 cmd: "test1".to_string(),
285 working_dir: PathBuf::from("/test"),
286 cpu_usage: 15.0,
287 memory_usage: 1024,
288 start_time: 1000,
289 },
290 ProcessInfo {
291 pid: 2,
292 name: "proc2".to_string(),
293 cmd: "test2".to_string(),
294 working_dir: PathBuf::from("/test"),
295 cpu_usage: 5.0,
296 memory_usage: 2048,
297 start_time: 1100,
298 },
299 ];
300
301 let stats = ProcessStats {
302 total_count: processes.len(),
303 total_memory: processes.iter().map(|p| p.memory_usage).sum(),
304 total_cpu: processes.iter().map(|p| p.cpu_usage).sum(),
305 high_cpu_count: processes.iter().filter(|p| p.cpu_usage > 10.0).count(),
306 processes,
307 };
308
309 assert_eq!(stats.total_count, 2);
310 assert_eq!(stats.total_memory, 3072);
311 assert_eq!(stats.total_cpu, 20.0);
312 assert_eq!(stats.high_cpu_count, 1);
313 }
314}