1use crate::error::{ProcError, Result};
7use serde::{Deserialize, Serialize};
8use std::time::Duration;
9use sysinfo::{Pid, ProcessStatus as SysProcessStatus, System};
10
11#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
13#[serde(rename_all = "lowercase")]
14pub enum ProcessStatus {
15 Running,
17 Sleeping,
19 Stopped,
21 Zombie,
23 Dead,
25 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#[derive(Debug, Clone, Serialize, Deserialize)]
44pub struct Process {
45 pub pid: u32,
47 pub name: String,
49 #[serde(skip_serializing_if = "Option::is_none")]
51 pub exe_path: Option<String>,
52 #[serde(skip_serializing_if = "Option::is_none")]
54 pub cwd: Option<String>,
55 #[serde(skip_serializing_if = "Option::is_none")]
57 pub command: Option<String>,
58 pub cpu_percent: f32,
60 pub memory_mb: f64,
62 pub status: ProcessStatus,
64 #[serde(skip_serializing_if = "Option::is_none")]
66 pub user: Option<String>,
67 #[serde(skip_serializing_if = "Option::is_none")]
69 pub parent_pid: Option<u32>,
70 #[serde(skip_serializing_if = "Option::is_none")]
72 pub start_time: Option<u64>,
73}
74
75impl Process {
76 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 self_pid = sysinfo::Pid::from_u32(std::process::id());
83 let processes: Vec<Process> = sys
84 .processes()
85 .iter()
86 .filter_map(|(pid, proc)| {
87 if *pid == self_pid {
90 return None;
91 }
92
93 let name = proc.name().to_string_lossy().to_string();
94 let cmd: String = proc
95 .cmd()
96 .iter()
97 .map(|s| s.to_string_lossy())
98 .collect::<Vec<_>>()
99 .join(" ");
100
101 if name.to_lowercase().contains(&pattern_lower)
103 || cmd.to_lowercase().contains(&pattern_lower)
104 {
105 Some(Process::from_sysinfo(*pid, proc))
106 } else {
107 None
108 }
109 })
110 .collect();
111
112 if processes.is_empty() {
113 return Err(ProcError::ProcessNotFound(pattern.to_string()));
114 }
115
116 Ok(processes)
117 }
118
119 pub fn find_by_pid(pid: u32) -> Result<Option<Process>> {
121 let mut sys = System::new_all();
122 sys.refresh_all();
123
124 let sysinfo_pid = Pid::from_u32(pid);
125
126 Ok(sys
127 .processes()
128 .get(&sysinfo_pid)
129 .map(|proc| Process::from_sysinfo(sysinfo_pid, proc)))
130 }
131
132 pub fn find_all() -> Result<Vec<Process>> {
134 let mut sys = System::new_all();
135 sys.refresh_all();
136
137 let processes: Vec<Process> = sys
138 .processes()
139 .iter()
140 .map(|(pid, proc)| Process::from_sysinfo(*pid, proc))
141 .collect();
142
143 Ok(processes)
144 }
145
146 pub fn find_by_exe_path(path: &std::path::Path) -> Result<Vec<Process>> {
148 let all = Self::find_all()?;
149 let path_str = path.to_string_lossy();
150
151 Ok(all
152 .into_iter()
153 .filter(|p| {
154 if let Some(ref exe) = p.exe_path {
155 exe == &*path_str || std::path::Path::new(exe) == path
157 } else {
158 false
159 }
160 })
161 .collect())
162 }
163
164 #[cfg(unix)]
166 pub fn find_by_open_file(path: &std::path::Path) -> Result<Vec<Process>> {
167 use std::process::Command;
168
169 let output = Command::new("lsof")
170 .args(["-t", &path.to_string_lossy()]) .output();
172
173 let output = match output {
174 Ok(o) => o,
175 Err(_) => return Ok(vec![]), };
177
178 if !output.status.success() {
179 return Ok(vec![]); }
181
182 let pids: Vec<u32> = String::from_utf8_lossy(&output.stdout)
183 .lines()
184 .filter_map(|line| line.trim().parse().ok())
185 .collect();
186
187 let mut processes = Vec::new();
188 for pid in pids {
189 if let Ok(Some(proc)) = Self::find_by_pid(pid) {
190 processes.push(proc);
191 }
192 }
193
194 Ok(processes)
195 }
196
197 #[cfg(not(unix))]
199 pub fn find_by_open_file(_path: &std::path::Path) -> Result<Vec<Process>> {
200 Ok(vec![])
202 }
203
204 pub fn find_stuck(timeout: Duration) -> Result<Vec<Process>> {
207 let mut sys = System::new_all();
208 sys.refresh_all();
209
210 std::thread::sleep(Duration::from_millis(500));
212 sys.refresh_all();
213
214 let timeout_secs = timeout.as_secs();
215 let processes: Vec<Process> = sys
216 .processes()
217 .iter()
218 .filter_map(|(pid, proc)| {
219 let cpu = proc.cpu_usage();
220 let run_time = proc.run_time();
221
222 if run_time > timeout_secs && cpu > 50.0 {
225 Some(Process::from_sysinfo(*pid, proc))
226 } else {
227 None
228 }
229 })
230 .collect();
231
232 Ok(processes)
233 }
234
235 pub fn kill(&self) -> Result<()> {
237 let mut sys = System::new();
238 sys.refresh_processes(
239 sysinfo::ProcessesToUpdate::Some(&[Pid::from_u32(self.pid)]),
240 true,
241 );
242
243 if let Some(proc) = sys.process(Pid::from_u32(self.pid)) {
244 if proc.kill() {
245 Ok(())
246 } else {
247 Err(ProcError::SignalError(format!(
248 "Failed to kill process {}",
249 self.pid
250 )))
251 }
252 } else {
253 Err(ProcError::ProcessNotFound(self.pid.to_string()))
254 }
255 }
256
257 pub fn kill_and_wait(&self) -> Result<Option<std::process::ExitStatus>> {
260 let mut sys = System::new();
261 sys.refresh_processes(
262 sysinfo::ProcessesToUpdate::Some(&[Pid::from_u32(self.pid)]),
263 true,
264 );
265
266 if let Some(proc) = sys.process(Pid::from_u32(self.pid)) {
267 proc.kill_and_wait().map_err(|e| {
268 ProcError::SignalError(format!("Failed to kill process {}: {:?}", self.pid, e))
269 })
270 } else {
271 Err(ProcError::ProcessNotFound(self.pid.to_string()))
272 }
273 }
274
275 #[cfg(unix)]
277 pub fn terminate(&self) -> Result<()> {
278 use nix::sys::signal::{kill, Signal};
279 use nix::unistd::Pid as NixPid;
280
281 kill(NixPid::from_raw(self.pid as i32), Signal::SIGTERM)
282 .map_err(|e| ProcError::SignalError(e.to_string()))
283 }
284
285 #[cfg(windows)]
287 pub fn terminate(&self) -> Result<()> {
288 use std::process::Command;
289
290 Command::new("taskkill")
291 .args(["/PID", &self.pid.to_string()])
292 .output()
293 .map_err(|e| ProcError::SystemError(e.to_string()))?;
294
295 Ok(())
296 }
297
298 pub fn exists(&self) -> bool {
300 let mut sys = System::new();
301 sys.refresh_processes(
302 sysinfo::ProcessesToUpdate::Some(&[Pid::from_u32(self.pid)]),
303 true,
304 );
305 sys.process(Pid::from_u32(self.pid)).is_some()
306 }
307
308 pub fn is_running(&self) -> bool {
310 self.exists()
311 }
312
313 pub fn wait(&self) -> Option<std::process::ExitStatus> {
316 let mut sys = System::new();
317 sys.refresh_processes(
318 sysinfo::ProcessesToUpdate::Some(&[Pid::from_u32(self.pid)]),
319 true,
320 );
321
322 sys.process(Pid::from_u32(self.pid))
323 .and_then(|proc| proc.wait())
324 }
325
326 fn from_sysinfo(pid: Pid, proc: &sysinfo::Process) -> Self {
328 let cmd_vec = proc.cmd();
329 let command = if cmd_vec.is_empty() {
330 None
331 } else {
332 Some(
333 cmd_vec
334 .iter()
335 .map(|s| s.to_string_lossy())
336 .collect::<Vec<_>>()
337 .join(" "),
338 )
339 };
340
341 let exe_path = proc.exe().map(|p| p.to_string_lossy().to_string());
342 let cwd = proc.cwd().map(|p| p.to_string_lossy().to_string());
343
344 Process {
345 pid: pid.as_u32(),
346 name: proc.name().to_string_lossy().to_string(),
347 exe_path,
348 cwd,
349 command,
350 cpu_percent: proc.cpu_usage(),
351 memory_mb: proc.memory() as f64 / 1024.0 / 1024.0,
352 status: ProcessStatus::from(proc.status()),
353 user: proc.user_id().map(|u| u.to_string()),
354 parent_pid: proc.parent().map(|p| p.as_u32()),
355 start_time: Some(proc.start_time()),
356 }
357 }
358}
359
360#[cfg(test)]
361mod tests {
362 use super::*;
363
364 #[test]
365 fn test_find_all_processes() {
366 let processes = Process::find_all().unwrap();
367 assert!(!processes.is_empty(), "Should find at least one process");
368 }
369
370 #[test]
371 fn test_find_by_pid_self() {
372 let pid = std::process::id();
373 let process = Process::find_by_pid(pid).unwrap();
374 assert!(process.is_some(), "Should find own process");
375 }
376
377 #[test]
378 fn test_find_nonexistent_process() {
379 let result = Process::find_by_name("nonexistent_process_12345");
380 assert!(result.is_err());
381 }
382}