git_warp/
process.rs

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    /// Refresh process information
38    pub fn refresh(&mut self) {
39        self.system.refresh_processes_specifics(ProcessRefreshKind::new());
40    }
41    
42    /// Find all processes running in a specific directory
43    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        // Sort by CPU usage (most active first)
67        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    /// Terminate processes with user confirmation and progress feedback
73    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    /// Terminate a single process by PID with graceful fallback
129    fn terminate_single_process(&self, pid: u32) -> bool {
130        #[cfg(unix)]
131        {
132            use std::process::Command;
133            
134            // Try graceful termination first (SIGTERM)
135            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                    // Wait for graceful shutdown
143                    std::thread::sleep(Duration::from_millis(2000));
144                    
145                    // Check if process is still running
146                    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                            // Process still running, force kill
154                            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, // Process gracefully terminated
161                    }
162                }
163                _ => {
164                    // Graceful termination failed, force kill immediately
165                    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    /// Check if any processes are running in the directory
194    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    /// Get detailed process statistics for a directory
200    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    /// Kill all processes in a directory with confirmation
218    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        // Just verify we can create a process manager
239    }
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        // Most likely no processes will be running in a temporary directory
249    }
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, // 1MB
260            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}