1use crate::core::name_match::{classify, Hit, NameMatch, Names};
7use crate::error::{ProcError, Result};
8use serde::{Deserialize, Serialize};
9use std::sync::OnceLock;
10use std::time::Duration;
11use sysinfo::{Pid, ProcessStatus as SysProcessStatus, System, Uid, Users};
12
13fn user_name(uid: &Uid) -> String {
16 static USERS: OnceLock<Users> = OnceLock::new();
17 USERS
18 .get_or_init(Users::new_with_refreshed_list)
19 .get_user_by_id(uid)
20 .map(|user| user.name().to_string())
21 .unwrap_or_else(|| uid.to_string())
22}
23
24pub fn escape_control(text: &str) -> std::borrow::Cow<'_, str> {
36 fn needs_escape(c: char) -> bool {
37 c.is_control()
38 || matches!(c, '\u{200e}' | '\u{200f}' | '\u{202a}'..='\u{202e}' | '\u{2066}'..='\u{2069}')
39 }
40 if !text.chars().any(needs_escape) {
41 return std::borrow::Cow::Borrowed(text);
42 }
43 let mut out = String::with_capacity(text.len() + 8);
44 for c in text.chars() {
45 match c {
46 '\n' => out.push_str("\\n"),
47 '\r' => out.push_str("\\r"),
48 '\t' => out.push_str("\\t"),
49 c if c.is_control() => out.push_str(&format!("\\x{:02x}", c as u32)),
50 c if needs_escape(c) => out.push_str(&format!("\\u{{{:x}}}", c as u32)),
51 c => out.push(c),
52 }
53 }
54 std::borrow::Cow::Owned(out)
55}
56
57const STUCK_READINGS: usize = 2;
60const STUCK_READING_GAP: Duration = Duration::from_secs(1);
61
62fn looks_stuck(cpu_percent: f32, status: SysProcessStatus) -> bool {
65 cpu_percent > 50.0 || status == SysProcessStatus::UninterruptibleDiskSleep
66}
67
68fn cpu_percent_between(used_before_ms: u64, used_after_ms: u64, wall: Duration) -> f32 {
74 let wall_ms = wall.as_secs_f64() * 1000.0;
75 if wall_ms <= 0.0 {
76 return 0.0;
77 }
78 (used_after_ms.saturating_sub(used_before_ms) as f64 / wall_ms * 100.0) as f32
79}
80
81#[cfg(not(unix))]
83pub(crate) fn orphans_not_supported() -> ProcError {
84 ProcError::NotSupported(
85 "orphans is not supported on Windows: Windows doesn't re-parent processes, \
86 so there is no reliable way to tell an orphan"
87 .to_string(),
88 )
89}
90
91#[cfg(any(windows, test))]
95fn command_failure(stdout: &[u8], stderr: &[u8], status: &str) -> String {
96 [stderr, stdout]
97 .iter()
98 .map(|out| {
99 String::from_utf8_lossy(out)
101 .lines()
102 .map(str::trim)
103 .filter(|line| !line.is_empty())
104 .collect::<Vec<_>>()
105 .join(" ")
106 })
107 .find(|text| !text.is_empty())
108 .unwrap_or_else(|| format!("exited with {status}"))
109}
110
111fn kill_outcome(delivered: bool, pid: u32) -> Result<()> {
114 if delivered {
115 Ok(())
116 } else {
117 Err(ProcError::SignalError(format!(
118 "Failed to kill process {}",
119 pid
120 )))
121 }
122}
123
124#[cfg(any(windows, test))]
127fn taskkill_outcome(output: &std::process::Output) -> Result<()> {
128 if output.status.success() {
129 Ok(())
130 } else {
131 Err(ProcError::SignalError(command_failure(
132 &output.stdout,
133 &output.stderr,
134 &output.status.to_string(),
135 )))
136 }
137}
138
139fn cpu_used(sys: &System) -> std::collections::HashMap<Pid, u64> {
141 sys.processes()
142 .iter()
143 .map(|(pid, p)| (*pid, p.accumulated_cpu_time()))
144 .collect()
145}
146
147fn ran_long_enough(run_time: u64, timeout_secs: u64) -> bool {
151 run_time >= timeout_secs
152}
153
154fn counts_as_stuck(is_thread: bool, run_time: u64, timeout_secs: u64, looks_stuck: bool) -> bool {
158 !is_thread && ran_long_enough(run_time, timeout_secs) && looks_stuck
159}
160
161#[cfg(target_os = "macos")]
165fn macos_zombies(known: &std::collections::HashSet<u32>) -> Vec<Process> {
166 let table = read_proc_table(|buf, size| {
167 let mut mib = [libc::CTL_KERN, libc::KERN_PROC, libc::KERN_PROC_ALL, 0];
168 let null = std::ptr::null_mut();
169 unsafe { libc::sysctl(mib.as_mut_ptr(), 4, buf, size, null, 0) }
172 });
173 zombies_in(&table, known)
174}
175
176#[cfg(target_os = "macos")]
179const KINFO_PROC_SIZE: usize = 648;
180
181#[cfg(target_os = "macos")]
185fn read_proc_table(
186 sysctl: impl Fn(*mut libc::c_void, &mut libc::size_t) -> libc::c_int,
187) -> Vec<u8> {
188 let mut size: libc::size_t = 0;
189 if sysctl(std::ptr::null_mut(), &mut size) != 0 {
191 return Vec::new();
192 }
193 size += 64 * KINFO_PROC_SIZE;
195 let mut buf: Vec<u8> = vec![0; size];
196 if sysctl(buf.as_mut_ptr().cast(), &mut size) != 0 {
197 return Vec::new();
198 }
199 buf.truncate(size);
200 buf
201}
202
203#[cfg(target_os = "macos")]
205fn zombies_in(buf: &[u8], known: &std::collections::HashSet<u32>) -> Vec<Process> {
206 const P_STARTTIME_SEC: usize = 0;
207 const P_STAT: usize = 36;
208 const P_PID: usize = 40;
209 const P_COMM: usize = 243; const P_RUID: usize = 392;
211 const E_PPID: usize = 560;
212 const SZOMB: u8 = 5;
213
214 if !buf.len().is_multiple_of(KINFO_PROC_SIZE) {
215 return Vec::new(); }
217
218 let read_u32 = |p: &[u8], at: usize| u32::from_ne_bytes(p[at..at + 4].try_into().unwrap());
219 let now = std::time::SystemTime::now()
220 .duration_since(std::time::UNIX_EPOCH)
221 .map(|d| d.as_secs())
222 .unwrap_or(0);
223
224 buf.as_chunks::<KINFO_PROC_SIZE>()
225 .0
226 .iter()
227 .filter(|p| p[P_STAT] == SZOMB)
228 .filter_map(|p| {
229 let pid = read_u32(p, P_PID);
230 if pid == 0 || known.contains(&pid) {
231 return None;
232 }
233 let comm = &p[P_COMM..P_COMM + 17];
234 let comm = &comm[..comm.iter().position(|&c| c == 0).unwrap_or(comm.len())];
235 let name = escape_control(&String::from_utf8_lossy(comm)).into_owned();
236 let start =
237 u64::from_ne_bytes(p[P_STARTTIME_SEC..P_STARTTIME_SEC + 8].try_into().unwrap());
238 Some(Process {
239 pid,
240 name: if name.is_empty() {
241 "<defunct>".to_string()
242 } else {
243 name
244 },
245 exe_path: None,
246 cwd: None,
247 command: None,
248 argv0: None,
249 raw: None,
250 cpu_percent: 0.0,
251 memory_mb: 0.0,
252 status: ProcessStatus::Zombie,
253 user: Uid::try_from(read_u32(p, P_RUID) as usize)
254 .ok()
255 .map(|uid| escape_control(&user_name(&uid)).into_owned()),
256 parent_pid: Some(read_u32(p, E_PPID)),
257 start_time: (start > 0).then_some(start),
258 uptime_secs: (start > 0).then(|| now.saturating_sub(start)),
259 })
260 })
261 .collect()
262}
263
264pub fn is_thread(proc: &sysinfo::Process) -> bool {
268 proc.thread_kind().is_some()
269}
270
271fn strip_deleted_marker(cwd: String) -> String {
275 match cwd.strip_suffix(" (deleted)") {
276 Some(path) if !std::path::Path::new(&cwd).exists() => path.to_string(),
277 _ => cwd,
278 }
279}
280
281#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
283#[serde(rename_all = "lowercase")]
284pub enum ProcessStatus {
285 Running,
287 Sleeping,
289 Stopped,
291 Zombie,
293 Dead,
295 Unknown,
297}
298
299impl From<SysProcessStatus> for ProcessStatus {
300 fn from(status: SysProcessStatus) -> Self {
301 match status {
302 SysProcessStatus::Run => ProcessStatus::Running,
303 SysProcessStatus::Sleep => ProcessStatus::Sleeping,
304 SysProcessStatus::Stop => ProcessStatus::Stopped,
305 SysProcessStatus::Zombie => ProcessStatus::Zombie,
306 SysProcessStatus::Dead => ProcessStatus::Dead,
307 _ => ProcessStatus::Unknown,
308 }
309 }
310}
311
312#[derive(Debug, Clone, Serialize, Deserialize)]
314pub struct Process {
315 pub pid: u32,
317 pub name: String,
319 #[serde(skip_serializing_if = "Option::is_none")]
321 pub exe_path: Option<String>,
322 #[serde(skip_serializing_if = "Option::is_none")]
324 pub cwd: Option<String>,
325 #[serde(skip_serializing_if = "Option::is_none")]
327 pub command: Option<String>,
328 #[serde(skip)]
332 pub argv0: Option<String>,
333 #[serde(skip)]
339 pub raw: Option<Box<RawText>>,
340 pub cpu_percent: f32,
342 pub memory_mb: f64,
344 pub status: ProcessStatus,
346 #[serde(skip_serializing_if = "Option::is_none")]
348 pub user: Option<String>,
349 #[serde(skip_serializing_if = "Option::is_none")]
351 pub parent_pid: Option<u32>,
352 #[serde(skip_serializing_if = "Option::is_none")]
354 pub start_time: Option<u64>,
355 #[serde(skip_serializing_if = "Option::is_none")]
357 pub uptime_secs: Option<u64>,
358}
359
360pub(crate) fn self_and_ancestors(sys: &System) -> std::collections::HashSet<Pid> {
364 let self_pid = Pid::from_u32(std::process::id());
365 let mut pids = std::collections::HashSet::new();
366 pids.insert(self_pid);
367 let mut current = self_pid;
368 for _ in 0..10 {
369 if let Some(parent) = sys.process(current).and_then(|p| p.parent()) {
370 pids.insert(parent);
371 current = parent;
372 } else {
373 break;
374 }
375 }
376 pids
377}
378
379#[derive(Debug, Clone)]
382pub struct RawText {
383 pub name: String,
385 pub exe_path: Option<String>,
387 pub command: Option<String>,
389}
390
391pub(crate) fn classify_sysinfo(pattern: &str, proc: &sysinfo::Process, mode: NameMatch) -> Hit {
395 let name = proc.name().to_string_lossy();
396 let exe = proc.exe().map(|exe| exe.to_string_lossy());
397 let argv0 = proc.cmd().first().map(|arg| arg.to_string_lossy());
398 let command = (!proc.cmd().is_empty()).then(|| {
400 proc.cmd()
401 .iter()
402 .map(|arg| arg.to_string_lossy())
403 .collect::<Vec<_>>()
404 .join(" ")
405 });
406 classify(
407 pattern,
408 &Names {
409 name: &name,
410 exe: exe.as_deref(),
411 argv0: argv0.as_deref(),
412 command: command.as_deref(),
413 },
414 mode,
415 )
416}
417
418impl Process {
419 pub fn names(&self) -> Names<'_> {
421 match &self.raw {
422 Some(raw) => Names {
423 name: &raw.name,
424 exe: raw.exe_path.as_deref(),
425 argv0: self.argv0.as_deref(),
426 command: raw.command.as_deref(),
427 },
428 None => Names {
429 name: &self.name,
430 exe: self.exe_path.as_deref(),
431 argv0: self.argv0.as_deref(),
432 command: self.command.as_deref(),
433 },
434 }
435 }
436
437 pub fn find_by_name(pattern: &str, mode: NameMatch) -> Result<Vec<Process>> {
444 let mut sys = System::new_all();
445 sys.refresh_all();
446
447 let ancestor_pids = self_and_ancestors(&sys);
450 let mut in_command_line_only = 0;
451 let mut processes: Vec<Process> = sys
452 .processes()
453 .iter()
454 .filter_map(|(pid, proc)| {
455 if ancestor_pids.contains(pid) || is_thread(proc) {
457 return None;
458 }
459 match classify_sysinfo(pattern, proc, mode) {
460 Hit::Match => Some(Process::from_sysinfo(*pid, proc)),
461 Hit::CommandLineOnly => {
462 in_command_line_only += 1;
463 None
464 }
465 Hit::No => None,
466 }
467 })
468 .collect();
469 processes.sort_by_key(|p| p.pid);
472
473 if processes.is_empty() {
474 return Err(if in_command_line_only > 0 {
475 ProcError::NameNotFound {
476 name: pattern.to_string(),
477 in_command_line: in_command_line_only,
478 }
479 } else {
480 ProcError::ProcessNotFound(pattern.to_string())
481 });
482 }
483
484 Ok(processes)
485 }
486
487 pub fn find_by_pid(pid: u32) -> Result<Option<Process>> {
489 let mut sys = System::new_all();
490 sys.refresh_all();
491
492 let sysinfo_pid = Pid::from_u32(pid);
493
494 Ok(sys
495 .processes()
496 .get(&sysinfo_pid)
497 .map(|proc| Process::from_sysinfo(sysinfo_pid, proc)))
498 }
499
500 pub fn find_all() -> Result<Vec<Process>> {
502 let mut sys = System::new_all();
503 sys.refresh_all();
504
505 let self_pid = sysinfo::Pid::from_u32(std::process::id());
506 #[allow(unused_mut)]
507 let mut processes: Vec<Process> = sys
508 .processes()
509 .iter()
510 .filter(|(pid, proc)| **pid != self_pid && !is_thread(proc))
511 .map(|(pid, proc)| Process::from_sysinfo(*pid, proc))
512 .collect();
513
514 #[cfg(target_os = "macos")]
515 {
516 let known: std::collections::HashSet<u32> = processes.iter().map(|p| p.pid).collect();
517 processes.extend(macos_zombies(&known));
518 }
519
520 processes.sort_by_key(|p| p.pid);
524
525 Ok(processes)
526 }
527
528 pub fn find_by_exe_path(path: &std::path::Path) -> Result<Vec<Process>> {
530 let all = Self::find_all()?;
531 let path_str = path.to_string_lossy();
532
533 Ok(all
534 .into_iter()
535 .filter(|p| {
536 if let Some(ref exe) = p.exe_path {
537 exe == &*path_str || std::path::Path::new(exe) == path
539 } else {
540 false
541 }
542 })
543 .collect())
544 }
545
546 #[cfg(unix)]
548 pub fn find_by_open_file(path: &std::path::Path) -> Result<Vec<Process>> {
549 Self::find_by_open_file_with("lsof", path)
550 }
551
552 #[cfg(unix)]
554 fn find_by_open_file_with(lsof: &str, path: &std::path::Path) -> Result<Vec<Process>> {
555 use std::process::Command;
556
557 let output = Command::new(lsof)
558 .args(["-t", &path.to_string_lossy()]) .output();
560
561 let output = match output {
562 Ok(o) => o,
563 Err(_) => return Ok(vec![]), };
565
566 if !output.status.success() {
567 return Ok(vec![]); }
569
570 let pids: Vec<u32> = String::from_utf8_lossy(&output.stdout)
571 .lines()
572 .filter_map(|line| line.trim().parse().ok())
573 .collect();
574
575 let mut processes = Vec::new();
576 for pid in pids {
577 if let Ok(Some(proc)) = Self::find_by_pid(pid) {
578 processes.push(proc);
579 }
580 }
581
582 Ok(processes)
583 }
584
585 #[cfg(not(unix))]
587 pub fn find_by_open_file(_path: &std::path::Path) -> Result<Vec<Process>> {
588 Ok(vec![])
590 }
591
592 pub fn find_stuck(timeout: Duration) -> Result<Vec<Process>> {
598 let timeout_secs = timeout.as_secs();
599 let mut read_at = std::time::Instant::now();
601 let mut sys = System::new_all();
602 let mut used = cpu_used(&sys);
603 let mut cpu: std::collections::HashMap<Pid, f32> = std::collections::HashMap::new();
604 let mut stuck: Option<std::collections::HashSet<Pid>> = None;
605
606 for _ in 0..STUCK_READINGS {
607 std::thread::sleep(STUCK_READING_GAP);
608 let started = std::time::Instant::now();
609 sys.refresh_processes(sysinfo::ProcessesToUpdate::All, true);
610 let wall = started - read_at;
611 read_at = started;
612 cpu = sys
614 .processes()
615 .iter()
616 .filter_map(|(pid, p)| {
617 let before = used.get(pid)?;
618 Some((
619 *pid,
620 cpu_percent_between(*before, p.accumulated_cpu_time(), wall),
621 ))
622 })
623 .collect();
624 used = cpu_used(&sys);
625 let now: std::collections::HashSet<Pid> = sys
626 .processes()
627 .iter()
628 .filter(|(pid, p)| {
629 let busy = cpu.get(pid).copied().unwrap_or(0.0);
630 counts_as_stuck(
631 is_thread(p),
632 p.run_time(),
633 timeout_secs,
634 looks_stuck(busy, p.status()),
635 )
636 })
637 .map(|(pid, _)| *pid)
638 .collect();
639 stuck = Some(match stuck {
640 None => now,
641 Some(before) => before.intersection(&now).copied().collect(),
642 });
643 }
644
645 let stuck = stuck.unwrap_or_default();
646 let mut found: Vec<Process> = sys
647 .processes()
648 .iter()
649 .filter(|(pid, _)| stuck.contains(pid))
650 .map(|(pid, p)| {
651 let mut process = Process::from_sysinfo(*pid, p);
652 if let Some(&busy) = cpu.get(pid) {
654 process.cpu_percent = busy;
655 }
656 process
657 })
658 .collect();
659 found.sort_by_key(|p| p.pid);
661 Ok(found)
662 }
663
664 pub fn looks_stuck_now(&self) -> bool {
667 let pid = Pid::from_u32(self.pid);
668 let mut sys = System::new();
669 let mut read_at = std::time::Instant::now();
670 sys.refresh_processes(sysinfo::ProcessesToUpdate::Some(&[pid]), true);
671 let mut used = sys
672 .process(pid)
673 .filter(|p| self.is_same(p))
674 .map(|p| p.accumulated_cpu_time());
675 (0..STUCK_READINGS).all(|_| {
676 std::thread::sleep(STUCK_READING_GAP);
677 let started = std::time::Instant::now();
678 sys.refresh_processes(sysinfo::ProcessesToUpdate::Some(&[pid]), true);
679 let wall = started - read_at;
680 read_at = started;
681 let (Some(before), Some(p)) = (used, sys.process(pid).filter(|p| self.is_same(p)))
682 else {
683 return false;
684 };
685 let after = p.accumulated_cpu_time();
686 used = Some(after);
687 looks_stuck(cpu_percent_between(before, after, wall), p.status())
688 })
689 }
690
691 #[cfg(unix)]
698 pub fn cpu_over(&self, window: Duration) -> Option<f32> {
699 let pid = Pid::from_u32(self.pid);
700 let mut sys = System::new();
701 let started = std::time::Instant::now();
702 sys.refresh_processes(sysinfo::ProcessesToUpdate::Some(&[pid]), true);
703 let before = sys
704 .process(pid)
705 .filter(|p| self.is_same(p))
706 .map(|p| p.accumulated_cpu_time())?;
707 std::thread::sleep(window.max(sysinfo::MINIMUM_CPU_UPDATE_INTERVAL));
708 let wall = started.elapsed();
709 sys.refresh_processes(sysinfo::ProcessesToUpdate::Some(&[pid]), true);
710 sys.process(pid)
711 .filter(|p| self.is_same(p))
712 .map(|p| cpu_percent_between(before, p.accumulated_cpu_time(), wall))
713 }
714
715 pub fn measure_cpu(processes: &mut [Process]) {
726 if processes.is_empty() {
727 return;
728 }
729 let pids: Vec<Pid> = processes.iter().map(|p| Pid::from_u32(p.pid)).collect();
730 let which = sysinfo::ProcessesToUpdate::Some(&pids);
731 let cpu_only = sysinfo::ProcessRefreshKind::nothing().with_cpu();
732 let mut sys = System::new();
733
734 let first = std::time::Instant::now();
735 sys.refresh_processes_specifics(which, true, cpu_only);
736 let before = cpu_used(&sys);
737 std::thread::sleep(sysinfo::MINIMUM_CPU_UPDATE_INTERVAL);
738 let wall = first.elapsed();
741 sys.refresh_processes_specifics(which, true, cpu_only);
742
743 for process in processes.iter_mut() {
744 let pid = Pid::from_u32(process.pid);
745 let (Some(current), Some(&cpu_before)) = (sys.process(pid), before.get(&pid)) else {
746 continue;
747 };
748 if process.is_same(current) {
749 process.cpu_percent =
750 cpu_percent_between(cpu_before, current.accumulated_cpu_time(), wall);
751 }
752 }
753 }
754
755 pub fn kill(&self) -> Result<()> {
757 let mut sys = System::new();
758 sys.refresh_processes(
759 sysinfo::ProcessesToUpdate::Some(&[Pid::from_u32(self.pid)]),
760 true,
761 );
762
763 match sys.process(Pid::from_u32(self.pid)) {
764 Some(proc) if self.is_same(proc) => kill_outcome(proc.kill(), self.pid),
765 _ => Err(ProcError::ProcessNotFound(self.pid.to_string())),
767 }
768 }
769
770 fn is_same(&self, current: &sysinfo::Process) -> bool {
773 match self.start_time {
774 Some(ours) if current.start_time() > 0 => current.start_time().abs_diff(ours) <= 1,
775 _ => true,
776 }
777 }
778
779 fn ensure_same_process(&self) -> Result<()> {
783 let mut sys = System::new();
784 let pid = Pid::from_u32(self.pid);
785 sys.refresh_processes(sysinfo::ProcessesToUpdate::Some(&[pid]), true);
786 match sys.process(pid) {
787 Some(current) if self.is_same(current) => Ok(()),
788 _ => Err(ProcError::ProcessNotFound(self.pid.to_string())),
789 }
790 }
791
792 pub fn kill_and_wait(&self) -> Result<()> {
798 self.kill_and_wait_within(Duration::from_secs(5))
799 }
800
801 fn kill_and_wait_within(&self, wait: Duration) -> Result<()> {
803 self.kill_then_wait(wait, Process::kill)
804 }
805
806 fn kill_then_wait(&self, wait: Duration, kill: impl Fn(&Process) -> Result<()>) -> Result<()> {
810 match kill(self) {
811 Err(ProcError::ProcessNotFound(_)) => return Ok(()),
813 other => other?,
814 }
815 let deadline = std::time::Instant::now() + wait;
816 while std::time::Instant::now() < deadline {
817 if !self.is_running() {
818 return Ok(());
819 }
820 std::thread::sleep(Duration::from_millis(50));
821 }
822 Err(ProcError::SignalError(format!(
823 "Process {} is still running {} seconds after SIGKILL",
824 self.pid,
825 wait.as_secs()
826 )))
827 }
828
829 #[cfg(unix)]
831 pub fn terminate(&self) -> Result<()> {
832 use nix::sys::signal::{kill, Signal};
833 use nix::unistd::Pid as NixPid;
834
835 self.ensure_same_process()?;
836 kill(NixPid::from_raw(self.pid as i32), Signal::SIGTERM)
837 .map_err(|e| ProcError::SignalError(e.to_string()))
838 }
839
840 #[cfg(windows)]
845 pub fn terminate(&self) -> Result<()> {
846 use std::process::Command;
847
848 self.ensure_same_process()?;
849 let output = Command::new("taskkill")
850 .args(["/PID", &self.pid.to_string()])
851 .output()
852 .map_err(|e| ProcError::SystemError(e.to_string()))?;
853
854 taskkill_outcome(&output)
855 }
856
857 #[cfg(unix)]
859 pub fn send_signal(&self, signal: nix::sys::signal::Signal) -> Result<()> {
860 use nix::sys::signal::kill;
861 use nix::unistd::Pid as NixPid;
862 self.ensure_same_process()?;
863 kill(NixPid::from_raw(self.pid as i32), signal)
864 .map_err(|e| ProcError::SignalError(format!("{}: {}", signal, e)))
865 }
866
867 #[cfg(unix)]
876 pub fn find_orphans() -> Result<Vec<Process>> {
877 Ok(Self::orphans_among(Self::find_all()?))
878 }
879
880 #[cfg(not(unix))]
882 pub fn find_orphans() -> Result<Vec<Process>> {
883 Err(orphans_not_supported())
884 }
885
886 #[cfg(unix)]
888 fn orphans_among(all: Vec<Process>) -> Vec<Process> {
889 all.into_iter()
890 .filter(|p| {
891 if let Some(ppid) = p.parent_pid {
892 ppid == 1 && p.pid != 1 && !Self::is_system_process(p)
893 } else {
894 false
895 }
896 })
897 .collect()
898 }
899
900 #[cfg(unix)]
906 fn is_system_process(p: &Process) -> bool {
907 if p.cwd.is_none() || p.cwd.as_deref() == Some("/") {
908 if let Some(ref exe) = p.exe_path {
909 if exe.starts_with("/System/") || exe.starts_with("/usr/libexec/") {
911 return true;
912 }
913 if exe.starts_with("/usr/sbin/")
915 || exe.starts_with("/sbin/")
916 || exe.starts_with("/usr/bin/")
917 || exe.starts_with("/usr/lib/")
918 || exe.starts_with("/usr/lib64/")
919 || exe.starts_with("/lib/")
920 || exe.starts_with("/lib64/")
921 || exe.starts_with("/opt/")
922 || exe.starts_with("/snap/")
923 {
924 return true;
925 }
926 }
927 return true; }
929 false
930 }
931
932 pub fn exists(&self) -> bool {
936 let mut sys = System::new();
937 let pid = Pid::from_u32(self.pid);
938 sys.refresh_processes(sysinfo::ProcessesToUpdate::Some(&[pid]), true);
939 match sys.process(pid) {
940 None => false,
941 Some(current) if current.status() == SysProcessStatus::Zombie => false,
942 Some(current) => self.is_same(current),
943 }
944 }
945
946 pub fn is_running(&self) -> bool {
948 self.exists()
949 }
950
951 pub(crate) fn from_sysinfo(pid: Pid, proc: &sysinfo::Process) -> Self {
953 let cmd_vec = proc.cmd();
954 let raw_command = (!cmd_vec.is_empty()).then(|| {
958 cmd_vec
959 .iter()
960 .map(|s| s.to_string_lossy())
961 .collect::<Vec<_>>()
962 .join(" ")
963 });
964 let raw_name = proc.name().to_string_lossy();
965 let raw_exe = proc.exe().map(|p| p.to_string_lossy());
966 let changed = |text: &str| matches!(escape_control(text), std::borrow::Cow::Owned(_));
967 let raw = (changed(&raw_name)
968 || raw_exe.as_deref().is_some_and(changed)
969 || raw_command.as_deref().is_some_and(changed))
970 .then(|| {
971 Box::new(RawText {
972 name: raw_name.to_string(),
973 exe_path: raw_exe.as_ref().map(|exe| exe.to_string()),
974 command: raw_command.clone(),
975 })
976 });
977 let command = raw_command
978 .as_deref()
979 .map(|joined| escape_control(joined).into_owned());
980
981 let exe_path = raw_exe
982 .as_deref()
983 .map(|exe| escape_control(exe).into_owned());
984 let cwd = proc.cwd().map(|p| {
985 let path = strip_deleted_marker(p.to_string_lossy().to_string());
986 escape_control(&path).into_owned()
987 });
988
989 Process {
990 pid: pid.as_u32(),
991 name: escape_control(&raw_name).into_owned(),
992 exe_path,
993 cwd,
994 command,
995 argv0: cmd_vec
996 .first()
997 .map(|arg| arg.to_string_lossy().into_owned()),
998 raw,
999 cpu_percent: proc.cpu_usage(),
1000 memory_mb: proc.memory() as f64 / 1024.0 / 1024.0,
1001 status: ProcessStatus::from(proc.status()),
1002 user: proc
1003 .user_id()
1004 .map(|uid| escape_control(&user_name(uid)).into_owned()),
1005 parent_pid: proc.parent().map(|p| p.as_u32()),
1006 start_time: (proc.start_time() > 0).then(|| proc.start_time()),
1009 uptime_secs: (proc.start_time() > 0).then(|| proc.run_time()),
1010 }
1011 }
1012}
1013
1014#[cfg(unix)]
1019pub fn parse_signal_name(name: &str) -> Result<nix::sys::signal::Signal> {
1020 use nix::sys::signal::Signal;
1021
1022 let upper = name.to_uppercase();
1023 let upper = upper.trim_start_matches("SIG");
1024 match upper {
1025 "HUP" => Ok(Signal::SIGHUP),
1026 "INT" => Ok(Signal::SIGINT),
1027 "QUIT" => Ok(Signal::SIGQUIT),
1028 "ABRT" => Ok(Signal::SIGABRT),
1029 "KILL" => Ok(Signal::SIGKILL),
1030 "TERM" => Ok(Signal::SIGTERM),
1031 "STOP" => Ok(Signal::SIGSTOP),
1032 "CONT" => Ok(Signal::SIGCONT),
1033 "USR1" => Ok(Signal::SIGUSR1),
1034 "USR2" => Ok(Signal::SIGUSR2),
1035 _ => Err(ProcError::InvalidInput(format!(
1036 "Unknown signal: '{}'. Valid signals: HUP, INT, QUIT, ABRT, KILL, TERM, STOP, CONT, USR1, USR2",
1037 name
1038 ))),
1039 }
1040}
1041
1042#[cfg(test)]
1043mod tests {
1044 use super::*;
1045
1046 #[test]
1047 fn cpu_is_the_cpu_time_used_over_the_wall_time() {
1048 let second = Duration::from_secs(1);
1049 assert_eq!(cpu_percent_between(5_000, 6_000, second), 100.0);
1051 assert_eq!(cpu_percent_between(5_000, 5_500, second), 50.0);
1052 assert_eq!(cpu_percent_between(5_000, 5_000, second), 0.0);
1053 assert_eq!(cpu_percent_between(0, 2_000, second), 200.0);
1055 assert_eq!(cpu_percent_between(6_000, 5_000, second), 0.0);
1057 assert_eq!(cpu_percent_between(0, 1_000, Duration::ZERO), 0.0);
1058 }
1059
1060 #[test]
1061 fn a_failed_command_is_explained_in_its_own_words() {
1062 let reason = "ERROR: The process with PID 7 could not be terminated.";
1063 assert_eq!(
1064 command_failure(b"", reason.as_bytes(), "exit code: 1"),
1065 reason
1066 );
1067 assert_eq!(
1069 command_failure(b" not here\r\n", b"", "exit code: 1"),
1070 "not here"
1071 );
1072 assert_eq!(
1074 command_failure(
1075 b"",
1076 b"ERROR: first part\r\n second part.\r\n\r\n",
1077 "exit code: 1"
1078 ),
1079 "ERROR: first part second part."
1080 );
1081 assert_eq!(
1082 command_failure(b"", b" \n", "exit code: 128"),
1083 "exited with exit code: 128"
1084 );
1085 }
1086
1087 #[test]
1088 fn busy_means_over_half_a_core_or_blocked_in_the_kernel() {
1089 assert!(looks_stuck(50.1, SysProcessStatus::Run));
1090 assert!(!looks_stuck(50.0, SysProcessStatus::Run));
1091 assert!(!looks_stuck(0.0, SysProcessStatus::Sleep));
1092 assert!(looks_stuck(0.0, SysProcessStatus::UninterruptibleDiskSleep));
1093 }
1094
1095 #[test]
1096 fn a_reading_counts_as_stuck_from_the_timeout_on() {
1097 assert!(counts_as_stuck(false, 0, 0, true));
1098 assert!(!counts_as_stuck(false, 59, 60, true));
1099 assert!(counts_as_stuck(false, 60, 60, true));
1100 assert!(counts_as_stuck(false, 61, 60, true));
1101 assert!(!counts_as_stuck(false, 60, 60, false));
1103 assert!(!counts_as_stuck(true, 60, 60, true));
1104 }
1105
1106 #[test]
1107 fn a_process_that_ran_the_timeout_has_run_long_enough() {
1108 assert!(ran_long_enough(0, 0));
1110 assert!(ran_long_enough(5, 5));
1111 assert!(ran_long_enough(6, 5));
1112 assert!(!ran_long_enough(4, 5));
1113 }
1114
1115 #[test]
1116 fn test_find_all_processes() {
1117 let processes = Process::find_all().unwrap();
1118 assert!(!processes.is_empty(), "Should find at least one process");
1119 }
1120
1121 #[test]
1122 fn every_process_source_lists_pid_ascending() {
1123 use crate::test_support::{end, idle_child, record_of};
1124
1125 let mut children: Vec<_> = (0..3).map(|_| idle_child()).collect();
1126 let mine: Vec<u32> = children.iter().map(|c| record_of(c).pid).collect();
1127 let name = record_of(&children[0]).name;
1128
1129 let ascending = |what: &str, list: &[Process]| {
1130 let pids: Vec<u32> = list.iter().map(|p| p.pid).collect();
1131 let mut sorted = pids.clone();
1132 sorted.sort_unstable();
1133 sorted.dedup();
1134 assert_eq!(pids, sorted, "{what} is not PID ascending");
1135 pids
1136 };
1137 let all = ascending("find_all", &Process::find_all().unwrap());
1138 let named = ascending(
1139 "find_by_name",
1140 &Process::find_by_name(&name, NameMatch::Exact).unwrap(),
1141 );
1142 ascending("find_stuck", &Process::find_stuck(Duration::ZERO).unwrap());
1143 for pid in &mine {
1144 assert!(all.contains(pid), "find_all has {pid}");
1145 assert!(named.contains(pid), "find_by_name has {pid}");
1146 }
1147 children.iter_mut().for_each(end);
1148 }
1149
1150 #[test]
1151 fn test_find_by_pid_self() {
1152 let pid = std::process::id();
1153 let process = Process::find_by_pid(pid).unwrap();
1154 assert!(process.is_some(), "Should find own process");
1155 }
1156
1157 #[test]
1158 fn test_is_running_tells_a_reused_pid_apart() {
1159 let mut me = Process::find_by_pid(std::process::id()).unwrap().unwrap();
1160 assert!(me.is_running());
1161 let start = me.start_time.expect("a running process has a start time");
1163 me.start_time = Some(start - 3600);
1164 assert!(!me.is_running(), "a reused PID counted as the same process");
1165 }
1166
1167 #[test]
1168 fn test_escape_control_makes_terminal_input_harmless() {
1169 assert_eq!(escape_control("node server.js"), "node server.js");
1171 assert_eq!(escape_control("café ✓ 日本"), "café ✓ 日本");
1172 assert_eq!(escape_control("a\x1b[31mb"), "a\\x1b[31mb");
1174 assert_eq!(escape_control("\x1b]0;pwned\x07"), "\\x1b]0;pwned\\x07");
1175 assert_eq!(escape_control("one\ntwo\tthree\r"), "one\\ntwo\\tthree\\r");
1177 assert_eq!(escape_control("x\x7fy\u{9b}2J"), "x\\x7fy\\x9b2J");
1179 assert_eq!(escape_control("ab\u{202e}cd"), "ab\\u{202e}cd");
1181 }
1182
1183 #[cfg(unix)]
1184 #[test]
1185 fn test_looks_stuck_needs_sustained_cpu() {
1186 let spawn = |script: &str| {
1187 std::process::Command::new("sh")
1188 .args(["-c", script])
1189 .spawn()
1190 .unwrap()
1191 };
1192 let mut spinning = spawn("while :; do :; done");
1193 let mut idle = spawn("exec sleep 300");
1194 std::thread::sleep(Duration::from_millis(300));
1195 let record = |pid: u32| Process::find_by_pid(pid).unwrap().unwrap();
1196
1197 let spinning_stuck = record(spinning.id()).looks_stuck_now();
1198 let idle_stuck = record(idle.id()).looks_stuck_now();
1199 for child in [&mut spinning, &mut idle] {
1200 let _ = child.kill();
1201 let _ = child.wait();
1202 }
1203 assert!(
1204 spinning_stuck,
1205 "a process spinning at 100% CPU wasn't stuck"
1206 );
1207 assert!(!idle_stuck, "an idle process counted as stuck");
1208 }
1209
1210 #[test]
1211 fn test_signals_never_reach_a_process_that_reused_the_pid() {
1212 let mut child = crate::test_support::idle_child();
1213 let mut stale = crate::test_support::record_of(&child);
1214 stale.start_time = stale.start_time.map(|t| t - 3600);
1216
1217 let kill = stale.kill();
1218 let term = stale.terminate();
1219 #[cfg(unix)]
1220 let sig = stale.send_signal(nix::sys::signal::Signal::SIGKILL);
1221 let alive = child.try_wait().unwrap().is_none();
1222 let _ = child.kill();
1223 let _ = child.wait();
1224
1225 assert!(
1226 alive,
1227 "a stale record signalled the process now using its PID"
1228 );
1229 assert!(
1230 matches!(kill, Err(ProcError::ProcessNotFound(_))),
1231 "{kill:?}"
1232 );
1233 assert!(
1234 matches!(term, Err(ProcError::ProcessNotFound(_))),
1235 "{term:?}"
1236 );
1237 #[cfg(unix)]
1238 assert!(matches!(sig, Err(ProcError::ProcessNotFound(_))), "{sig:?}");
1239 }
1240
1241 #[cfg(unix)]
1245 #[test]
1246 fn text_with_control_characters_matches_as_typed_and_displays_escaped() {
1247 use crate::core::matches_by_filter;
1248 use std::os::unix::process::CommandExt;
1249
1250 let word = format!("zz\u{7}ctl{}x", std::process::id());
1251 let mut child = std::process::Command::new("sleep")
1252 .arg0(&word)
1253 .arg("300")
1254 .spawn()
1255 .unwrap();
1256 let pid = child.id();
1257 let record = std::iter::repeat_with(|| {
1262 std::thread::sleep(Duration::from_millis(5));
1263 Process::find_by_pid(pid).unwrap().unwrap()
1264 })
1265 .take(1000)
1266 .find(|record| record.argv0.is_some());
1267 let part = format!("\u{7}ctl{}", std::process::id());
1268
1269 let found = |pattern: &str, mode| {
1270 Process::find_by_name(pattern, mode)
1271 .map(|found| found.iter().any(|p| p.pid == pid))
1272 .unwrap_or(false)
1273 };
1274 let named = found(&word, NameMatch::Exact);
1275 let named_in_command = found(&part, NameMatch::Command);
1276 let _ = child.kill();
1277 let _ = child.wait();
1278
1279 let record = record.expect("the child's command line never appeared");
1280 assert!(named, "find_by_name read the escaped first argument");
1281 assert!(
1282 named_in_command,
1283 "find_by_name read the escaped command line"
1284 );
1285 assert!(
1286 matches_by_filter(&record, &word, NameMatch::Exact),
1287 "--by read the escaped first argument"
1288 );
1289 assert!(
1290 matches_by_filter(&record, &part, NameMatch::Command),
1291 "--by --cmd read the escaped command line"
1292 );
1293 let shown = record.command.as_deref().unwrap();
1295 assert!(shown.contains("\\x07ctl"), "{shown}");
1296 assert!(!shown.contains('\u{7}'), "{shown:?}");
1297 }
1298
1299 #[cfg(windows)]
1303 #[test]
1304 fn a_control_character_in_a_windows_command_line_matches_as_typed_and_displays_escaped() {
1305 use crate::core::matches_by_filter;
1306
1307 let mut exe = std::env::current_exe().unwrap();
1308 exe.pop();
1309 exe.pop();
1310 exe.push("examples");
1311 exe.push("listener.exe");
1312 assert!(exe.exists(), "build the example first: {}", exe.display());
1313 let part = format!("\u{7}ctl{}", std::process::id());
1314 let mut child = std::process::Command::new(&exe)
1315 .arg(&part)
1316 .stdin(std::process::Stdio::null())
1317 .stdout(std::process::Stdio::null())
1318 .spawn()
1319 .unwrap();
1320 let pid = child.id();
1321 let record = std::iter::repeat_with(|| {
1322 std::thread::sleep(Duration::from_millis(20));
1323 Process::find_by_pid(pid).unwrap()
1324 })
1325 .take(500)
1326 .flatten()
1327 .find(|record| record.command.is_some())
1328 .expect("the child's command line never appeared");
1329 let found = Process::find_by_name(&part, NameMatch::Command)
1330 .map(|found| found.iter().any(|p| p.pid == pid))
1331 .unwrap_or(false);
1332 let kept = matches_by_filter(&record, &part, NameMatch::Command);
1333 crate::test_support::end(&mut child);
1334
1335 assert!(found, "find_by_name read the escaped command line");
1336 assert!(kept, "--by --cmd read the escaped command line");
1337 let shown = record.command.as_deref().unwrap();
1338 assert!(shown.contains("\\x07ctl"), "{shown}");
1339 assert!(!shown.contains('\u{7}'), "{shown:?}");
1340 }
1341
1342 #[test]
1343 fn a_name_is_read_from_the_raw_text_when_escaping_changed_it() {
1344 let record = Process {
1345 name: "a\\x07b".to_string(),
1346 raw: Some(Box::new(RawText {
1347 name: "a\u{7}b".to_string(),
1348 exe_path: Some("c\u{7}d".to_string()),
1349 command: Some("e\u{7}f".to_string()),
1350 })),
1351 argv0: Some("g".to_string()),
1352 ..ghost()
1353 };
1354 let names = record.names();
1355 assert_eq!(names.name, "a\u{7}b");
1356 assert_eq!(names.exe, Some("c\u{7}d"));
1357 assert_eq!(names.argv0, Some("g"));
1358 assert_eq!(names.command, Some("e\u{7}f"));
1359 }
1360
1361 #[test]
1362 fn the_ancestors_stop_where_the_process_list_does() {
1363 let me = Pid::from_u32(std::process::id());
1366 let mut sys = System::new();
1367 sys.refresh_processes(sysinfo::ProcessesToUpdate::Some(&[me]), true);
1368 let parent = sys.process(me).and_then(|p| p.parent());
1369 let expected: std::collections::HashSet<Pid> = std::iter::once(me).chain(parent).collect();
1370 assert_eq!(self_and_ancestors(&sys), expected);
1371 }
1372
1373 #[test]
1374 fn test_strip_deleted_marker() {
1375 assert_eq!(
1376 strip_deleted_marker("/nonexistent/proc-gone (deleted)".to_string()),
1377 "/nonexistent/proc-gone"
1378 );
1379 assert_eq!(
1380 strip_deleted_marker("/tmp/project".to_string()),
1381 "/tmp/project"
1382 );
1383
1384 let real = std::env::temp_dir().join(format!("proc-real-{} (deleted)", std::process::id()));
1386 std::fs::create_dir_all(&real).unwrap();
1387 let path = real.to_string_lossy().to_string();
1388 assert_eq!(strip_deleted_marker(path.clone()), path);
1389 std::fs::remove_dir(&real).unwrap();
1390 }
1391
1392 #[test]
1393 fn a_name_nothing_has_is_plain_not_found_in_both_modes() {
1394 for mode in [NameMatch::Exact, NameMatch::Command] {
1395 let result = Process::find_by_name("nonexistent_process_12345", mode);
1396 assert!(
1397 matches!(&result, Err(ProcError::ProcessNotFound(n)) if n == "nonexistent_process_12345"),
1398 "{result:?}"
1399 );
1400 }
1401 }
1402
1403 #[cfg(unix)]
1407 fn waiting_shell_with(marker: &str) -> std::process::Child {
1408 std::process::Command::new("sh")
1409 .args(["-c", &format!("read line; : {marker}")])
1410 .stdin(std::process::Stdio::piped())
1411 .spawn()
1412 .unwrap()
1413 }
1414
1415 #[cfg(unix)]
1416 #[test]
1417 fn a_word_only_in_a_command_line_is_found_with_cmd_and_hinted_without() {
1418 let marker = format!("hintmarker{}", std::process::id());
1419 let mut child = waiting_shell_with(&marker);
1420 let pid = child.id();
1421 let exact = Process::find_by_name(&marker, NameMatch::Exact);
1422 let cmd = Process::find_by_name(&marker, NameMatch::Command);
1423 let _ = child.kill();
1424 let _ = child.wait();
1425
1426 assert!(
1429 matches!(&exact, Err(ProcError::NameNotFound { name, in_command_line })
1430 if name == &marker && *in_command_line >= 1),
1431 "{exact:?}"
1432 );
1433 let found = cmd.unwrap();
1435 assert_eq!(found.iter().map(|p| p.pid).collect::<Vec<_>>(), vec![pid]);
1436 }
1437
1438 #[cfg(unix)]
1439 #[test]
1440 fn test_find_orphans_returns_ok() {
1441 let result = Process::find_orphans();
1443 assert!(result.is_ok());
1444 }
1445
1446 #[cfg(not(unix))]
1447 #[test]
1448 fn find_orphans_is_not_supported_on_windows() {
1449 let result = Process::find_orphans();
1450 assert!(
1451 matches!(&result, Err(ProcError::NotSupported(m)) if m.starts_with("orphans is not supported on Windows")),
1452 "{result:?}"
1453 );
1454 }
1455
1456 #[cfg(unix)]
1457 #[test]
1458 fn test_is_system_process_system_paths() {
1459 let make_proc = |exe: Option<&str>, cwd: Option<&str>| Process {
1460 pid: 100,
1461 name: "test".to_string(),
1462 exe_path: exe.map(String::from),
1463 cwd: cwd.map(String::from),
1464 command: None,
1465 argv0: None,
1466 raw: None,
1467 cpu_percent: 0.0,
1468 memory_mb: 0.0,
1469 status: ProcessStatus::Running,
1470 user: None,
1471 parent_pid: Some(1),
1472 start_time: None,
1473 uptime_secs: None,
1474 };
1475
1476 assert!(Process::is_system_process(&make_proc(
1478 Some("/usr/sbin/sshd"),
1479 Some("/")
1480 )));
1481 assert!(Process::is_system_process(&make_proc(
1482 Some("/System/Library/foo"),
1483 Some("/")
1484 )));
1485 assert!(Process::is_system_process(&make_proc(
1486 Some("/usr/bin/systemd"),
1487 Some("/")
1488 )));
1489 assert!(Process::is_system_process(&make_proc(
1490 Some("/usr/lib/snapd/snapd"),
1491 Some("/")
1492 )));
1493
1494 assert!(Process::is_system_process(&make_proc(None, Some("/"))));
1496
1497 assert!(Process::is_system_process(&make_proc(
1499 Some("/usr/bin/foo"),
1500 None
1501 )));
1502
1503 for exe in [
1505 "/System/x",
1506 "/usr/libexec/x",
1507 "/usr/sbin/x",
1508 "/sbin/x",
1509 "/usr/bin/x",
1510 "/usr/lib/x",
1511 "/usr/lib64/x",
1512 "/lib/x",
1513 "/lib64/x",
1514 "/opt/x",
1515 "/snap/x",
1516 ] {
1517 assert!(
1518 Process::is_system_process(&make_proc(Some(exe), Some("/"))),
1519 "{exe}"
1520 );
1521 }
1522 assert!(Process::is_system_process(&make_proc(
1524 Some("/srv/x"),
1525 Some("/")
1526 )));
1527
1528 assert!(!Process::is_system_process(&make_proc(
1530 Some("/usr/bin/node"),
1531 Some("/home/user/project")
1532 )));
1533 assert!(!Process::is_system_process(&make_proc(
1534 Some("/home/user/.local/bin/app"),
1535 Some("/home/user")
1536 )));
1537 }
1538
1539 #[cfg(unix)]
1540 #[test]
1541 fn test_parse_signal_name_valid() {
1542 use nix::sys::signal::Signal;
1543
1544 assert_eq!(parse_signal_name("HUP").unwrap(), Signal::SIGHUP);
1545 assert_eq!(parse_signal_name("hup").unwrap(), Signal::SIGHUP);
1546 assert_eq!(parse_signal_name("SIGHUP").unwrap(), Signal::SIGHUP);
1547 assert_eq!(parse_signal_name("sighup").unwrap(), Signal::SIGHUP);
1548 assert_eq!(parse_signal_name("INT").unwrap(), Signal::SIGINT);
1549 assert_eq!(parse_signal_name("QUIT").unwrap(), Signal::SIGQUIT);
1550 assert_eq!(parse_signal_name("ABRT").unwrap(), Signal::SIGABRT);
1551 assert_eq!(parse_signal_name("KILL").unwrap(), Signal::SIGKILL);
1552 assert_eq!(parse_signal_name("TERM").unwrap(), Signal::SIGTERM);
1553 assert_eq!(parse_signal_name("STOP").unwrap(), Signal::SIGSTOP);
1554 assert_eq!(parse_signal_name("CONT").unwrap(), Signal::SIGCONT);
1555 assert_eq!(parse_signal_name("USR1").unwrap(), Signal::SIGUSR1);
1556 assert_eq!(parse_signal_name("USR2").unwrap(), Signal::SIGUSR2);
1557 }
1558
1559 #[cfg(unix)]
1560 #[test]
1561 fn test_parse_signal_name_invalid() {
1562 assert!(parse_signal_name("INVALID").is_err());
1563 assert!(parse_signal_name("FOO").is_err());
1564 assert!(parse_signal_name("").is_err());
1565 }
1566
1567 #[cfg(unix)]
1568 #[test]
1569 fn test_parse_signal_name_case_insensitive() {
1570 use nix::sys::signal::Signal;
1571
1572 assert_eq!(parse_signal_name("term").unwrap(), Signal::SIGTERM);
1573 assert_eq!(parse_signal_name("Term").unwrap(), Signal::SIGTERM);
1574 assert_eq!(parse_signal_name("TERM").unwrap(), Signal::SIGTERM);
1575 assert_eq!(parse_signal_name("sigterm").unwrap(), Signal::SIGTERM);
1576 assert_eq!(parse_signal_name("SigTerm").unwrap(), Signal::SIGTERM);
1577 }
1578
1579 #[cfg(unix)]
1580 #[test]
1581 fn test_send_signal_nonexistent_process() {
1582 use nix::sys::signal::Signal;
1583
1584 let proc = Process {
1586 pid: 99999999,
1587 name: "ghost".to_string(),
1588 exe_path: None,
1589 cwd: None,
1590 command: None,
1591 argv0: None,
1592 raw: None,
1593 cpu_percent: 0.0,
1594 memory_mb: 0.0,
1595 status: ProcessStatus::Running,
1596 user: None,
1597 parent_pid: None,
1598 start_time: None,
1599 uptime_secs: None,
1600 };
1601
1602 let result = proc.send_signal(Signal::SIGCONT);
1603 assert!(result.is_err());
1604 }
1605
1606 fn ghost() -> Process {
1608 Process {
1609 pid: 99999999,
1610 name: "ghost".to_string(),
1611 exe_path: None,
1612 cwd: None,
1613 command: None,
1614 argv0: None,
1615 raw: None,
1616 cpu_percent: 0.0,
1617 memory_mb: 0.0,
1618 status: ProcessStatus::Running,
1619 user: None,
1620 parent_pid: None,
1621 start_time: None,
1622 uptime_secs: None,
1623 }
1624 }
1625
1626 #[test]
1627 fn every_sysinfo_status_maps_to_ours() {
1628 for (sys, ours) in [
1629 (SysProcessStatus::Run, ProcessStatus::Running),
1630 (SysProcessStatus::Sleep, ProcessStatus::Sleeping),
1631 (SysProcessStatus::Stop, ProcessStatus::Stopped),
1632 (SysProcessStatus::Zombie, ProcessStatus::Zombie),
1633 (SysProcessStatus::Dead, ProcessStatus::Dead),
1634 (SysProcessStatus::Idle, ProcessStatus::Unknown),
1635 ] {
1636 assert_eq!(ProcessStatus::from(sys), ours, "{sys:?}");
1637 }
1638 }
1639
1640 #[test]
1641 fn a_process_that_is_gone_does_not_look_stuck() {
1642 assert!(!ghost().looks_stuck_now());
1643 }
1644
1645 #[test]
1646 fn killing_a_process_that_is_already_gone_is_success() {
1647 assert!(matches!(ghost().kill(), Err(ProcError::ProcessNotFound(_))));
1648 ghost().kill_and_wait().unwrap();
1649 }
1650
1651 #[test]
1652 fn a_kill_that_was_not_delivered_is_an_error() {
1653 kill_outcome(true, 7).unwrap();
1654 let err = kill_outcome(false, 7).unwrap_err();
1655 assert!(
1656 matches!(&err, ProcError::SignalError(m) if m == "Failed to kill process 7"),
1657 "{err:?}"
1658 );
1659 }
1660
1661 #[test]
1662 fn taskkill_without_force_reports_what_it_said() {
1663 #[cfg(unix)]
1664 use std::os::unix::process::ExitStatusExt;
1665 #[cfg(windows)]
1666 use std::os::windows::process::ExitStatusExt;
1667 use std::process::{ExitStatus, Output};
1668
1669 let output = |raw, stdout: &str, stderr: &str| Output {
1670 status: ExitStatus::from_raw(raw),
1671 stdout: stdout.as_bytes().to_vec(),
1672 stderr: stderr.as_bytes().to_vec(),
1673 };
1674 taskkill_outcome(&output(0, "SUCCESS: sent termination signal", "")).unwrap();
1675
1676 #[cfg(unix)]
1678 let failed = 1 << 8;
1679 #[cfg(windows)]
1680 let failed = 1;
1681 let err =
1682 taskkill_outcome(&output(failed, "", "ERROR: could not be terminated")).unwrap_err();
1683 assert!(
1684 matches!(&err, ProcError::SignalError(m) if m == "ERROR: could not be terminated"),
1685 "{err:?}"
1686 );
1687 }
1688
1689 #[cfg(unix)]
1690 #[test]
1691 fn orphans_are_the_children_of_init_that_are_not_system_daemons() {
1692 let proc = |pid: u32, ppid: Option<u32>, exe: Option<&str>, cwd: Option<&str>| Process {
1693 pid,
1694 name: format!("p{pid}"),
1695 exe_path: exe.map(String::from),
1696 cwd: cwd.map(String::from),
1697 parent_pid: ppid,
1698 ..ghost()
1699 };
1700 let orphans = Process::orphans_among(vec![
1701 proc(10, Some(1), Some("/home/z/app"), Some("/home/z")),
1703 proc(11, Some(1), Some("/usr/sbin/sshd"), Some("/")),
1705 proc(1, Some(1), Some("/sbin/init"), Some("/home/z")),
1707 proc(12, Some(500), Some("/home/z/app"), Some("/home/z")),
1708 proc(13, None, Some("/home/z/app"), Some("/home/z")),
1710 ]);
1711 let pids: Vec<u32> = orphans.iter().map(|p| p.pid).collect();
1712 assert_eq!(pids, vec![10]);
1713 }
1714
1715 #[cfg(unix)]
1716 #[test]
1717 fn a_missing_lsof_finds_no_open_file_owners() {
1718 let found = Process::find_by_open_file_with("proc-no-such-lsof", std::path::Path::new("/"));
1719 assert!(found.unwrap().is_empty());
1720 }
1721
1722 #[test]
1723 fn a_wait_for_a_process_that_stays_is_a_wait_and_then_an_error() {
1724 let mut child = crate::test_support::idle_child();
1725 let record = crate::test_support::record_of(&child);
1726 let started = std::time::Instant::now();
1727 let result = record.kill_then_wait(Duration::from_millis(300), |_| Ok(()));
1730 let waited = started.elapsed();
1731 let still_there = child.try_wait().unwrap().is_none();
1732 crate::test_support::end(&mut child);
1733 assert!(
1734 matches!(&result, Err(ProcError::SignalError(_))),
1735 "{result:?}"
1736 );
1737 assert!(waited >= Duration::from_millis(300), "{waited:?}");
1738 assert!(still_there);
1739 }
1740
1741 #[test]
1742 fn a_process_still_alive_when_the_wait_ends_is_an_error() {
1743 let mut child = crate::test_support::idle_child();
1744 let record = crate::test_support::record_of(&child);
1745 let result = record.kill_and_wait_within(Duration::ZERO);
1747 let _ = child.kill();
1748 let _ = child.wait();
1749 assert!(
1750 matches!(&result, Err(ProcError::SignalError(m))
1751 if m == &format!("Process {} is still running 0 seconds after SIGKILL", record.pid)),
1752 "{result:?}"
1753 );
1754 }
1755
1756 #[cfg(target_os = "macos")]
1758 fn kinfo(pid: u32, stat: u8, comm: &str, ruid: u32, ppid: u32, start: u64) -> Vec<u8> {
1759 let mut rec = vec![0u8; KINFO_PROC_SIZE];
1760 rec[0..8].copy_from_slice(&start.to_ne_bytes());
1761 rec[36] = stat;
1762 rec[40..44].copy_from_slice(&pid.to_ne_bytes());
1763 rec[243..243 + comm.len()].copy_from_slice(comm.as_bytes());
1764 rec[392..396].copy_from_slice(&ruid.to_ne_bytes());
1765 rec[560..564].copy_from_slice(&ppid.to_ne_bytes());
1766 rec
1767 }
1768
1769 #[cfg(target_os = "macos")]
1770 #[test]
1771 fn zombies_are_read_from_the_kernels_table() {
1772 let known = std::collections::HashSet::from([30]);
1773 let table: Vec<u8> = [
1774 kinfo(10, 5, "defunct-app", 0, 99, 1_700_000_000),
1775 kinfo(11, 5, "", 0, 1, 0),
1777 kinfo(20, 2, "alive", 0, 1, 1_700_000_000),
1779 kinfo(30, 5, "known", 0, 1, 1_700_000_000),
1781 kinfo(0, 5, "kernel", 0, 0, 0),
1783 ]
1784 .concat();
1785
1786 let zombies = zombies_in(&table, &known);
1787 let pids: Vec<u32> = zombies.iter().map(|z| z.pid).collect();
1788 assert_eq!(pids, vec![10, 11]);
1789
1790 let named = &zombies[0];
1791 assert_eq!(named.name, "defunct-app");
1792 assert_eq!(named.status, ProcessStatus::Zombie);
1793 assert_eq!(named.parent_pid, Some(99));
1794 assert_eq!(named.start_time, Some(1_700_000_000));
1795 assert!(named.uptime_secs.unwrap() > 0);
1796 assert_eq!(named.user.as_deref(), Some("root"));
1797
1798 let unnamed = &zombies[1];
1799 assert_eq!(unnamed.name, "<defunct>");
1800 assert_eq!(unnamed.start_time, None);
1801 assert_eq!(unnamed.uptime_secs, None);
1802 }
1803
1804 #[cfg(target_os = "macos")]
1805 #[test]
1806 fn a_table_of_an_unknown_layout_shows_no_zombies() {
1807 let known = std::collections::HashSet::new();
1808 let mut table = kinfo(10, 5, "z", 0, 1, 0);
1809 table.push(0);
1810 assert!(zombies_in(&table, &known).is_empty());
1811 }
1812
1813 #[cfg(target_os = "macos")]
1814 #[test]
1815 fn a_refused_sysctl_gives_an_empty_table() {
1816 use std::cell::Cell;
1817
1818 assert!(read_proc_table(|_, _| -1).is_empty());
1820
1821 let calls = Cell::new(0);
1823 let table = read_proc_table(|_, size| {
1824 calls.set(calls.get() + 1);
1825 if calls.get() == 1 {
1826 *size = 2 * KINFO_PROC_SIZE;
1827 0
1828 } else {
1829 -1
1830 }
1831 });
1832 assert_eq!(calls.get(), 2);
1833 assert!(table.is_empty());
1834
1835 let calls = Cell::new(0);
1837 let table = read_proc_table(|buf, size| {
1838 calls.set(calls.get() + 1);
1839 if calls.get() == 1 {
1840 *size = 2 * KINFO_PROC_SIZE;
1841 } else {
1842 assert_eq!(*size, 66 * KINFO_PROC_SIZE, "room for 64 newcomers");
1843 unsafe { buf.cast::<u8>().write(7) };
1845 *size = KINFO_PROC_SIZE;
1846 }
1847 0
1848 });
1849 assert_eq!(table.len(), KINFO_PROC_SIZE);
1850 assert_eq!(table[0], 7);
1851 }
1852}