Skip to main content

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