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