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