proc_cli/core/
process.rs

1//! Cross-platform process abstraction
2//!
3//! Provides a unified interface for discovering and managing processes
4//! across macOS, Linux, and Windows.
5
6use crate::error::{ProcError, Result};
7use serde::{Deserialize, Serialize};
8use std::time::Duration;
9use sysinfo::{Pid, ProcessStatus as SysProcessStatus, System};
10
11/// Process status
12#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
13#[serde(rename_all = "lowercase")]
14pub enum ProcessStatus {
15    Running,
16    Sleeping,
17    Stopped,
18    Zombie,
19    Dead,
20    Unknown,
21}
22
23impl From<SysProcessStatus> for ProcessStatus {
24    fn from(status: SysProcessStatus) -> Self {
25        match status {
26            SysProcessStatus::Run => ProcessStatus::Running,
27            SysProcessStatus::Sleep => ProcessStatus::Sleeping,
28            SysProcessStatus::Stop => ProcessStatus::Stopped,
29            SysProcessStatus::Zombie => ProcessStatus::Zombie,
30            SysProcessStatus::Dead => ProcessStatus::Dead,
31            _ => ProcessStatus::Unknown,
32        }
33    }
34}
35
36/// Represents a system process with relevant information
37#[derive(Debug, Clone, Serialize, Deserialize)]
38pub struct Process {
39    /// Process ID
40    pub pid: u32,
41    /// Process name (executable name)
42    pub name: String,
43    /// Path to the executable
44    #[serde(skip_serializing_if = "Option::is_none")]
45    pub exe_path: Option<String>,
46    /// Current working directory
47    #[serde(skip_serializing_if = "Option::is_none")]
48    pub cwd: Option<String>,
49    /// Full command line (if available)
50    #[serde(skip_serializing_if = "Option::is_none")]
51    pub command: Option<String>,
52    /// CPU usage percentage (0.0 - 100.0+)
53    pub cpu_percent: f32,
54    /// Memory usage in megabytes
55    pub memory_mb: f64,
56    /// Process status
57    pub status: ProcessStatus,
58    /// User who owns the process
59    #[serde(skip_serializing_if = "Option::is_none")]
60    pub user: Option<String>,
61    /// Parent process ID
62    #[serde(skip_serializing_if = "Option::is_none")]
63    pub parent_pid: Option<u32>,
64    /// Process start time (Unix timestamp)
65    #[serde(skip_serializing_if = "Option::is_none")]
66    pub start_time: Option<u64>,
67}
68
69impl Process {
70    /// Find all processes matching a name pattern (case-insensitive)
71    pub fn find_by_name(pattern: &str) -> Result<Vec<Process>> {
72        let mut sys = System::new_all();
73        sys.refresh_all();
74
75        let pattern_lower = pattern.to_lowercase();
76        let processes: Vec<Process> = sys
77            .processes()
78            .iter()
79            .filter_map(|(pid, proc)| {
80                let name = proc.name().to_string_lossy().to_string();
81                let cmd: String = proc
82                    .cmd()
83                    .iter()
84                    .map(|s| s.to_string_lossy())
85                    .collect::<Vec<_>>()
86                    .join(" ");
87
88                // Match against name or command
89                if name.to_lowercase().contains(&pattern_lower)
90                    || cmd.to_lowercase().contains(&pattern_lower)
91                {
92                    Some(Process::from_sysinfo(*pid, proc))
93                } else {
94                    None
95                }
96            })
97            .collect();
98
99        if processes.is_empty() {
100            return Err(ProcError::ProcessNotFound(pattern.to_string()));
101        }
102
103        Ok(processes)
104    }
105
106    /// Find a specific process by PID
107    pub fn find_by_pid(pid: u32) -> Result<Option<Process>> {
108        let mut sys = System::new_all();
109        sys.refresh_all();
110
111        let sysinfo_pid = Pid::from_u32(pid);
112
113        Ok(sys
114            .processes()
115            .get(&sysinfo_pid)
116            .map(|proc| Process::from_sysinfo(sysinfo_pid, proc)))
117    }
118
119    /// Get all running processes
120    pub fn find_all() -> Result<Vec<Process>> {
121        let mut sys = System::new_all();
122        sys.refresh_all();
123
124        let processes: Vec<Process> = sys
125            .processes()
126            .iter()
127            .map(|(pid, proc)| Process::from_sysinfo(*pid, proc))
128            .collect();
129
130        Ok(processes)
131    }
132
133    /// Find processes that appear to be stuck (high CPU, no progress)
134    /// This is a heuristic-based detection
135    pub fn find_stuck(timeout: Duration) -> Result<Vec<Process>> {
136        let mut sys = System::new_all();
137        sys.refresh_all();
138
139        // Wait a bit and refresh to compare
140        std::thread::sleep(Duration::from_millis(500));
141        sys.refresh_all();
142
143        let timeout_secs = timeout.as_secs();
144        let processes: Vec<Process> = sys
145            .processes()
146            .iter()
147            .filter_map(|(pid, proc)| {
148                let cpu = proc.cpu_usage();
149                let run_time = proc.run_time();
150
151                // Heuristic: Process using significant CPU for longer than timeout
152                // and in a potentially stuck state
153                if run_time > timeout_secs && cpu > 50.0 {
154                    Some(Process::from_sysinfo(*pid, proc))
155                } else {
156                    None
157                }
158            })
159            .collect();
160
161        Ok(processes)
162    }
163
164    /// Force kill the process (SIGKILL on Unix, taskkill /F on Windows)
165    pub fn kill(&self) -> Result<()> {
166        let mut sys = System::new();
167        sys.refresh_processes(
168            sysinfo::ProcessesToUpdate::Some(&[Pid::from_u32(self.pid)]),
169            true,
170        );
171
172        if let Some(proc) = sys.process(Pid::from_u32(self.pid)) {
173            if proc.kill() {
174                Ok(())
175            } else {
176                Err(ProcError::SignalError(format!(
177                    "Failed to kill process {}",
178                    self.pid
179                )))
180            }
181        } else {
182            Err(ProcError::ProcessNotFound(self.pid.to_string()))
183        }
184    }
185
186    /// Force kill and wait for process to terminate
187    /// Returns the exit status if available
188    pub fn kill_and_wait(&self) -> Result<Option<std::process::ExitStatus>> {
189        let mut sys = System::new();
190        sys.refresh_processes(
191            sysinfo::ProcessesToUpdate::Some(&[Pid::from_u32(self.pid)]),
192            true,
193        );
194
195        if let Some(proc) = sys.process(Pid::from_u32(self.pid)) {
196            proc.kill_and_wait().map_err(|e| {
197                ProcError::SignalError(format!("Failed to kill process {}: {:?}", self.pid, e))
198            })
199        } else {
200            Err(ProcError::ProcessNotFound(self.pid.to_string()))
201        }
202    }
203
204    /// Send SIGTERM for graceful termination (Unix) or taskkill (Windows)
205    #[cfg(unix)]
206    pub fn terminate(&self) -> Result<()> {
207        use nix::sys::signal::{kill, Signal};
208        use nix::unistd::Pid as NixPid;
209
210        kill(NixPid::from_raw(self.pid as i32), Signal::SIGTERM)
211            .map_err(|e| ProcError::SignalError(e.to_string()))
212    }
213
214    /// Graceful termination (Windows)
215    #[cfg(windows)]
216    pub fn terminate(&self) -> Result<()> {
217        use std::process::Command;
218
219        Command::new("taskkill")
220            .args(["/PID", &self.pid.to_string()])
221            .output()
222            .map_err(|e| ProcError::SystemError(e.to_string()))?;
223
224        Ok(())
225    }
226
227    /// Check if the process still exists
228    pub fn exists(&self) -> bool {
229        let mut sys = System::new();
230        sys.refresh_processes(
231            sysinfo::ProcessesToUpdate::Some(&[Pid::from_u32(self.pid)]),
232            true,
233        );
234        sys.process(Pid::from_u32(self.pid)).is_some()
235    }
236
237    /// Check if the process is still running (alias for exists for compatibility)
238    pub fn is_running(&self) -> bool {
239        self.exists()
240    }
241
242    /// Wait for the process to terminate
243    /// Returns the exit status if available
244    pub fn wait(&self) -> Option<std::process::ExitStatus> {
245        let mut sys = System::new();
246        sys.refresh_processes(
247            sysinfo::ProcessesToUpdate::Some(&[Pid::from_u32(self.pid)]),
248            true,
249        );
250
251        sys.process(Pid::from_u32(self.pid))
252            .and_then(|proc| proc.wait())
253    }
254
255    /// Convert from sysinfo Process
256    fn from_sysinfo(pid: Pid, proc: &sysinfo::Process) -> Self {
257        let cmd_vec = proc.cmd();
258        let command = if cmd_vec.is_empty() {
259            None
260        } else {
261            Some(
262                cmd_vec
263                    .iter()
264                    .map(|s| s.to_string_lossy())
265                    .collect::<Vec<_>>()
266                    .join(" "),
267            )
268        };
269
270        let exe_path = proc.exe().map(|p| p.to_string_lossy().to_string());
271        let cwd = proc.cwd().map(|p| p.to_string_lossy().to_string());
272
273        Process {
274            pid: pid.as_u32(),
275            name: proc.name().to_string_lossy().to_string(),
276            exe_path,
277            cwd,
278            command,
279            cpu_percent: proc.cpu_usage(),
280            memory_mb: proc.memory() as f64 / 1024.0 / 1024.0,
281            status: ProcessStatus::from(proc.status()),
282            user: proc.user_id().map(|u| u.to_string()),
283            parent_pid: proc.parent().map(|p| p.as_u32()),
284            start_time: Some(proc.start_time()),
285        }
286    }
287}
288
289#[cfg(test)]
290mod tests {
291    use super::*;
292
293    #[test]
294    fn test_find_all_processes() {
295        let processes = Process::find_all().unwrap();
296        assert!(!processes.is_empty(), "Should find at least one process");
297    }
298
299    #[test]
300    fn test_find_by_pid_self() {
301        let pid = std::process::id();
302        let process = Process::find_by_pid(pid).unwrap();
303        assert!(process.is_some(), "Should find own process");
304    }
305
306    #[test]
307    fn test_find_nonexistent_process() {
308        let result = Process::find_by_name("nonexistent_process_12345");
309        assert!(result.is_err());
310    }
311}