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 parent_pid = sys.process(self_pid).and_then(|p| p.parent());
86 let processes: Vec<Process> = sys
87 .processes()
88 .iter()
89 .filter_map(|(pid, proc)| {
90 if *pid == self_pid || Some(*pid) == parent_pid {
92 return None;
93 }
94
95 let name = proc.name().to_string_lossy().to_string();
96 let cmd: String = proc
97 .cmd()
98 .iter()
99 .map(|s| s.to_string_lossy())
100 .collect::<Vec<_>>()
101 .join(" ");
102
103 if name.to_lowercase().contains(&pattern_lower)
105 || cmd.to_lowercase().contains(&pattern_lower)
106 {
107 Some(Process::from_sysinfo(*pid, proc))
108 } else {
109 None
110 }
111 })
112 .collect();
113
114 if processes.is_empty() {
115 return Err(ProcError::ProcessNotFound(pattern.to_string()));
116 }
117
118 Ok(processes)
119 }
120
121 pub fn find_by_pid(pid: u32) -> Result<Option<Process>> {
123 let mut sys = System::new_all();
124 sys.refresh_all();
125
126 let sysinfo_pid = Pid::from_u32(pid);
127
128 Ok(sys
129 .processes()
130 .get(&sysinfo_pid)
131 .map(|proc| Process::from_sysinfo(sysinfo_pid, proc)))
132 }
133
134 pub fn find_all() -> Result<Vec<Process>> {
136 let mut sys = System::new_all();
137 sys.refresh_all();
138
139 let self_pid = sysinfo::Pid::from_u32(std::process::id());
140 let processes: Vec<Process> = sys
141 .processes()
142 .iter()
143 .filter(|(pid, _)| **pid != self_pid)
144 .map(|(pid, proc)| Process::from_sysinfo(*pid, proc))
145 .collect();
146
147 Ok(processes)
148 }
149
150 pub fn find_by_exe_path(path: &std::path::Path) -> Result<Vec<Process>> {
152 let all = Self::find_all()?;
153 let path_str = path.to_string_lossy();
154
155 Ok(all
156 .into_iter()
157 .filter(|p| {
158 if let Some(ref exe) = p.exe_path {
159 exe == &*path_str || std::path::Path::new(exe) == path
161 } else {
162 false
163 }
164 })
165 .collect())
166 }
167
168 #[cfg(unix)]
170 pub fn find_by_open_file(path: &std::path::Path) -> Result<Vec<Process>> {
171 use std::process::Command;
172
173 let output = Command::new("lsof")
174 .args(["-t", &path.to_string_lossy()]) .output();
176
177 let output = match output {
178 Ok(o) => o,
179 Err(_) => return Ok(vec![]), };
181
182 if !output.status.success() {
183 return Ok(vec![]); }
185
186 let pids: Vec<u32> = String::from_utf8_lossy(&output.stdout)
187 .lines()
188 .filter_map(|line| line.trim().parse().ok())
189 .collect();
190
191 let mut processes = Vec::new();
192 for pid in pids {
193 if let Ok(Some(proc)) = Self::find_by_pid(pid) {
194 processes.push(proc);
195 }
196 }
197
198 Ok(processes)
199 }
200
201 #[cfg(not(unix))]
203 pub fn find_by_open_file(_path: &std::path::Path) -> Result<Vec<Process>> {
204 Ok(vec![])
206 }
207
208 pub fn find_stuck(timeout: Duration) -> Result<Vec<Process>> {
211 let mut sys = System::new_all();
212 sys.refresh_all();
213
214 std::thread::sleep(Duration::from_millis(500));
216 sys.refresh_all();
217
218 let timeout_secs = timeout.as_secs();
219 let processes: Vec<Process> = sys
220 .processes()
221 .iter()
222 .filter_map(|(pid, proc)| {
223 let cpu = proc.cpu_usage();
224 let run_time = proc.run_time();
225
226 if run_time > timeout_secs && cpu > 50.0 {
229 Some(Process::from_sysinfo(*pid, proc))
230 } else {
231 None
232 }
233 })
234 .collect();
235
236 Ok(processes)
237 }
238
239 pub fn kill(&self) -> Result<()> {
241 let mut sys = System::new();
242 sys.refresh_processes(
243 sysinfo::ProcessesToUpdate::Some(&[Pid::from_u32(self.pid)]),
244 true,
245 );
246
247 if let Some(proc) = sys.process(Pid::from_u32(self.pid)) {
248 if proc.kill() {
249 Ok(())
250 } else {
251 Err(ProcError::SignalError(format!(
252 "Failed to kill process {}",
253 self.pid
254 )))
255 }
256 } else {
257 Err(ProcError::ProcessNotFound(self.pid.to_string()))
258 }
259 }
260
261 pub fn kill_and_wait(&self) -> Result<Option<std::process::ExitStatus>> {
264 let mut sys = System::new();
265 sys.refresh_processes(
266 sysinfo::ProcessesToUpdate::Some(&[Pid::from_u32(self.pid)]),
267 true,
268 );
269
270 if let Some(proc) = sys.process(Pid::from_u32(self.pid)) {
271 proc.kill_and_wait().map_err(|e| {
272 ProcError::SignalError(format!("Failed to kill process {}: {:?}", self.pid, e))
273 })
274 } else {
275 Err(ProcError::ProcessNotFound(self.pid.to_string()))
276 }
277 }
278
279 #[cfg(unix)]
281 pub fn terminate(&self) -> Result<()> {
282 use nix::sys::signal::{kill, Signal};
283 use nix::unistd::Pid as NixPid;
284
285 kill(NixPid::from_raw(self.pid as i32), Signal::SIGTERM)
286 .map_err(|e| ProcError::SignalError(e.to_string()))
287 }
288
289 #[cfg(windows)]
291 pub fn terminate(&self) -> Result<()> {
292 use std::process::Command;
293
294 Command::new("taskkill")
295 .args(["/PID", &self.pid.to_string()])
296 .output()
297 .map_err(|e| ProcError::SystemError(e.to_string()))?;
298
299 Ok(())
300 }
301
302 #[cfg(unix)]
304 pub fn send_signal(&self, signal: nix::sys::signal::Signal) -> Result<()> {
305 use nix::sys::signal::kill;
306 use nix::unistd::Pid as NixPid;
307 kill(NixPid::from_raw(self.pid as i32), signal)
308 .map_err(|e| ProcError::SignalError(format!("{}: {}", signal, e)))
309 }
310
311 pub fn find_orphans() -> Result<Vec<Process>> {
316 let all = Self::find_all()?;
317
318 Ok(all
319 .into_iter()
320 .filter(|p| {
321 if let Some(ppid) = p.parent_pid {
322 ppid == 1 && p.pid != 1 && !Self::is_system_process(p)
323 } else {
324 false
325 }
326 })
327 .collect())
328 }
329
330 fn is_system_process(p: &Process) -> bool {
336 if p.cwd.is_none() || p.cwd.as_deref() == Some("/") {
337 if let Some(ref exe) = p.exe_path {
338 if exe.starts_with("/System/") || exe.starts_with("/usr/libexec/") {
340 return true;
341 }
342 if exe.starts_with("/usr/sbin/")
344 || exe.starts_with("/sbin/")
345 || exe.starts_with("/usr/bin/")
346 || exe.starts_with("/usr/lib/")
347 || exe.starts_with("/usr/lib64/")
348 || exe.starts_with("/lib/")
349 || exe.starts_with("/lib64/")
350 || exe.starts_with("/opt/")
351 || exe.starts_with("/snap/")
352 {
353 return true;
354 }
355 }
356 return true; }
358 false
359 }
360
361 pub fn exists(&self) -> bool {
363 let mut sys = System::new();
364 sys.refresh_processes(
365 sysinfo::ProcessesToUpdate::Some(&[Pid::from_u32(self.pid)]),
366 true,
367 );
368 sys.process(Pid::from_u32(self.pid)).is_some()
369 }
370
371 pub fn is_running(&self) -> bool {
373 self.exists()
374 }
375
376 pub fn wait(&self) -> Option<std::process::ExitStatus> {
379 let mut sys = System::new();
380 sys.refresh_processes(
381 sysinfo::ProcessesToUpdate::Some(&[Pid::from_u32(self.pid)]),
382 true,
383 );
384
385 sys.process(Pid::from_u32(self.pid))
386 .and_then(|proc| proc.wait())
387 }
388
389 pub(crate) fn from_sysinfo(pid: Pid, proc: &sysinfo::Process) -> Self {
391 let cmd_vec = proc.cmd();
392 let command = if cmd_vec.is_empty() {
393 None
394 } else {
395 Some(
396 cmd_vec
397 .iter()
398 .map(|s| s.to_string_lossy())
399 .collect::<Vec<_>>()
400 .join(" "),
401 )
402 };
403
404 let exe_path = proc.exe().map(|p| p.to_string_lossy().to_string());
405 let cwd = proc.cwd().map(|p| p.to_string_lossy().to_string());
406
407 Process {
408 pid: pid.as_u32(),
409 name: proc.name().to_string_lossy().to_string(),
410 exe_path,
411 cwd,
412 command,
413 cpu_percent: proc.cpu_usage(),
414 memory_mb: proc.memory() as f64 / 1024.0 / 1024.0,
415 status: ProcessStatus::from(proc.status()),
416 user: proc.user_id().map(|u| u.to_string()),
417 parent_pid: proc.parent().map(|p| p.as_u32()),
418 start_time: Some(proc.start_time()),
419 }
420 }
421}
422
423#[cfg(unix)]
428pub fn parse_signal_name(name: &str) -> Result<nix::sys::signal::Signal> {
429 use nix::sys::signal::Signal;
430
431 let upper = name.to_uppercase();
432 let upper = upper.trim_start_matches("SIG");
433 match upper {
434 "HUP" => Ok(Signal::SIGHUP),
435 "INT" => Ok(Signal::SIGINT),
436 "QUIT" => Ok(Signal::SIGQUIT),
437 "ABRT" => Ok(Signal::SIGABRT),
438 "KILL" => Ok(Signal::SIGKILL),
439 "TERM" => Ok(Signal::SIGTERM),
440 "STOP" => Ok(Signal::SIGSTOP),
441 "CONT" => Ok(Signal::SIGCONT),
442 "USR1" => Ok(Signal::SIGUSR1),
443 "USR2" => Ok(Signal::SIGUSR2),
444 _ => Err(ProcError::InvalidInput(format!(
445 "Unknown signal: '{}'. Valid signals: HUP, INT, QUIT, ABRT, KILL, TERM, STOP, CONT, USR1, USR2",
446 name
447 ))),
448 }
449}
450
451#[cfg(test)]
452mod tests {
453 use super::*;
454
455 #[test]
456 fn test_find_all_processes() {
457 let processes = Process::find_all().unwrap();
458 assert!(!processes.is_empty(), "Should find at least one process");
459 }
460
461 #[test]
462 fn test_find_by_pid_self() {
463 let pid = std::process::id();
464 let process = Process::find_by_pid(pid).unwrap();
465 assert!(process.is_some(), "Should find own process");
466 }
467
468 #[test]
469 fn test_find_nonexistent_process() {
470 let result = Process::find_by_name("nonexistent_process_12345");
471 assert!(result.is_err());
472 }
473
474 #[test]
475 fn test_find_orphans_returns_ok() {
476 let result = Process::find_orphans();
478 assert!(result.is_ok());
479 }
480
481 #[test]
482 fn test_find_orphans_excludes_system_processes() {
483 let orphans = Process::find_orphans().unwrap();
484 for orphan in &orphans {
485 if orphan.cwd.as_deref() == Some("/") {
487 if let Some(ref exe) = orphan.exe_path {
488 assert!(
489 !exe.starts_with("/usr/sbin/")
490 && !exe.starts_with("/sbin/")
491 && !exe.starts_with("/System/")
492 && !exe.starts_with("/usr/libexec/"),
493 "System process should have been filtered: {} ({})",
494 orphan.name,
495 exe
496 );
497 }
498 }
499 }
500 }
501
502 #[test]
503 fn test_is_system_process_system_paths() {
504 let make_proc = |exe: Option<&str>, cwd: Option<&str>| Process {
505 pid: 100,
506 name: "test".to_string(),
507 exe_path: exe.map(String::from),
508 cwd: cwd.map(String::from),
509 command: None,
510 cpu_percent: 0.0,
511 memory_mb: 0.0,
512 status: ProcessStatus::Running,
513 user: None,
514 parent_pid: Some(1),
515 start_time: None,
516 };
517
518 assert!(Process::is_system_process(&make_proc(
520 Some("/usr/sbin/sshd"),
521 Some("/")
522 )));
523 assert!(Process::is_system_process(&make_proc(
524 Some("/System/Library/foo"),
525 Some("/")
526 )));
527 assert!(Process::is_system_process(&make_proc(
528 Some("/usr/bin/systemd"),
529 Some("/")
530 )));
531 assert!(Process::is_system_process(&make_proc(
532 Some("/usr/lib/snapd/snapd"),
533 Some("/")
534 )));
535
536 assert!(Process::is_system_process(&make_proc(None, Some("/"))));
538
539 assert!(Process::is_system_process(&make_proc(
541 Some("/usr/bin/foo"),
542 None
543 )));
544
545 assert!(!Process::is_system_process(&make_proc(
547 Some("/usr/bin/node"),
548 Some("/home/user/project")
549 )));
550 assert!(!Process::is_system_process(&make_proc(
551 Some("/home/user/.local/bin/app"),
552 Some("/home/user")
553 )));
554 }
555
556 #[cfg(unix)]
557 #[test]
558 fn test_parse_signal_name_valid() {
559 use nix::sys::signal::Signal;
560
561 assert_eq!(parse_signal_name("HUP").unwrap(), Signal::SIGHUP);
562 assert_eq!(parse_signal_name("hup").unwrap(), Signal::SIGHUP);
563 assert_eq!(parse_signal_name("SIGHUP").unwrap(), Signal::SIGHUP);
564 assert_eq!(parse_signal_name("sighup").unwrap(), Signal::SIGHUP);
565 assert_eq!(parse_signal_name("INT").unwrap(), Signal::SIGINT);
566 assert_eq!(parse_signal_name("QUIT").unwrap(), Signal::SIGQUIT);
567 assert_eq!(parse_signal_name("ABRT").unwrap(), Signal::SIGABRT);
568 assert_eq!(parse_signal_name("KILL").unwrap(), Signal::SIGKILL);
569 assert_eq!(parse_signal_name("TERM").unwrap(), Signal::SIGTERM);
570 assert_eq!(parse_signal_name("STOP").unwrap(), Signal::SIGSTOP);
571 assert_eq!(parse_signal_name("CONT").unwrap(), Signal::SIGCONT);
572 assert_eq!(parse_signal_name("USR1").unwrap(), Signal::SIGUSR1);
573 assert_eq!(parse_signal_name("USR2").unwrap(), Signal::SIGUSR2);
574 }
575
576 #[cfg(unix)]
577 #[test]
578 fn test_parse_signal_name_invalid() {
579 assert!(parse_signal_name("INVALID").is_err());
580 assert!(parse_signal_name("FOO").is_err());
581 assert!(parse_signal_name("").is_err());
582 }
583
584 #[cfg(unix)]
585 #[test]
586 fn test_parse_signal_name_case_insensitive() {
587 use nix::sys::signal::Signal;
588
589 assert_eq!(parse_signal_name("term").unwrap(), Signal::SIGTERM);
590 assert_eq!(parse_signal_name("Term").unwrap(), Signal::SIGTERM);
591 assert_eq!(parse_signal_name("TERM").unwrap(), Signal::SIGTERM);
592 assert_eq!(parse_signal_name("sigterm").unwrap(), Signal::SIGTERM);
593 assert_eq!(parse_signal_name("SigTerm").unwrap(), Signal::SIGTERM);
594 }
595
596 #[cfg(unix)]
597 #[test]
598 fn test_send_signal_nonexistent_process() {
599 use nix::sys::signal::Signal;
600
601 let proc = Process {
603 pid: 99999999,
604 name: "ghost".to_string(),
605 exe_path: None,
606 cwd: None,
607 command: None,
608 cpu_percent: 0.0,
609 memory_mb: 0.0,
610 status: ProcessStatus::Running,
611 user: None,
612 parent_pid: None,
613 start_time: None,
614 };
615
616 let result = proc.send_signal(Signal::SIGCONT);
617 assert!(result.is_err());
618 }
619}