1use crate::error::{ProcError, Result};
7use serde::{Deserialize, Serialize};
8use std::sync::OnceLock;
9use std::time::Duration;
10use sysinfo::{Pid, ProcessStatus as SysProcessStatus, System, Uid, Users};
11
12fn user_name(uid: &Uid) -> String {
15 static USERS: OnceLock<Users> = OnceLock::new();
16 USERS
17 .get_or_init(Users::new_with_refreshed_list)
18 .get_user_by_id(uid)
19 .map(|user| user.name().to_string())
20 .unwrap_or_else(|| uid.to_string())
21}
22
23pub fn is_thread(proc: &sysinfo::Process) -> bool {
27 proc.thread_kind().is_some()
28}
29
30fn strip_deleted_marker(cwd: String) -> String {
34 match cwd.strip_suffix(" (deleted)") {
35 Some(path) if !std::path::Path::new(&cwd).exists() => path.to_string(),
36 _ => cwd,
37 }
38}
39
40#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
42#[serde(rename_all = "lowercase")]
43pub enum ProcessStatus {
44 Running,
46 Sleeping,
48 Stopped,
50 Zombie,
52 Dead,
54 Unknown,
56}
57
58impl From<SysProcessStatus> for ProcessStatus {
59 fn from(status: SysProcessStatus) -> Self {
60 match status {
61 SysProcessStatus::Run => ProcessStatus::Running,
62 SysProcessStatus::Sleep => ProcessStatus::Sleeping,
63 SysProcessStatus::Stop => ProcessStatus::Stopped,
64 SysProcessStatus::Zombie => ProcessStatus::Zombie,
65 SysProcessStatus::Dead => ProcessStatus::Dead,
66 _ => ProcessStatus::Unknown,
67 }
68 }
69}
70
71#[derive(Debug, Clone, Serialize, Deserialize)]
73pub struct Process {
74 pub pid: u32,
76 pub name: String,
78 #[serde(skip_serializing_if = "Option::is_none")]
80 pub exe_path: Option<String>,
81 #[serde(skip_serializing_if = "Option::is_none")]
83 pub cwd: Option<String>,
84 #[serde(skip_serializing_if = "Option::is_none")]
86 pub command: Option<String>,
87 pub cpu_percent: f32,
89 pub memory_mb: f64,
91 pub status: ProcessStatus,
93 #[serde(skip_serializing_if = "Option::is_none")]
95 pub user: Option<String>,
96 #[serde(skip_serializing_if = "Option::is_none")]
98 pub parent_pid: Option<u32>,
99 #[serde(skip_serializing_if = "Option::is_none")]
101 pub start_time: Option<u64>,
102 #[serde(skip_serializing_if = "Option::is_none")]
104 pub uptime_secs: Option<u64>,
105}
106
107impl Process {
108 pub fn find_by_name(pattern: &str) -> Result<Vec<Process>> {
110 let mut sys = System::new_all();
111 sys.refresh_all();
112
113 let pattern_lower = pattern.to_lowercase();
114 let self_pid = sysinfo::Pid::from_u32(std::process::id());
115 let ancestor_pids = {
119 let mut pids = std::collections::HashSet::new();
120 pids.insert(self_pid);
121 let mut current = self_pid;
122 for _ in 0..10 {
123 if let Some(parent) = sys.process(current).and_then(|p| p.parent()) {
124 pids.insert(parent);
125 current = parent;
126 } else {
127 break;
128 }
129 }
130 pids
131 };
132 let processes: Vec<Process> = sys
133 .processes()
134 .iter()
135 .filter_map(|(pid, proc)| {
136 if ancestor_pids.contains(pid) || is_thread(proc) {
138 return None;
139 }
140
141 let name = proc.name().to_string_lossy().to_string();
142 let cmd: String = proc
143 .cmd()
144 .iter()
145 .map(|s| s.to_string_lossy())
146 .collect::<Vec<_>>()
147 .join(" ");
148
149 if name.to_lowercase().contains(&pattern_lower)
151 || cmd.to_lowercase().contains(&pattern_lower)
152 {
153 Some(Process::from_sysinfo(*pid, proc))
154 } else {
155 None
156 }
157 })
158 .collect();
159
160 if processes.is_empty() {
161 return Err(ProcError::ProcessNotFound(pattern.to_string()));
162 }
163
164 Ok(processes)
165 }
166
167 pub fn find_by_pid(pid: u32) -> Result<Option<Process>> {
169 let mut sys = System::new_all();
170 sys.refresh_all();
171
172 let sysinfo_pid = Pid::from_u32(pid);
173
174 Ok(sys
175 .processes()
176 .get(&sysinfo_pid)
177 .map(|proc| Process::from_sysinfo(sysinfo_pid, proc)))
178 }
179
180 pub fn find_all() -> Result<Vec<Process>> {
182 let mut sys = System::new_all();
183 sys.refresh_all();
184
185 let self_pid = sysinfo::Pid::from_u32(std::process::id());
186 let processes: Vec<Process> = sys
187 .processes()
188 .iter()
189 .filter(|(pid, proc)| **pid != self_pid && !is_thread(proc))
190 .map(|(pid, proc)| Process::from_sysinfo(*pid, proc))
191 .collect();
192
193 Ok(processes)
194 }
195
196 pub fn find_by_exe_path(path: &std::path::Path) -> Result<Vec<Process>> {
198 let all = Self::find_all()?;
199 let path_str = path.to_string_lossy();
200
201 Ok(all
202 .into_iter()
203 .filter(|p| {
204 if let Some(ref exe) = p.exe_path {
205 exe == &*path_str || std::path::Path::new(exe) == path
207 } else {
208 false
209 }
210 })
211 .collect())
212 }
213
214 #[cfg(unix)]
216 pub fn find_by_open_file(path: &std::path::Path) -> Result<Vec<Process>> {
217 use std::process::Command;
218
219 let output = Command::new("lsof")
220 .args(["-t", &path.to_string_lossy()]) .output();
222
223 let output = match output {
224 Ok(o) => o,
225 Err(_) => return Ok(vec![]), };
227
228 if !output.status.success() {
229 return Ok(vec![]); }
231
232 let pids: Vec<u32> = String::from_utf8_lossy(&output.stdout)
233 .lines()
234 .filter_map(|line| line.trim().parse().ok())
235 .collect();
236
237 let mut processes = Vec::new();
238 for pid in pids {
239 if let Ok(Some(proc)) = Self::find_by_pid(pid) {
240 processes.push(proc);
241 }
242 }
243
244 Ok(processes)
245 }
246
247 #[cfg(not(unix))]
249 pub fn find_by_open_file(_path: &std::path::Path) -> Result<Vec<Process>> {
250 Ok(vec![])
252 }
253
254 pub fn find_stuck(timeout: Duration) -> Result<Vec<Process>> {
257 let mut sys = System::new_all();
258 sys.refresh_all();
259
260 std::thread::sleep(Duration::from_millis(500));
262 sys.refresh_all();
263
264 let timeout_secs = timeout.as_secs();
265 let processes: Vec<Process> = sys
266 .processes()
267 .iter()
268 .filter_map(|(pid, proc)| {
269 if is_thread(proc) {
270 return None;
271 }
272 let cpu = proc.cpu_usage();
273 let run_time = proc.run_time();
274
275 if run_time > timeout_secs && cpu > 50.0 {
278 Some(Process::from_sysinfo(*pid, proc))
279 } else {
280 None
281 }
282 })
283 .collect();
284
285 Ok(processes)
286 }
287
288 pub fn kill(&self) -> Result<()> {
290 let mut sys = System::new();
291 sys.refresh_processes(
292 sysinfo::ProcessesToUpdate::Some(&[Pid::from_u32(self.pid)]),
293 true,
294 );
295
296 if let Some(proc) = sys.process(Pid::from_u32(self.pid)) {
297 if proc.kill() {
298 Ok(())
299 } else {
300 Err(ProcError::SignalError(format!(
301 "Failed to kill process {}",
302 self.pid
303 )))
304 }
305 } else {
306 Err(ProcError::ProcessNotFound(self.pid.to_string()))
307 }
308 }
309
310 pub fn kill_and_wait(&self) -> Result<Option<std::process::ExitStatus>> {
313 let mut sys = System::new();
314 sys.refresh_processes(
315 sysinfo::ProcessesToUpdate::Some(&[Pid::from_u32(self.pid)]),
316 true,
317 );
318
319 if let Some(proc) = sys.process(Pid::from_u32(self.pid)) {
320 proc.kill_and_wait().map_err(|e| {
321 ProcError::SignalError(format!("Failed to kill process {}: {:?}", self.pid, e))
322 })
323 } else {
324 Err(ProcError::ProcessNotFound(self.pid.to_string()))
325 }
326 }
327
328 #[cfg(unix)]
330 pub fn terminate(&self) -> Result<()> {
331 use nix::sys::signal::{kill, Signal};
332 use nix::unistd::Pid as NixPid;
333
334 kill(NixPid::from_raw(self.pid as i32), Signal::SIGTERM)
335 .map_err(|e| ProcError::SignalError(e.to_string()))
336 }
337
338 #[cfg(windows)]
340 pub fn terminate(&self) -> Result<()> {
341 use std::process::Command;
342
343 Command::new("taskkill")
344 .args(["/PID", &self.pid.to_string()])
345 .output()
346 .map_err(|e| ProcError::SystemError(e.to_string()))?;
347
348 Ok(())
349 }
350
351 #[cfg(unix)]
353 pub fn send_signal(&self, signal: nix::sys::signal::Signal) -> Result<()> {
354 use nix::sys::signal::kill;
355 use nix::unistd::Pid as NixPid;
356 kill(NixPid::from_raw(self.pid as i32), signal)
357 .map_err(|e| ProcError::SignalError(format!("{}: {}", signal, e)))
358 }
359
360 pub fn find_orphans() -> Result<Vec<Process>> {
365 let all = Self::find_all()?;
366
367 Ok(all
368 .into_iter()
369 .filter(|p| {
370 if let Some(ppid) = p.parent_pid {
371 ppid == 1 && p.pid != 1 && !Self::is_system_process(p)
372 } else {
373 false
374 }
375 })
376 .collect())
377 }
378
379 fn is_system_process(p: &Process) -> bool {
385 if p.cwd.is_none() || p.cwd.as_deref() == Some("/") {
386 if let Some(ref exe) = p.exe_path {
387 if exe.starts_with("/System/") || exe.starts_with("/usr/libexec/") {
389 return true;
390 }
391 if exe.starts_with("/usr/sbin/")
393 || exe.starts_with("/sbin/")
394 || exe.starts_with("/usr/bin/")
395 || exe.starts_with("/usr/lib/")
396 || exe.starts_with("/usr/lib64/")
397 || exe.starts_with("/lib/")
398 || exe.starts_with("/lib64/")
399 || exe.starts_with("/opt/")
400 || exe.starts_with("/snap/")
401 {
402 return true;
403 }
404 }
405 return true; }
407 false
408 }
409
410 pub fn exists(&self) -> bool {
412 let mut sys = System::new();
413 sys.refresh_processes(
414 sysinfo::ProcessesToUpdate::Some(&[Pid::from_u32(self.pid)]),
415 true,
416 );
417 sys.process(Pid::from_u32(self.pid)).is_some()
418 }
419
420 pub fn is_running(&self) -> bool {
422 self.exists()
423 }
424
425 pub fn wait(&self) -> Option<std::process::ExitStatus> {
428 let mut sys = System::new();
429 sys.refresh_processes(
430 sysinfo::ProcessesToUpdate::Some(&[Pid::from_u32(self.pid)]),
431 true,
432 );
433
434 sys.process(Pid::from_u32(self.pid))
435 .and_then(|proc| proc.wait())
436 }
437
438 pub(crate) fn from_sysinfo(pid: Pid, proc: &sysinfo::Process) -> Self {
440 let cmd_vec = proc.cmd();
441 let command = if cmd_vec.is_empty() {
442 None
443 } else {
444 Some(
445 cmd_vec
446 .iter()
447 .map(|s| s.to_string_lossy())
448 .collect::<Vec<_>>()
449 .join(" "),
450 )
451 };
452
453 let exe_path = proc.exe().map(|p| p.to_string_lossy().to_string());
454 let cwd = proc
455 .cwd()
456 .map(|p| strip_deleted_marker(p.to_string_lossy().to_string()));
457
458 Process {
459 pid: pid.as_u32(),
460 name: proc.name().to_string_lossy().to_string(),
461 exe_path,
462 cwd,
463 command,
464 cpu_percent: proc.cpu_usage(),
465 memory_mb: proc.memory() as f64 / 1024.0 / 1024.0,
466 status: ProcessStatus::from(proc.status()),
467 user: proc.user_id().map(user_name),
468 parent_pid: proc.parent().map(|p| p.as_u32()),
469 start_time: (proc.start_time() > 0).then(|| proc.start_time()),
472 uptime_secs: (proc.start_time() > 0).then(|| proc.run_time()),
473 }
474 }
475}
476
477#[cfg(unix)]
482pub fn parse_signal_name(name: &str) -> Result<nix::sys::signal::Signal> {
483 use nix::sys::signal::Signal;
484
485 let upper = name.to_uppercase();
486 let upper = upper.trim_start_matches("SIG");
487 match upper {
488 "HUP" => Ok(Signal::SIGHUP),
489 "INT" => Ok(Signal::SIGINT),
490 "QUIT" => Ok(Signal::SIGQUIT),
491 "ABRT" => Ok(Signal::SIGABRT),
492 "KILL" => Ok(Signal::SIGKILL),
493 "TERM" => Ok(Signal::SIGTERM),
494 "STOP" => Ok(Signal::SIGSTOP),
495 "CONT" => Ok(Signal::SIGCONT),
496 "USR1" => Ok(Signal::SIGUSR1),
497 "USR2" => Ok(Signal::SIGUSR2),
498 _ => Err(ProcError::InvalidInput(format!(
499 "Unknown signal: '{}'. Valid signals: HUP, INT, QUIT, ABRT, KILL, TERM, STOP, CONT, USR1, USR2",
500 name
501 ))),
502 }
503}
504
505#[cfg(test)]
506mod tests {
507 use super::*;
508
509 #[test]
510 fn test_find_all_processes() {
511 let processes = Process::find_all().unwrap();
512 assert!(!processes.is_empty(), "Should find at least one process");
513 }
514
515 #[test]
516 fn test_find_by_pid_self() {
517 let pid = std::process::id();
518 let process = Process::find_by_pid(pid).unwrap();
519 assert!(process.is_some(), "Should find own process");
520 }
521
522 #[test]
523 fn test_strip_deleted_marker() {
524 assert_eq!(
525 strip_deleted_marker("/nonexistent/proc-gone (deleted)".to_string()),
526 "/nonexistent/proc-gone"
527 );
528 assert_eq!(
529 strip_deleted_marker("/tmp/project".to_string()),
530 "/tmp/project"
531 );
532
533 let real = std::env::temp_dir().join(format!("proc-real-{} (deleted)", std::process::id()));
535 std::fs::create_dir_all(&real).unwrap();
536 let path = real.to_string_lossy().to_string();
537 assert_eq!(strip_deleted_marker(path.clone()), path);
538 std::fs::remove_dir(&real).unwrap();
539 }
540
541 #[test]
542 fn test_find_nonexistent_process() {
543 let result = Process::find_by_name("nonexistent_process_12345");
544 assert!(result.is_err());
545 }
546
547 #[test]
548 fn test_find_orphans_returns_ok() {
549 let result = Process::find_orphans();
551 assert!(result.is_ok());
552 }
553
554 #[test]
555 fn test_find_orphans_excludes_system_processes() {
556 let orphans = Process::find_orphans().unwrap();
557 for orphan in &orphans {
558 if orphan.cwd.as_deref() == Some("/") {
560 if let Some(ref exe) = orphan.exe_path {
561 assert!(
562 !exe.starts_with("/usr/sbin/")
563 && !exe.starts_with("/sbin/")
564 && !exe.starts_with("/System/")
565 && !exe.starts_with("/usr/libexec/"),
566 "System process should have been filtered: {} ({})",
567 orphan.name,
568 exe
569 );
570 }
571 }
572 }
573 }
574
575 #[test]
576 fn test_is_system_process_system_paths() {
577 let make_proc = |exe: Option<&str>, cwd: Option<&str>| Process {
578 pid: 100,
579 name: "test".to_string(),
580 exe_path: exe.map(String::from),
581 cwd: cwd.map(String::from),
582 command: None,
583 cpu_percent: 0.0,
584 memory_mb: 0.0,
585 status: ProcessStatus::Running,
586 user: None,
587 parent_pid: Some(1),
588 start_time: None,
589 uptime_secs: None,
590 };
591
592 assert!(Process::is_system_process(&make_proc(
594 Some("/usr/sbin/sshd"),
595 Some("/")
596 )));
597 assert!(Process::is_system_process(&make_proc(
598 Some("/System/Library/foo"),
599 Some("/")
600 )));
601 assert!(Process::is_system_process(&make_proc(
602 Some("/usr/bin/systemd"),
603 Some("/")
604 )));
605 assert!(Process::is_system_process(&make_proc(
606 Some("/usr/lib/snapd/snapd"),
607 Some("/")
608 )));
609
610 assert!(Process::is_system_process(&make_proc(None, Some("/"))));
612
613 assert!(Process::is_system_process(&make_proc(
615 Some("/usr/bin/foo"),
616 None
617 )));
618
619 assert!(!Process::is_system_process(&make_proc(
621 Some("/usr/bin/node"),
622 Some("/home/user/project")
623 )));
624 assert!(!Process::is_system_process(&make_proc(
625 Some("/home/user/.local/bin/app"),
626 Some("/home/user")
627 )));
628 }
629
630 #[cfg(unix)]
631 #[test]
632 fn test_parse_signal_name_valid() {
633 use nix::sys::signal::Signal;
634
635 assert_eq!(parse_signal_name("HUP").unwrap(), Signal::SIGHUP);
636 assert_eq!(parse_signal_name("hup").unwrap(), Signal::SIGHUP);
637 assert_eq!(parse_signal_name("SIGHUP").unwrap(), Signal::SIGHUP);
638 assert_eq!(parse_signal_name("sighup").unwrap(), Signal::SIGHUP);
639 assert_eq!(parse_signal_name("INT").unwrap(), Signal::SIGINT);
640 assert_eq!(parse_signal_name("QUIT").unwrap(), Signal::SIGQUIT);
641 assert_eq!(parse_signal_name("ABRT").unwrap(), Signal::SIGABRT);
642 assert_eq!(parse_signal_name("KILL").unwrap(), Signal::SIGKILL);
643 assert_eq!(parse_signal_name("TERM").unwrap(), Signal::SIGTERM);
644 assert_eq!(parse_signal_name("STOP").unwrap(), Signal::SIGSTOP);
645 assert_eq!(parse_signal_name("CONT").unwrap(), Signal::SIGCONT);
646 assert_eq!(parse_signal_name("USR1").unwrap(), Signal::SIGUSR1);
647 assert_eq!(parse_signal_name("USR2").unwrap(), Signal::SIGUSR2);
648 }
649
650 #[cfg(unix)]
651 #[test]
652 fn test_parse_signal_name_invalid() {
653 assert!(parse_signal_name("INVALID").is_err());
654 assert!(parse_signal_name("FOO").is_err());
655 assert!(parse_signal_name("").is_err());
656 }
657
658 #[cfg(unix)]
659 #[test]
660 fn test_parse_signal_name_case_insensitive() {
661 use nix::sys::signal::Signal;
662
663 assert_eq!(parse_signal_name("term").unwrap(), Signal::SIGTERM);
664 assert_eq!(parse_signal_name("Term").unwrap(), Signal::SIGTERM);
665 assert_eq!(parse_signal_name("TERM").unwrap(), Signal::SIGTERM);
666 assert_eq!(parse_signal_name("sigterm").unwrap(), Signal::SIGTERM);
667 assert_eq!(parse_signal_name("SigTerm").unwrap(), Signal::SIGTERM);
668 }
669
670 #[cfg(unix)]
671 #[test]
672 fn test_send_signal_nonexistent_process() {
673 use nix::sys::signal::Signal;
674
675 let proc = Process {
677 pid: 99999999,
678 name: "ghost".to_string(),
679 exe_path: None,
680 cwd: None,
681 command: None,
682 cpu_percent: 0.0,
683 memory_mb: 0.0,
684 status: ProcessStatus::Running,
685 user: None,
686 parent_pid: None,
687 start_time: None,
688 uptime_secs: None,
689 };
690
691 let result = proc.send_signal(Signal::SIGCONT);
692 assert!(result.is_err());
693 }
694}