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