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