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