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 pub cwd: Option<String>,
391}
392
393pub(crate) fn classify_sysinfo(pattern: &str, proc: &sysinfo::Process, mode: NameMatch) -> Hit {
397 let name = proc.name().to_string_lossy();
398 let exe = proc.exe().map(|exe| exe.to_string_lossy());
399 let argv0 = proc.cmd().first().map(|arg| arg.to_string_lossy());
400 let command = (!proc.cmd().is_empty()).then(|| {
402 proc.cmd()
403 .iter()
404 .map(|arg| arg.to_string_lossy())
405 .collect::<Vec<_>>()
406 .join(" ")
407 });
408 classify(
409 pattern,
410 &Names {
411 name: &name,
412 exe: exe.as_deref(),
413 argv0: argv0.as_deref(),
414 command: command.as_deref(),
415 },
416 mode,
417 )
418}
419
420impl Process {
421 pub fn names(&self) -> Names<'_> {
423 match &self.raw {
424 Some(raw) => Names {
425 name: &raw.name,
426 exe: raw.exe_path.as_deref(),
427 argv0: self.argv0.as_deref(),
428 command: raw.command.as_deref(),
429 },
430 None => Names {
431 name: &self.name,
432 exe: self.exe_path.as_deref(),
433 argv0: self.argv0.as_deref(),
434 command: self.command.as_deref(),
435 },
436 }
437 }
438
439 pub fn raw_cwd(&self) -> Option<&str> {
444 match &self.raw {
445 Some(raw) => raw.cwd.as_deref(),
446 None => self.cwd.as_deref(),
447 }
448 }
449
450 pub fn find_by_name(pattern: &str, mode: NameMatch) -> Result<Vec<Process>> {
457 let mut sys = System::new_all();
458 sys.refresh_all();
459
460 let ancestor_pids = self_and_ancestors(&sys);
463 let mut in_command_line_only = 0;
464 let mut processes: Vec<Process> = sys
465 .processes()
466 .iter()
467 .filter_map(|(pid, proc)| {
468 if ancestor_pids.contains(pid) || is_thread(proc) {
470 return None;
471 }
472 match classify_sysinfo(pattern, proc, mode) {
473 Hit::Match => Some(Process::from_sysinfo(*pid, proc)),
474 Hit::CommandLineOnly => {
475 in_command_line_only += 1;
476 None
477 }
478 Hit::No => None,
479 }
480 })
481 .collect();
482 processes.sort_by_key(|p| p.pid);
485
486 if processes.is_empty() {
487 return Err(ProcError::name_not_found(pattern, in_command_line_only));
488 }
489
490 Ok(processes)
491 }
492
493 pub fn find_by_pid(pid: u32) -> Result<Option<Process>> {
495 let mut sys = System::new_all();
496 sys.refresh_all();
497
498 let sysinfo_pid = Pid::from_u32(pid);
499
500 Ok(sys
501 .processes()
502 .get(&sysinfo_pid)
503 .map(|proc| Process::from_sysinfo(sysinfo_pid, proc)))
504 }
505
506 pub fn find_all() -> Result<Vec<Process>> {
508 let mut sys = System::new_all();
509 sys.refresh_all();
510
511 let self_pid = sysinfo::Pid::from_u32(std::process::id());
512 #[allow(unused_mut)]
513 let mut processes: Vec<Process> = sys
514 .processes()
515 .iter()
516 .filter(|(pid, proc)| **pid != self_pid && !is_thread(proc))
517 .map(|(pid, proc)| Process::from_sysinfo(*pid, proc))
518 .collect();
519
520 #[cfg(target_os = "macos")]
521 {
522 let known: std::collections::HashSet<u32> = processes.iter().map(|p| p.pid).collect();
523 processes.extend(macos_zombies(&known));
524 }
525
526 processes.sort_by_key(|p| p.pid);
530
531 Ok(processes)
532 }
533
534 pub fn find_by_exe_path(path: &std::path::Path) -> Result<Vec<Process>> {
536 let all = Self::find_all()?;
537 let path_str = path.to_string_lossy();
538
539 Ok(all
540 .into_iter()
541 .filter(|p| {
542 if let Some(ref exe) = p.exe_path {
543 exe == &*path_str || std::path::Path::new(exe) == path
545 } else {
546 false
547 }
548 })
549 .collect())
550 }
551
552 #[cfg(unix)]
554 pub fn find_by_open_file(path: &std::path::Path) -> Result<Vec<Process>> {
555 Self::find_by_open_file_with("lsof", path)
556 }
557
558 #[cfg(unix)]
560 fn find_by_open_file_with(lsof: &str, path: &std::path::Path) -> Result<Vec<Process>> {
561 use std::process::Command;
562
563 let output = crate::ui::interrupt::unblocked_in_child(&mut Command::new(lsof))
564 .args(["-t", &path.to_string_lossy()]) .output();
566
567 let output = match output {
568 Ok(o) => o,
569 Err(_) => return Ok(vec![]), };
571
572 if !output.status.success() {
573 return Ok(vec![]); }
575
576 let pids: Vec<u32> = String::from_utf8_lossy(&output.stdout)
577 .lines()
578 .filter_map(|line| line.trim().parse().ok())
579 .collect();
580
581 let mut processes = Vec::new();
582 for pid in pids {
583 if let Ok(Some(proc)) = Self::find_by_pid(pid) {
584 processes.push(proc);
585 }
586 }
587
588 Ok(processes)
589 }
590
591 #[cfg(not(unix))]
593 pub fn find_by_open_file(_path: &std::path::Path) -> Result<Vec<Process>> {
594 Ok(vec![])
596 }
597
598 pub fn find_stuck(timeout: Duration) -> Result<Vec<Process>> {
604 let timeout_secs = timeout.as_secs();
605 let mut read_at = std::time::Instant::now();
607 let mut sys = System::new_all();
608 let mut used = cpu_used(&sys);
609 let mut cpu: std::collections::HashMap<Pid, f32> = std::collections::HashMap::new();
610 let mut stuck: Option<std::collections::HashSet<Pid>> = None;
611
612 for _ in 0..STUCK_READINGS {
613 std::thread::sleep(STUCK_READING_GAP);
614 let started = std::time::Instant::now();
615 sys.refresh_processes(sysinfo::ProcessesToUpdate::All, true);
616 let wall = started - read_at;
617 read_at = started;
618 cpu = sys
620 .processes()
621 .iter()
622 .filter_map(|(pid, p)| {
623 let before = used.get(pid)?;
624 Some((
625 *pid,
626 cpu_percent_between(*before, p.accumulated_cpu_time(), wall),
627 ))
628 })
629 .collect();
630 used = cpu_used(&sys);
631 let now: std::collections::HashSet<Pid> = sys
632 .processes()
633 .iter()
634 .filter(|(pid, p)| {
635 let busy = cpu.get(pid).copied().unwrap_or(0.0);
636 counts_as_stuck(
637 is_thread(p),
638 p.run_time(),
639 timeout_secs,
640 looks_stuck(busy, p.status()),
641 )
642 })
643 .map(|(pid, _)| *pid)
644 .collect();
645 stuck = Some(match stuck {
646 None => now,
647 Some(before) => before.intersection(&now).copied().collect(),
648 });
649 }
650
651 let stuck = stuck.unwrap_or_default();
652 let mut found: Vec<Process> = sys
653 .processes()
654 .iter()
655 .filter(|(pid, _)| stuck.contains(pid))
656 .map(|(pid, p)| {
657 let mut process = Process::from_sysinfo(*pid, p);
658 if let Some(&busy) = cpu.get(pid) {
660 process.cpu_percent = busy;
661 }
662 process
663 })
664 .collect();
665 found.sort_by_key(|p| p.pid);
667 Ok(found)
668 }
669
670 pub fn looks_stuck_now(&self) -> bool {
673 let pid = Pid::from_u32(self.pid);
674 let mut sys = System::new();
675 let mut read_at = std::time::Instant::now();
676 sys.refresh_processes(sysinfo::ProcessesToUpdate::Some(&[pid]), true);
677 let mut used = sys
678 .process(pid)
679 .filter(|p| self.is_same(p))
680 .map(|p| p.accumulated_cpu_time());
681 (0..STUCK_READINGS).all(|_| {
682 std::thread::sleep(STUCK_READING_GAP);
683 let started = std::time::Instant::now();
684 sys.refresh_processes(sysinfo::ProcessesToUpdate::Some(&[pid]), true);
685 let wall = started - read_at;
686 read_at = started;
687 let (Some(before), Some(p)) = (used, sys.process(pid).filter(|p| self.is_same(p)))
688 else {
689 return false;
690 };
691 let after = p.accumulated_cpu_time();
692 used = Some(after);
693 looks_stuck(cpu_percent_between(before, after, wall), p.status())
694 })
695 }
696
697 #[cfg(unix)]
704 pub fn cpu_over(&self, window: Duration) -> Option<f32> {
705 let pid = Pid::from_u32(self.pid);
706 let mut sys = System::new();
707 let started = std::time::Instant::now();
708 sys.refresh_processes(sysinfo::ProcessesToUpdate::Some(&[pid]), true);
709 let before = sys
710 .process(pid)
711 .filter(|p| self.is_same(p))
712 .map(|p| p.accumulated_cpu_time())?;
713 std::thread::sleep(window.max(sysinfo::MINIMUM_CPU_UPDATE_INTERVAL));
714 let wall = started.elapsed();
715 sys.refresh_processes(sysinfo::ProcessesToUpdate::Some(&[pid]), true);
716 sys.process(pid)
717 .filter(|p| self.is_same(p))
718 .map(|p| cpu_percent_between(before, p.accumulated_cpu_time(), wall))
719 }
720
721 pub fn measure_cpu(processes: &mut [Process]) {
732 if processes.is_empty() {
733 return;
734 }
735 let pids: Vec<Pid> = processes.iter().map(|p| Pid::from_u32(p.pid)).collect();
736 let which = sysinfo::ProcessesToUpdate::Some(&pids);
737 let cpu_only = sysinfo::ProcessRefreshKind::nothing().with_cpu();
738 let mut sys = System::new();
739
740 let first = std::time::Instant::now();
741 sys.refresh_processes_specifics(which, true, cpu_only);
742 let before = cpu_used(&sys);
743 std::thread::sleep(sysinfo::MINIMUM_CPU_UPDATE_INTERVAL);
744 let wall = first.elapsed();
747 sys.refresh_processes_specifics(which, true, cpu_only);
748
749 for process in processes.iter_mut() {
750 let pid = Pid::from_u32(process.pid);
751 let (Some(current), Some(&cpu_before)) = (sys.process(pid), before.get(&pid)) else {
752 continue;
753 };
754 if process.is_same(current) {
755 process.cpu_percent =
756 cpu_percent_between(cpu_before, current.accumulated_cpu_time(), wall);
757 }
758 }
759 }
760
761 pub fn kill(&self) -> Result<()> {
763 let mut sys = System::new();
764 sys.refresh_processes(
765 sysinfo::ProcessesToUpdate::Some(&[Pid::from_u32(self.pid)]),
766 true,
767 );
768
769 match sys.process(Pid::from_u32(self.pid)) {
770 Some(proc) if self.is_same(proc) => kill_outcome(proc.kill(), self.pid),
771 _ => Err(ProcError::ProcessNotFound(self.pid.to_string())),
773 }
774 }
775
776 fn is_same(&self, current: &sysinfo::Process) -> bool {
779 match self.start_time {
780 Some(ours) if current.start_time() > 0 => current.start_time().abs_diff(ours) <= 1,
781 _ => true,
782 }
783 }
784
785 fn ensure_same_process(&self) -> Result<()> {
789 let mut sys = System::new();
790 let pid = Pid::from_u32(self.pid);
791 sys.refresh_processes(sysinfo::ProcessesToUpdate::Some(&[pid]), true);
792 match sys.process(pid) {
793 Some(current) if self.is_same(current) => Ok(()),
794 _ => Err(ProcError::ProcessNotFound(self.pid.to_string())),
795 }
796 }
797
798 pub fn kill_and_wait(&self) -> Result<()> {
804 self.kill_and_wait_within(Duration::from_secs(5))
805 }
806
807 fn kill_and_wait_within(&self, wait: Duration) -> Result<()> {
809 self.kill_then_wait(wait, Process::kill)
810 }
811
812 fn kill_then_wait(&self, wait: Duration, kill: impl Fn(&Process) -> Result<()>) -> Result<()> {
816 match kill(self) {
817 Err(ProcError::ProcessNotFound(_)) => return Ok(()),
819 other => other?,
820 }
821 let deadline = std::time::Instant::now() + wait;
822 while std::time::Instant::now() < deadline {
823 if !self.is_running() {
824 return Ok(());
825 }
826 std::thread::sleep(Duration::from_millis(50));
827 }
828 Err(ProcError::SignalError(format!(
829 "Process {} is still running {} seconds after SIGKILL",
830 self.pid,
831 wait.as_secs()
832 )))
833 }
834
835 #[cfg(unix)]
837 pub fn terminate(&self) -> Result<()> {
838 use nix::sys::signal::{kill, Signal};
839 use nix::unistd::Pid as NixPid;
840
841 self.ensure_same_process()?;
842 kill(NixPid::from_raw(self.pid as i32), Signal::SIGTERM)
843 .map_err(|e| ProcError::SignalError(e.to_string()))
844 }
845
846 #[cfg(windows)]
851 pub fn terminate(&self) -> Result<()> {
852 use std::process::Command;
853
854 self.ensure_same_process()?;
855 let output = Command::new("taskkill")
856 .args(["/PID", &self.pid.to_string()])
857 .output()
858 .map_err(|e| ProcError::SystemError(e.to_string()))?;
859
860 taskkill_outcome(&output)
861 }
862
863 #[cfg(unix)]
865 pub fn send_signal(&self, signal: nix::sys::signal::Signal) -> Result<()> {
866 use nix::sys::signal::kill;
867 use nix::unistd::Pid as NixPid;
868 self.ensure_same_process()?;
869 kill(NixPid::from_raw(self.pid as i32), signal)
870 .map_err(|e| ProcError::SignalError(format!("{}: {}", signal, e)))
871 }
872
873 #[cfg(unix)]
882 pub fn find_orphans() -> Result<Vec<Process>> {
883 Ok(Self::orphans_among(Self::find_all()?))
884 }
885
886 #[cfg(not(unix))]
888 pub fn find_orphans() -> Result<Vec<Process>> {
889 Err(orphans_not_supported())
890 }
891
892 #[cfg(unix)]
894 fn orphans_among(all: Vec<Process>) -> Vec<Process> {
895 all.into_iter()
896 .filter(|p| {
897 if let Some(ppid) = p.parent_pid {
898 ppid == 1 && p.pid != 1 && !Self::is_system_process(p)
899 } else {
900 false
901 }
902 })
903 .collect()
904 }
905
906 #[cfg(unix)]
912 fn is_system_process(p: &Process) -> bool {
913 if p.cwd.is_none() || p.cwd.as_deref() == Some("/") {
914 if let Some(ref exe) = p.exe_path {
915 if exe.starts_with("/System/") || exe.starts_with("/usr/libexec/") {
917 return true;
918 }
919 if exe.starts_with("/usr/sbin/")
921 || exe.starts_with("/sbin/")
922 || exe.starts_with("/usr/bin/")
923 || exe.starts_with("/usr/lib/")
924 || exe.starts_with("/usr/lib64/")
925 || exe.starts_with("/lib/")
926 || exe.starts_with("/lib64/")
927 || exe.starts_with("/opt/")
928 || exe.starts_with("/snap/")
929 {
930 return true;
931 }
932 }
933 return true; }
935 false
936 }
937
938 pub fn exists(&self) -> bool {
942 let mut sys = System::new();
943 let pid = Pid::from_u32(self.pid);
944 sys.refresh_processes(sysinfo::ProcessesToUpdate::Some(&[pid]), true);
945 match sys.process(pid) {
946 None => false,
947 Some(current) if current.status() == SysProcessStatus::Zombie => false,
948 Some(current) => self.is_same(current),
949 }
950 }
951
952 pub fn is_running(&self) -> bool {
954 self.exists()
955 }
956
957 pub(crate) fn from_sysinfo(pid: Pid, proc: &sysinfo::Process) -> Self {
959 let cmd_vec = proc.cmd();
960 let raw_command = (!cmd_vec.is_empty()).then(|| {
964 cmd_vec
965 .iter()
966 .map(|s| s.to_string_lossy())
967 .collect::<Vec<_>>()
968 .join(" ")
969 });
970 let raw_name = proc.name().to_string_lossy();
971 let raw_exe = proc.exe().map(|p| p.to_string_lossy());
972 let raw_cwd = proc
973 .cwd()
974 .map(|p| strip_deleted_marker(p.to_string_lossy().to_string()));
975 let changed = |text: &str| matches!(escape_control(text), std::borrow::Cow::Owned(_));
976 let raw = (changed(&raw_name)
977 || raw_exe.as_deref().is_some_and(changed)
978 || raw_command.as_deref().is_some_and(changed)
979 || raw_cwd.as_deref().is_some_and(changed))
980 .then(|| {
981 Box::new(RawText {
982 name: raw_name.to_string(),
983 exe_path: raw_exe.as_ref().map(|exe| exe.to_string()),
984 command: raw_command.clone(),
985 cwd: raw_cwd.clone(),
986 })
987 });
988 let command = raw_command
989 .as_deref()
990 .map(|joined| escape_control(joined).into_owned());
991
992 let exe_path = raw_exe
993 .as_deref()
994 .map(|exe| escape_control(exe).into_owned());
995 let cwd = raw_cwd
996 .as_deref()
997 .map(|path| escape_control(path).into_owned());
998
999 Process {
1000 pid: pid.as_u32(),
1001 name: escape_control(&raw_name).into_owned(),
1002 exe_path,
1003 cwd,
1004 command,
1005 argv0: cmd_vec
1006 .first()
1007 .map(|arg| arg.to_string_lossy().into_owned()),
1008 raw,
1009 cpu_percent: proc.cpu_usage(),
1010 memory_mb: proc.memory() as f64 / 1024.0 / 1024.0,
1011 status: ProcessStatus::from(proc.status()),
1012 user: proc
1013 .user_id()
1014 .map(|uid| escape_control(&user_name(uid)).into_owned()),
1015 parent_pid: proc.parent().map(|p| p.as_u32()),
1016 start_time: (proc.start_time() > 0).then(|| proc.start_time()),
1019 uptime_secs: (proc.start_time() > 0).then(|| proc.run_time()),
1020 }
1021 }
1022}
1023
1024#[cfg(unix)]
1029pub fn parse_signal_name(name: &str) -> Result<nix::sys::signal::Signal> {
1030 use nix::sys::signal::Signal;
1031
1032 let upper = name.to_uppercase();
1033 let upper = upper.trim_start_matches("SIG");
1034 match upper {
1035 "HUP" => Ok(Signal::SIGHUP),
1036 "INT" => Ok(Signal::SIGINT),
1037 "QUIT" => Ok(Signal::SIGQUIT),
1038 "ABRT" => Ok(Signal::SIGABRT),
1039 "KILL" => Ok(Signal::SIGKILL),
1040 "TERM" => Ok(Signal::SIGTERM),
1041 "STOP" => Ok(Signal::SIGSTOP),
1042 "CONT" => Ok(Signal::SIGCONT),
1043 "USR1" => Ok(Signal::SIGUSR1),
1044 "USR2" => Ok(Signal::SIGUSR2),
1045 _ => Err(ProcError::InvalidInput(format!(
1046 "Unknown signal: '{}'. Valid signals: HUP, INT, QUIT, ABRT, KILL, TERM, STOP, CONT, USR1, USR2",
1047 name
1048 ))),
1049 }
1050}
1051
1052#[cfg(test)]
1053mod tests {
1054 use super::*;
1055
1056 #[test]
1057 fn cpu_is_the_cpu_time_used_over_the_wall_time() {
1058 let second = Duration::from_secs(1);
1059 assert_eq!(cpu_percent_between(5_000, 6_000, second), 100.0);
1061 assert_eq!(cpu_percent_between(5_000, 5_500, second), 50.0);
1062 assert_eq!(cpu_percent_between(5_000, 5_000, second), 0.0);
1063 assert_eq!(cpu_percent_between(0, 2_000, second), 200.0);
1065 assert_eq!(cpu_percent_between(6_000, 5_000, second), 0.0);
1067 assert_eq!(cpu_percent_between(0, 1_000, Duration::ZERO), 0.0);
1068 }
1069
1070 #[test]
1071 fn a_failed_command_is_explained_in_its_own_words() {
1072 let reason = "ERROR: The process with PID 7 could not be terminated.";
1073 assert_eq!(
1074 command_failure(b"", reason.as_bytes(), "exit code: 1"),
1075 reason
1076 );
1077 assert_eq!(
1079 command_failure(b" not here\r\n", b"", "exit code: 1"),
1080 "not here"
1081 );
1082 assert_eq!(
1084 command_failure(
1085 b"",
1086 b"ERROR: first part\r\n second part.\r\n\r\n",
1087 "exit code: 1"
1088 ),
1089 "ERROR: first part second part."
1090 );
1091 assert_eq!(
1092 command_failure(b"", b" \n", "exit code: 128"),
1093 "exited with exit code: 128"
1094 );
1095 }
1096
1097 #[test]
1098 fn busy_means_over_half_a_core_or_blocked_in_the_kernel() {
1099 assert!(looks_stuck(50.1, SysProcessStatus::Run));
1100 assert!(!looks_stuck(50.0, SysProcessStatus::Run));
1101 assert!(!looks_stuck(0.0, SysProcessStatus::Sleep));
1102 assert!(looks_stuck(0.0, SysProcessStatus::UninterruptibleDiskSleep));
1103 }
1104
1105 #[test]
1106 fn a_reading_counts_as_stuck_from_the_timeout_on() {
1107 assert!(counts_as_stuck(false, 0, 0, true));
1108 assert!(!counts_as_stuck(false, 59, 60, true));
1109 assert!(counts_as_stuck(false, 60, 60, true));
1110 assert!(counts_as_stuck(false, 61, 60, true));
1111 assert!(!counts_as_stuck(false, 60, 60, false));
1113 assert!(!counts_as_stuck(true, 60, 60, true));
1114 }
1115
1116 #[test]
1117 fn a_process_that_ran_the_timeout_has_run_long_enough() {
1118 assert!(ran_long_enough(0, 0));
1120 assert!(ran_long_enough(5, 5));
1121 assert!(ran_long_enough(6, 5));
1122 assert!(!ran_long_enough(4, 5));
1123 }
1124
1125 #[test]
1126 fn test_find_all_processes() {
1127 let processes = Process::find_all().unwrap();
1128 assert!(!processes.is_empty(), "Should find at least one process");
1129 }
1130
1131 #[test]
1132 fn every_process_source_lists_pid_ascending() {
1133 use crate::test_support::{end, idle_child, record_of};
1134
1135 let mut children: Vec<_> = (0..3).map(|_| idle_child()).collect();
1136 let mine: Vec<u32> = children.iter().map(|c| record_of(c).pid).collect();
1137 let name = record_of(&children[0]).name;
1138
1139 let ascending = |what: &str, list: &[Process]| {
1140 let pids: Vec<u32> = list.iter().map(|p| p.pid).collect();
1141 let mut sorted = pids.clone();
1142 sorted.sort_unstable();
1143 sorted.dedup();
1144 assert_eq!(pids, sorted, "{what} is not PID ascending");
1145 pids
1146 };
1147 let all = ascending("find_all", &Process::find_all().unwrap());
1148 let named = ascending(
1149 "find_by_name",
1150 &Process::find_by_name(&name, NameMatch::Exact).unwrap(),
1151 );
1152 ascending("find_stuck", &Process::find_stuck(Duration::ZERO).unwrap());
1153 for pid in &mine {
1154 assert!(all.contains(pid), "find_all has {pid}");
1155 assert!(named.contains(pid), "find_by_name has {pid}");
1156 }
1157 children.iter_mut().for_each(end);
1158 }
1159
1160 #[test]
1161 fn test_find_by_pid_self() {
1162 let pid = std::process::id();
1163 let process = Process::find_by_pid(pid).unwrap();
1164 assert!(process.is_some(), "Should find own process");
1165 }
1166
1167 #[test]
1168 fn test_is_running_tells_a_reused_pid_apart() {
1169 let mut me = Process::find_by_pid(std::process::id()).unwrap().unwrap();
1170 assert!(me.is_running());
1171 let start = me.start_time.expect("a running process has a start time");
1173 me.start_time = Some(start - 3600);
1174 assert!(!me.is_running(), "a reused PID counted as the same process");
1175 }
1176
1177 #[test]
1178 fn test_escape_control_makes_terminal_input_harmless() {
1179 assert_eq!(escape_control("node server.js"), "node server.js");
1181 assert_eq!(escape_control("café ✓ 日本"), "café ✓ 日本");
1182 assert_eq!(escape_control("a\x1b[31mb"), "a\\x1b[31mb");
1184 assert_eq!(escape_control("\x1b]0;pwned\x07"), "\\x1b]0;pwned\\x07");
1185 assert_eq!(escape_control("one\ntwo\tthree\r"), "one\\ntwo\\tthree\\r");
1187 assert_eq!(escape_control("x\x7fy\u{9b}2J"), "x\\x7fy\\x9b2J");
1189 assert_eq!(escape_control("ab\u{202e}cd"), "ab\\u{202e}cd");
1191 }
1192
1193 #[cfg(unix)]
1194 #[test]
1195 fn test_looks_stuck_needs_sustained_cpu() {
1196 let spawn = |script: &str| {
1197 std::process::Command::new("sh")
1198 .args(["-c", script])
1199 .spawn()
1200 .unwrap()
1201 };
1202 let mut spinning = spawn("while :; do :; done");
1203 let mut idle = spawn("exec sleep 300");
1204 std::thread::sleep(Duration::from_millis(300));
1205 let record = |pid: u32| Process::find_by_pid(pid).unwrap().unwrap();
1206
1207 let spinning_stuck = record(spinning.id()).looks_stuck_now();
1208 let idle_stuck = record(idle.id()).looks_stuck_now();
1209 for child in [&mut spinning, &mut idle] {
1210 let _ = child.kill();
1211 let _ = child.wait();
1212 }
1213 assert!(
1214 spinning_stuck,
1215 "a process spinning at 100% CPU wasn't stuck"
1216 );
1217 assert!(!idle_stuck, "an idle process counted as stuck");
1218 }
1219
1220 #[test]
1221 fn test_signals_never_reach_a_process_that_reused_the_pid() {
1222 let mut child = crate::test_support::idle_child();
1223 let mut stale = crate::test_support::record_of(&child);
1224 stale.start_time = stale.start_time.map(|t| t - 3600);
1226
1227 let kill = stale.kill();
1228 let term = stale.terminate();
1229 #[cfg(unix)]
1230 let sig = stale.send_signal(nix::sys::signal::Signal::SIGKILL);
1231 let alive = child.try_wait().unwrap().is_none();
1232 let _ = child.kill();
1233 let _ = child.wait();
1234
1235 assert!(
1236 alive,
1237 "a stale record signalled the process now using its PID"
1238 );
1239 assert!(
1240 matches!(kill, Err(ProcError::ProcessNotFound(_))),
1241 "{kill:?}"
1242 );
1243 assert!(
1244 matches!(term, Err(ProcError::ProcessNotFound(_))),
1245 "{term:?}"
1246 );
1247 #[cfg(unix)]
1248 assert!(matches!(sig, Err(ProcError::ProcessNotFound(_))), "{sig:?}");
1249 }
1250
1251 #[cfg(unix)]
1255 #[test]
1256 fn text_with_control_characters_matches_as_typed_and_displays_escaped() {
1257 use crate::core::matches_by_filter;
1258 use std::os::unix::process::CommandExt;
1259
1260 let word = format!("zz\u{7}ctl{}x", std::process::id());
1261 let mut child = std::process::Command::new("sleep")
1262 .arg0(&word)
1263 .arg("300")
1264 .spawn()
1265 .unwrap();
1266 let pid = child.id();
1267 let record = std::iter::repeat_with(|| {
1272 std::thread::sleep(Duration::from_millis(5));
1273 Process::find_by_pid(pid).unwrap().unwrap()
1274 })
1275 .take(1000)
1276 .find(|record| record.argv0.is_some());
1277 let part = format!("\u{7}ctl{}", std::process::id());
1278
1279 let found = |pattern: &str, mode| {
1280 Process::find_by_name(pattern, mode)
1281 .map(|found| found.iter().any(|p| p.pid == pid))
1282 .unwrap_or(false)
1283 };
1284 let named = found(&word, NameMatch::Exact);
1285 let named_in_command = found(&part, NameMatch::Command);
1286 let _ = child.kill();
1287 let _ = child.wait();
1288
1289 let record = record.expect("the child's command line never appeared");
1290 assert!(named, "find_by_name read the escaped first argument");
1291 assert!(
1292 named_in_command,
1293 "find_by_name read the escaped command line"
1294 );
1295 assert!(
1296 matches_by_filter(&record, &word, NameMatch::Exact),
1297 "--by read the escaped first argument"
1298 );
1299 assert!(
1300 matches_by_filter(&record, &part, NameMatch::Command),
1301 "--by --cmd read the escaped command line"
1302 );
1303 let shown = record.command.as_deref().unwrap();
1305 assert!(shown.contains("\\x07ctl"), "{shown}");
1306 assert!(!shown.contains('\u{7}'), "{shown:?}");
1307 }
1308
1309 #[cfg(windows)]
1313 #[test]
1314 fn a_control_character_in_a_windows_command_line_matches_as_typed_and_displays_escaped() {
1315 use crate::core::matches_by_filter;
1316
1317 let mut exe = std::env::current_exe().unwrap();
1318 exe.pop();
1319 exe.pop();
1320 exe.push("examples");
1321 exe.push("listener.exe");
1322 assert!(exe.exists(), "build the example first: {}", exe.display());
1323 let part = format!("\u{7}ctl{}", std::process::id());
1324 let mut child = std::process::Command::new(&exe)
1325 .arg(&part)
1326 .stdin(std::process::Stdio::null())
1327 .stdout(std::process::Stdio::null())
1328 .spawn()
1329 .unwrap();
1330 let pid = child.id();
1331 let record = std::iter::repeat_with(|| {
1332 std::thread::sleep(Duration::from_millis(20));
1333 Process::find_by_pid(pid).unwrap()
1334 })
1335 .take(500)
1336 .flatten()
1337 .find(|record| record.command.is_some())
1338 .expect("the child's command line never appeared");
1339 let found = Process::find_by_name(&part, NameMatch::Command)
1340 .map(|found| found.iter().any(|p| p.pid == pid))
1341 .unwrap_or(false);
1342 let kept = matches_by_filter(&record, &part, NameMatch::Command);
1343 crate::test_support::end(&mut child);
1344
1345 assert!(found, "find_by_name read the escaped command line");
1346 assert!(kept, "--by --cmd read the escaped command line");
1347 let shown = record.command.as_deref().unwrap();
1348 assert!(shown.contains("\\x07ctl"), "{shown}");
1349 assert!(!shown.contains('\u{7}'), "{shown:?}");
1350 }
1351
1352 #[test]
1353 fn a_name_is_read_from_the_raw_text_when_escaping_changed_it() {
1354 let record = Process {
1355 name: "a\\x07b".to_string(),
1356 raw: Some(Box::new(RawText {
1357 name: "a\u{7}b".to_string(),
1358 exe_path: Some("c\u{7}d".to_string()),
1359 command: Some("e\u{7}f".to_string()),
1360 cwd: None,
1361 })),
1362 argv0: Some("g".to_string()),
1363 ..ghost()
1364 };
1365 let names = record.names();
1366 assert_eq!(names.name, "a\u{7}b");
1367 assert_eq!(names.exe, Some("c\u{7}d"));
1368 assert_eq!(names.argv0, Some("g"));
1369 assert_eq!(names.command, Some("e\u{7}f"));
1370 }
1371
1372 #[test]
1373 fn the_ancestors_stop_where_the_process_list_does() {
1374 let me = Pid::from_u32(std::process::id());
1377 let mut sys = System::new();
1378 sys.refresh_processes(sysinfo::ProcessesToUpdate::Some(&[me]), true);
1379 let parent = sys.process(me).and_then(|p| p.parent());
1380 let expected: std::collections::HashSet<Pid> = std::iter::once(me).chain(parent).collect();
1381 assert_eq!(self_and_ancestors(&sys), expected);
1382 }
1383
1384 #[test]
1385 fn test_strip_deleted_marker() {
1386 assert_eq!(
1387 strip_deleted_marker("/nonexistent/proc-gone (deleted)".to_string()),
1388 "/nonexistent/proc-gone"
1389 );
1390 assert_eq!(
1391 strip_deleted_marker("/tmp/project".to_string()),
1392 "/tmp/project"
1393 );
1394
1395 let real = std::env::temp_dir().join(format!("proc-real-{} (deleted)", std::process::id()));
1397 std::fs::create_dir_all(&real).unwrap();
1398 let path = real.to_string_lossy().to_string();
1399 assert_eq!(strip_deleted_marker(path.clone()), path);
1400 std::fs::remove_dir(&real).unwrap();
1401 }
1402
1403 #[test]
1404 fn a_name_nothing_has_is_plain_not_found_in_both_modes() {
1405 for mode in [NameMatch::Exact, NameMatch::Command] {
1406 let result = Process::find_by_name("nonexistent_process_12345", mode);
1407 assert!(
1408 matches!(&result, Err(ProcError::ProcessNotFound(n)) if n == "nonexistent_process_12345"),
1409 "{result:?}"
1410 );
1411 }
1412 }
1413
1414 #[cfg(unix)]
1418 fn waiting_shell_with(marker: &str) -> std::process::Child {
1419 std::process::Command::new("sh")
1420 .args(["-c", &format!("read line; : {marker}")])
1421 .stdin(std::process::Stdio::piped())
1422 .spawn()
1423 .unwrap()
1424 }
1425
1426 #[cfg(unix)]
1427 #[test]
1428 fn a_word_only_in_a_command_line_is_found_with_cmd_and_hinted_without() {
1429 let marker = format!("hintmarker{}", std::process::id());
1430 let mut child = waiting_shell_with(&marker);
1431 let pid = child.id();
1432 let exact = Process::find_by_name(&marker, NameMatch::Exact);
1433 let cmd = Process::find_by_name(&marker, NameMatch::Command);
1434 let _ = child.kill();
1435 let _ = child.wait();
1436
1437 assert!(
1440 matches!(&exact, Err(ProcError::NameNotFound { name, in_command_line })
1441 if name == &marker && *in_command_line >= 1),
1442 "{exact:?}"
1443 );
1444 let found = cmd.unwrap();
1446 assert_eq!(found.iter().map(|p| p.pid).collect::<Vec<_>>(), vec![pid]);
1447 }
1448
1449 #[cfg(unix)]
1450 #[test]
1451 fn test_find_orphans_returns_ok() {
1452 let result = Process::find_orphans();
1454 assert!(result.is_ok());
1455 }
1456
1457 #[cfg(not(unix))]
1458 #[test]
1459 fn find_orphans_is_not_supported_on_windows() {
1460 let result = Process::find_orphans();
1461 assert!(
1462 matches!(&result, Err(ProcError::NotSupported(m)) if m.starts_with("orphans is not supported on Windows")),
1463 "{result:?}"
1464 );
1465 }
1466
1467 #[cfg(unix)]
1468 #[test]
1469 fn test_is_system_process_system_paths() {
1470 let make_proc = |exe: Option<&str>, cwd: Option<&str>| Process {
1471 pid: 100,
1472 name: "test".to_string(),
1473 exe_path: exe.map(String::from),
1474 cwd: cwd.map(String::from),
1475 command: None,
1476 argv0: None,
1477 raw: None,
1478 cpu_percent: 0.0,
1479 memory_mb: 0.0,
1480 status: ProcessStatus::Running,
1481 user: None,
1482 parent_pid: Some(1),
1483 start_time: None,
1484 uptime_secs: None,
1485 };
1486
1487 assert!(Process::is_system_process(&make_proc(
1489 Some("/usr/sbin/sshd"),
1490 Some("/")
1491 )));
1492 assert!(Process::is_system_process(&make_proc(
1493 Some("/System/Library/foo"),
1494 Some("/")
1495 )));
1496 assert!(Process::is_system_process(&make_proc(
1497 Some("/usr/bin/systemd"),
1498 Some("/")
1499 )));
1500 assert!(Process::is_system_process(&make_proc(
1501 Some("/usr/lib/snapd/snapd"),
1502 Some("/")
1503 )));
1504
1505 assert!(Process::is_system_process(&make_proc(None, Some("/"))));
1507
1508 assert!(Process::is_system_process(&make_proc(
1510 Some("/usr/bin/foo"),
1511 None
1512 )));
1513
1514 for exe in [
1516 "/System/x",
1517 "/usr/libexec/x",
1518 "/usr/sbin/x",
1519 "/sbin/x",
1520 "/usr/bin/x",
1521 "/usr/lib/x",
1522 "/usr/lib64/x",
1523 "/lib/x",
1524 "/lib64/x",
1525 "/opt/x",
1526 "/snap/x",
1527 ] {
1528 assert!(
1529 Process::is_system_process(&make_proc(Some(exe), Some("/"))),
1530 "{exe}"
1531 );
1532 }
1533 assert!(Process::is_system_process(&make_proc(
1535 Some("/srv/x"),
1536 Some("/")
1537 )));
1538
1539 assert!(!Process::is_system_process(&make_proc(
1541 Some("/usr/bin/node"),
1542 Some("/home/user/project")
1543 )));
1544 assert!(!Process::is_system_process(&make_proc(
1545 Some("/home/user/.local/bin/app"),
1546 Some("/home/user")
1547 )));
1548 }
1549
1550 #[cfg(unix)]
1551 #[test]
1552 fn test_parse_signal_name_valid() {
1553 use nix::sys::signal::Signal;
1554
1555 assert_eq!(parse_signal_name("HUP").unwrap(), Signal::SIGHUP);
1556 assert_eq!(parse_signal_name("hup").unwrap(), Signal::SIGHUP);
1557 assert_eq!(parse_signal_name("SIGHUP").unwrap(), Signal::SIGHUP);
1558 assert_eq!(parse_signal_name("sighup").unwrap(), Signal::SIGHUP);
1559 assert_eq!(parse_signal_name("INT").unwrap(), Signal::SIGINT);
1560 assert_eq!(parse_signal_name("QUIT").unwrap(), Signal::SIGQUIT);
1561 assert_eq!(parse_signal_name("ABRT").unwrap(), Signal::SIGABRT);
1562 assert_eq!(parse_signal_name("KILL").unwrap(), Signal::SIGKILL);
1563 assert_eq!(parse_signal_name("TERM").unwrap(), Signal::SIGTERM);
1564 assert_eq!(parse_signal_name("STOP").unwrap(), Signal::SIGSTOP);
1565 assert_eq!(parse_signal_name("CONT").unwrap(), Signal::SIGCONT);
1566 assert_eq!(parse_signal_name("USR1").unwrap(), Signal::SIGUSR1);
1567 assert_eq!(parse_signal_name("USR2").unwrap(), Signal::SIGUSR2);
1568 }
1569
1570 #[cfg(unix)]
1571 #[test]
1572 fn test_parse_signal_name_invalid() {
1573 assert!(parse_signal_name("INVALID").is_err());
1574 assert!(parse_signal_name("FOO").is_err());
1575 assert!(parse_signal_name("").is_err());
1576 }
1577
1578 #[cfg(unix)]
1579 #[test]
1580 fn test_parse_signal_name_case_insensitive() {
1581 use nix::sys::signal::Signal;
1582
1583 assert_eq!(parse_signal_name("term").unwrap(), Signal::SIGTERM);
1584 assert_eq!(parse_signal_name("Term").unwrap(), Signal::SIGTERM);
1585 assert_eq!(parse_signal_name("TERM").unwrap(), Signal::SIGTERM);
1586 assert_eq!(parse_signal_name("sigterm").unwrap(), Signal::SIGTERM);
1587 assert_eq!(parse_signal_name("SigTerm").unwrap(), Signal::SIGTERM);
1588 }
1589
1590 #[cfg(unix)]
1591 #[test]
1592 fn test_send_signal_nonexistent_process() {
1593 use nix::sys::signal::Signal;
1594
1595 let proc = Process {
1597 pid: 99999999,
1598 name: "ghost".to_string(),
1599 exe_path: None,
1600 cwd: None,
1601 command: None,
1602 argv0: None,
1603 raw: None,
1604 cpu_percent: 0.0,
1605 memory_mb: 0.0,
1606 status: ProcessStatus::Running,
1607 user: None,
1608 parent_pid: None,
1609 start_time: None,
1610 uptime_secs: None,
1611 };
1612
1613 let result = proc.send_signal(Signal::SIGCONT);
1614 assert!(result.is_err());
1615 }
1616
1617 fn ghost() -> Process {
1619 Process {
1620 pid: 99999999,
1621 name: "ghost".to_string(),
1622 exe_path: None,
1623 cwd: None,
1624 command: None,
1625 argv0: None,
1626 raw: None,
1627 cpu_percent: 0.0,
1628 memory_mb: 0.0,
1629 status: ProcessStatus::Running,
1630 user: None,
1631 parent_pid: None,
1632 start_time: None,
1633 uptime_secs: None,
1634 }
1635 }
1636
1637 #[test]
1638 fn every_sysinfo_status_maps_to_ours() {
1639 for (sys, ours) in [
1640 (SysProcessStatus::Run, ProcessStatus::Running),
1641 (SysProcessStatus::Sleep, ProcessStatus::Sleeping),
1642 (SysProcessStatus::Stop, ProcessStatus::Stopped),
1643 (SysProcessStatus::Zombie, ProcessStatus::Zombie),
1644 (SysProcessStatus::Dead, ProcessStatus::Dead),
1645 (SysProcessStatus::Idle, ProcessStatus::Unknown),
1646 ] {
1647 assert_eq!(ProcessStatus::from(sys), ours, "{sys:?}");
1648 }
1649 }
1650
1651 #[test]
1652 fn a_process_that_is_gone_does_not_look_stuck() {
1653 assert!(!ghost().looks_stuck_now());
1654 }
1655
1656 #[test]
1657 fn killing_a_process_that_is_already_gone_is_success() {
1658 assert!(matches!(ghost().kill(), Err(ProcError::ProcessNotFound(_))));
1659 ghost().kill_and_wait().unwrap();
1660 }
1661
1662 #[test]
1663 fn a_kill_that_was_not_delivered_is_an_error() {
1664 kill_outcome(true, 7).unwrap();
1665 let err = kill_outcome(false, 7).unwrap_err();
1666 assert!(
1667 matches!(&err, ProcError::SignalError(m) if m == "Failed to kill process 7"),
1668 "{err:?}"
1669 );
1670 }
1671
1672 #[test]
1673 fn taskkill_without_force_reports_what_it_said() {
1674 #[cfg(unix)]
1675 use std::os::unix::process::ExitStatusExt;
1676 #[cfg(windows)]
1677 use std::os::windows::process::ExitStatusExt;
1678 use std::process::{ExitStatus, Output};
1679
1680 let output = |raw, stdout: &str, stderr: &str| Output {
1681 status: ExitStatus::from_raw(raw),
1682 stdout: stdout.as_bytes().to_vec(),
1683 stderr: stderr.as_bytes().to_vec(),
1684 };
1685 taskkill_outcome(&output(0, "SUCCESS: sent termination signal", "")).unwrap();
1686
1687 #[cfg(unix)]
1689 let failed = 1 << 8;
1690 #[cfg(windows)]
1691 let failed = 1;
1692 let err =
1693 taskkill_outcome(&output(failed, "", "ERROR: could not be terminated")).unwrap_err();
1694 assert!(
1695 matches!(&err, ProcError::SignalError(m) if m == "ERROR: could not be terminated"),
1696 "{err:?}"
1697 );
1698 }
1699
1700 #[cfg(unix)]
1701 #[test]
1702 fn orphans_are_the_children_of_init_that_are_not_system_daemons() {
1703 let proc = |pid: u32, ppid: Option<u32>, exe: Option<&str>, cwd: Option<&str>| Process {
1704 pid,
1705 name: format!("p{pid}"),
1706 exe_path: exe.map(String::from),
1707 cwd: cwd.map(String::from),
1708 parent_pid: ppid,
1709 ..ghost()
1710 };
1711 let orphans = Process::orphans_among(vec![
1712 proc(10, Some(1), Some("/home/z/app"), Some("/home/z")),
1714 proc(11, Some(1), Some("/usr/sbin/sshd"), Some("/")),
1716 proc(1, Some(1), Some("/sbin/init"), Some("/home/z")),
1718 proc(12, Some(500), Some("/home/z/app"), Some("/home/z")),
1719 proc(13, None, Some("/home/z/app"), Some("/home/z")),
1721 ]);
1722 let pids: Vec<u32> = orphans.iter().map(|p| p.pid).collect();
1723 assert_eq!(pids, vec![10]);
1724 }
1725
1726 #[cfg(unix)]
1727 #[test]
1728 fn a_missing_lsof_finds_no_open_file_owners() {
1729 let found = Process::find_by_open_file_with("proc-no-such-lsof", std::path::Path::new("/"));
1730 assert!(found.unwrap().is_empty());
1731 }
1732
1733 #[test]
1734 fn a_wait_for_a_process_that_stays_is_a_wait_and_then_an_error() {
1735 let mut child = crate::test_support::idle_child();
1736 let record = crate::test_support::record_of(&child);
1737 let started = std::time::Instant::now();
1738 let result = record.kill_then_wait(Duration::from_millis(300), |_| Ok(()));
1741 let waited = started.elapsed();
1742 let still_there = child.try_wait().unwrap().is_none();
1743 crate::test_support::end(&mut child);
1744 assert!(
1745 matches!(&result, Err(ProcError::SignalError(_))),
1746 "{result:?}"
1747 );
1748 assert!(waited >= Duration::from_millis(300), "{waited:?}");
1749 assert!(still_there);
1750 }
1751
1752 #[test]
1753 fn a_process_still_alive_when_the_wait_ends_is_an_error() {
1754 let mut child = crate::test_support::idle_child();
1755 let record = crate::test_support::record_of(&child);
1756 let result = record.kill_and_wait_within(Duration::ZERO);
1758 let _ = child.kill();
1759 let _ = child.wait();
1760 assert!(
1761 matches!(&result, Err(ProcError::SignalError(m))
1762 if m == &format!("Process {} is still running 0 seconds after SIGKILL", record.pid)),
1763 "{result:?}"
1764 );
1765 }
1766
1767 #[cfg(target_os = "macos")]
1769 fn kinfo(pid: u32, stat: u8, comm: &str, ruid: u32, ppid: u32, start: u64) -> Vec<u8> {
1770 let mut rec = vec![0u8; KINFO_PROC_SIZE];
1771 rec[0..8].copy_from_slice(&start.to_ne_bytes());
1772 rec[36] = stat;
1773 rec[40..44].copy_from_slice(&pid.to_ne_bytes());
1774 rec[243..243 + comm.len()].copy_from_slice(comm.as_bytes());
1775 rec[392..396].copy_from_slice(&ruid.to_ne_bytes());
1776 rec[560..564].copy_from_slice(&ppid.to_ne_bytes());
1777 rec
1778 }
1779
1780 #[cfg(target_os = "macos")]
1781 #[test]
1782 fn zombies_are_read_from_the_kernels_table() {
1783 let known = std::collections::HashSet::from([30]);
1784 let table: Vec<u8> = [
1785 kinfo(10, 5, "defunct-app", 0, 99, 1_700_000_000),
1786 kinfo(11, 5, "", 0, 1, 0),
1788 kinfo(20, 2, "alive", 0, 1, 1_700_000_000),
1790 kinfo(30, 5, "known", 0, 1, 1_700_000_000),
1792 kinfo(0, 5, "kernel", 0, 0, 0),
1794 ]
1795 .concat();
1796
1797 let zombies = zombies_in(&table, &known);
1798 let pids: Vec<u32> = zombies.iter().map(|z| z.pid).collect();
1799 assert_eq!(pids, vec![10, 11]);
1800
1801 let named = &zombies[0];
1802 assert_eq!(named.name, "defunct-app");
1803 assert_eq!(named.status, ProcessStatus::Zombie);
1804 assert_eq!(named.parent_pid, Some(99));
1805 assert_eq!(named.start_time, Some(1_700_000_000));
1806 assert!(named.uptime_secs.unwrap() > 0);
1807 assert_eq!(named.user.as_deref(), Some("root"));
1808
1809 let unnamed = &zombies[1];
1810 assert_eq!(unnamed.name, "<defunct>");
1811 assert_eq!(unnamed.start_time, None);
1812 assert_eq!(unnamed.uptime_secs, None);
1813 }
1814
1815 #[cfg(target_os = "macos")]
1816 #[test]
1817 fn a_table_of_an_unknown_layout_shows_no_zombies() {
1818 let known = std::collections::HashSet::new();
1819 let mut table = kinfo(10, 5, "z", 0, 1, 0);
1820 table.push(0);
1821 assert!(zombies_in(&table, &known).is_empty());
1822 }
1823
1824 #[cfg(target_os = "macos")]
1825 #[test]
1826 fn a_refused_sysctl_gives_an_empty_table() {
1827 use std::cell::Cell;
1828
1829 assert!(read_proc_table(|_, _| -1).is_empty());
1831
1832 let calls = Cell::new(0);
1834 let table = read_proc_table(|_, size| {
1835 calls.set(calls.get() + 1);
1836 if calls.get() == 1 {
1837 *size = 2 * KINFO_PROC_SIZE;
1838 0
1839 } else {
1840 -1
1841 }
1842 });
1843 assert_eq!(calls.get(), 2);
1844 assert!(table.is_empty());
1845
1846 let calls = Cell::new(0);
1848 let table = read_proc_table(|buf, size| {
1849 calls.set(calls.get() + 1);
1850 if calls.get() == 1 {
1851 *size = 2 * KINFO_PROC_SIZE;
1852 } else {
1853 assert_eq!(*size, 66 * KINFO_PROC_SIZE, "room for 64 newcomers");
1854 unsafe { buf.cast::<u8>().write(7) };
1856 *size = KINFO_PROC_SIZE;
1857 }
1858 0
1859 });
1860 assert_eq!(table.len(), KINFO_PROC_SIZE);
1861 assert_eq!(table[0], 7);
1862 }
1863}