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;
11#[cfg(windows)]
12use sysinfo::Users;
13use sysinfo::{Pid, ProcessStatus as SysProcessStatus, System, Uid};
14
15#[cfg(unix)]
22fn user_name(uid: &Uid) -> String {
23 use std::collections::HashMap;
24 use std::sync::Mutex;
25
26 static NAMES: OnceLock<Mutex<HashMap<u32, String>>> = OnceLock::new();
27 let id: u32 = **uid;
28 let mut names = NAMES.get_or_init(Default::default).lock().unwrap();
29 names
30 .entry(id)
31 .or_insert_with(|| {
32 nix::unistd::User::from_uid(nix::unistd::Uid::from_raw(id))
33 .ok()
34 .flatten()
35 .map(|user| user.name)
36 .unwrap_or_else(|| id.to_string())
37 })
38 .clone()
39}
40
41#[cfg(windows)]
44fn user_name(uid: &Uid) -> String {
45 static USERS: OnceLock<Users> = OnceLock::new();
46 USERS
47 .get_or_init(Users::new_with_refreshed_list)
48 .get_user_by_id(uid)
49 .map(|user| user.name().to_string())
50 .unwrap_or_else(|| uid.to_string())
51}
52
53pub fn escape_control(text: &str) -> std::borrow::Cow<'_, str> {
65 fn needs_escape(c: char) -> bool {
66 c.is_control()
67 || matches!(c, '\u{200e}' | '\u{200f}' | '\u{202a}'..='\u{202e}' | '\u{2066}'..='\u{2069}')
68 }
69 if !text.chars().any(needs_escape) {
70 return std::borrow::Cow::Borrowed(text);
71 }
72 let mut out = String::with_capacity(text.len() + 8);
73 for c in text.chars() {
74 match c {
75 '\n' => out.push_str("\\n"),
76 '\r' => out.push_str("\\r"),
77 '\t' => out.push_str("\\t"),
78 c if c.is_control() => out.push_str(&format!("\\x{:02x}", c as u32)),
79 c if needs_escape(c) => out.push_str(&format!("\\u{{{:x}}}", c as u32)),
80 c => out.push(c),
81 }
82 }
83 std::borrow::Cow::Owned(out)
84}
85
86const STUCK_READINGS: usize = 2;
89const STUCK_READING_GAP: Duration = Duration::from_secs(1);
90
91fn looks_stuck(cpu_percent: f32, status: SysProcessStatus) -> bool {
94 cpu_percent > 50.0 || status == SysProcessStatus::UninterruptibleDiskSleep
95}
96
97fn cpu_percent_between(used_before_ms: u64, used_after_ms: u64, wall: Duration) -> f32 {
103 let wall_ms = wall.as_secs_f64() * 1000.0;
104 if wall_ms <= 0.0 {
105 return 0.0;
106 }
107 (used_after_ms.saturating_sub(used_before_ms) as f64 / wall_ms * 100.0) as f32
108}
109
110#[cfg(not(unix))]
112pub(crate) fn orphans_not_supported() -> ProcError {
113 ProcError::NotSupported(
114 "orphans is not supported on Windows: Windows doesn't re-parent processes, \
115 so there is no reliable way to tell an orphan"
116 .to_string(),
117 )
118}
119
120#[cfg(any(windows, test))]
124fn command_failure(stdout: &[u8], stderr: &[u8], status: &str) -> String {
125 [stderr, stdout]
126 .iter()
127 .map(|out| {
128 String::from_utf8_lossy(out)
130 .lines()
131 .map(str::trim)
132 .filter(|line| !line.is_empty())
133 .collect::<Vec<_>>()
134 .join(" ")
135 })
136 .find(|text| !text.is_empty())
137 .unwrap_or_else(|| format!("exited with {status}"))
138}
139
140fn kill_outcome(delivered: bool, pid: u32) -> Result<()> {
143 if delivered {
144 Ok(())
145 } else {
146 Err(ProcError::SignalError(format!(
147 "Failed to kill process {}",
148 pid
149 )))
150 }
151}
152
153#[cfg(any(windows, test))]
156fn taskkill_outcome(output: &std::process::Output) -> Result<()> {
157 if output.status.success() {
158 Ok(())
159 } else {
160 Err(ProcError::SignalError(command_failure(
161 &output.stdout,
162 &output.stderr,
163 &output.status.to_string(),
164 )))
165 }
166}
167
168fn cpu_used(sys: &System) -> std::collections::HashMap<Pid, u64> {
170 sys.processes()
171 .iter()
172 .map(|(pid, p)| (*pid, p.accumulated_cpu_time()))
173 .collect()
174}
175
176fn ran_long_enough(run_time: u64, timeout_secs: u64) -> bool {
180 run_time >= timeout_secs
181}
182
183fn counts_as_stuck(is_thread: bool, run_time: u64, timeout_secs: u64, looks_stuck: bool) -> bool {
187 !is_thread && ran_long_enough(run_time, timeout_secs) && looks_stuck
188}
189
190#[cfg(target_os = "macos")]
194fn macos_zombies(known: &std::collections::HashSet<u32>) -> Vec<Process> {
195 let table = read_proc_table(|buf, size| {
196 let mut mib = [libc::CTL_KERN, libc::KERN_PROC, libc::KERN_PROC_ALL, 0];
197 let null = std::ptr::null_mut();
198 unsafe { libc::sysctl(mib.as_mut_ptr(), 4, buf, size, null, 0) }
201 });
202 zombies_in(&table, known)
203}
204
205#[cfg(target_os = "macos")]
208const KINFO_PROC_SIZE: usize = 648;
209
210#[cfg(target_os = "macos")]
214fn read_proc_table(
215 sysctl: impl Fn(*mut libc::c_void, &mut libc::size_t) -> libc::c_int,
216) -> Vec<u8> {
217 let mut size: libc::size_t = 0;
218 if sysctl(std::ptr::null_mut(), &mut size) != 0 {
220 return Vec::new();
221 }
222 size += 64 * KINFO_PROC_SIZE;
224 let mut buf: Vec<u8> = vec![0; size];
225 if sysctl(buf.as_mut_ptr().cast(), &mut size) != 0 {
226 return Vec::new();
227 }
228 buf.truncate(size);
229 buf
230}
231
232#[cfg(target_os = "macos")]
234fn zombies_in(buf: &[u8], known: &std::collections::HashSet<u32>) -> Vec<Process> {
235 const P_STARTTIME_SEC: usize = 0;
236 const P_STAT: usize = 36;
237 const P_PID: usize = 40;
238 const P_COMM: usize = 243; const P_RUID: usize = 392;
240 const E_PPID: usize = 560;
241 const SZOMB: u8 = 5;
242
243 if !buf.len().is_multiple_of(KINFO_PROC_SIZE) {
244 return Vec::new(); }
246
247 let read_u32 = |p: &[u8], at: usize| u32::from_ne_bytes(p[at..at + 4].try_into().unwrap());
248 let now = std::time::SystemTime::now()
249 .duration_since(std::time::UNIX_EPOCH)
250 .map(|d| d.as_secs())
251 .unwrap_or(0);
252
253 buf.as_chunks::<KINFO_PROC_SIZE>()
254 .0
255 .iter()
256 .filter(|p| p[P_STAT] == SZOMB)
257 .filter_map(|p| {
258 let pid = read_u32(p, P_PID);
259 if pid == 0 || known.contains(&pid) {
260 return None;
261 }
262 let comm = &p[P_COMM..P_COMM + 17];
263 let comm = &comm[..comm.iter().position(|&c| c == 0).unwrap_or(comm.len())];
264 let name = escape_control(&String::from_utf8_lossy(comm)).into_owned();
265 let start =
266 u64::from_ne_bytes(p[P_STARTTIME_SEC..P_STARTTIME_SEC + 8].try_into().unwrap());
267 Some(Process {
268 pid,
269 name: if name.is_empty() {
270 "<defunct>".to_string()
271 } else {
272 name
273 },
274 exe_path: None,
275 cwd: None,
276 command: None,
277 argv0: None,
278 raw: None,
279 cpu_percent: 0.0,
280 memory_mb: 0.0,
281 status: ProcessStatus::Zombie,
282 user: Uid::try_from(read_u32(p, P_RUID) as usize)
283 .ok()
284 .map(|uid| escape_control(&user_name(&uid)).into_owned()),
285 parent_pid: Some(read_u32(p, E_PPID)),
286 start_time: (start > 0).then_some(start),
287 uptime_secs: (start > 0).then(|| now.saturating_sub(start)),
288 })
289 })
290 .collect()
291}
292
293pub fn is_thread(proc: &sysinfo::Process) -> bool {
297 proc.thread_kind().is_some()
298}
299
300fn strip_deleted_marker(cwd: String) -> String {
304 match cwd.strip_suffix(" (deleted)") {
305 Some(path) if !std::path::Path::new(&cwd).exists() => path.to_string(),
306 _ => cwd,
307 }
308}
309
310#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
312#[serde(rename_all = "lowercase")]
313pub enum ProcessStatus {
314 Running,
316 Sleeping,
318 Stopped,
320 Zombie,
322 Dead,
324 Unknown,
326}
327
328impl From<SysProcessStatus> for ProcessStatus {
329 fn from(status: SysProcessStatus) -> Self {
330 match status {
331 SysProcessStatus::Run => ProcessStatus::Running,
332 SysProcessStatus::Sleep => ProcessStatus::Sleeping,
333 SysProcessStatus::Stop => ProcessStatus::Stopped,
334 SysProcessStatus::Zombie => ProcessStatus::Zombie,
335 SysProcessStatus::Dead => ProcessStatus::Dead,
336 _ => ProcessStatus::Unknown,
337 }
338 }
339}
340
341#[derive(Debug, Clone, Serialize, Deserialize)]
343pub struct Process {
344 pub pid: u32,
346 pub name: String,
348 #[serde(skip_serializing_if = "Option::is_none")]
350 pub exe_path: Option<String>,
351 #[serde(skip_serializing_if = "Option::is_none")]
353 pub cwd: Option<String>,
354 #[serde(skip_serializing_if = "Option::is_none")]
356 pub command: Option<String>,
357 #[serde(skip)]
361 pub argv0: Option<String>,
362 #[serde(skip)]
368 pub raw: Option<Box<RawText>>,
369 pub cpu_percent: f32,
371 pub memory_mb: f64,
373 pub status: ProcessStatus,
375 #[serde(skip_serializing_if = "Option::is_none")]
377 pub user: Option<String>,
378 #[serde(skip_serializing_if = "Option::is_none")]
380 pub parent_pid: Option<u32>,
381 #[serde(skip_serializing_if = "Option::is_none")]
383 pub start_time: Option<u64>,
384 #[serde(skip_serializing_if = "Option::is_none")]
386 pub uptime_secs: Option<u64>,
387}
388
389pub(crate) fn self_and_ancestors(sys: &System) -> std::collections::HashSet<Pid> {
393 let self_pid = Pid::from_u32(std::process::id());
394 let mut pids = std::collections::HashSet::new();
395 pids.insert(self_pid);
396 let mut current = self_pid;
397 for _ in 0..10 {
398 if let Some(parent) = sys.process(current).and_then(|p| p.parent()) {
399 pids.insert(parent);
400 current = parent;
401 } else {
402 break;
403 }
404 }
405 pids
406}
407
408#[derive(Debug, Clone)]
411pub struct RawText {
412 pub name: String,
414 pub exe_path: Option<String>,
416 pub command: Option<String>,
418 pub cwd: Option<String>,
420}
421
422pub(crate) fn classify_sysinfo(pattern: &str, proc: &sysinfo::Process, mode: NameMatch) -> Hit {
426 let name = proc.name().to_string_lossy();
427 let exe = proc.exe().map(|exe| exe.to_string_lossy());
428 let argv0 = proc.cmd().first().map(|arg| arg.to_string_lossy());
429 let command = (!proc.cmd().is_empty()).then(|| {
431 proc.cmd()
432 .iter()
433 .map(|arg| arg.to_string_lossy())
434 .collect::<Vec<_>>()
435 .join(" ")
436 });
437 classify(
438 pattern,
439 &Names {
440 name: &name,
441 exe: exe.as_deref(),
442 argv0: argv0.as_deref(),
443 command: command.as_deref(),
444 },
445 mode,
446 )
447}
448
449impl Process {
450 pub fn names(&self) -> Names<'_> {
452 match &self.raw {
453 Some(raw) => Names {
454 name: &raw.name,
455 exe: raw.exe_path.as_deref(),
456 argv0: self.argv0.as_deref(),
457 command: raw.command.as_deref(),
458 },
459 None => Names {
460 name: &self.name,
461 exe: self.exe_path.as_deref(),
462 argv0: self.argv0.as_deref(),
463 command: self.command.as_deref(),
464 },
465 }
466 }
467
468 pub fn raw_cwd(&self) -> Option<&str> {
473 match &self.raw {
474 Some(raw) => raw.cwd.as_deref(),
475 None => self.cwd.as_deref(),
476 }
477 }
478
479 pub fn find_by_name(pattern: &str, mode: NameMatch) -> Result<Vec<Process>> {
486 let mut sys = System::new_all();
487 sys.refresh_all();
488
489 let ancestor_pids = self_and_ancestors(&sys);
492 let mut in_command_line_only = 0;
493 let mut processes: Vec<Process> = sys
494 .processes()
495 .iter()
496 .filter_map(|(pid, proc)| {
497 if ancestor_pids.contains(pid) || is_thread(proc) {
499 return None;
500 }
501 match classify_sysinfo(pattern, proc, mode) {
502 Hit::Match => Some(Process::from_sysinfo(*pid, proc)),
503 Hit::CommandLineOnly => {
504 in_command_line_only += 1;
505 None
506 }
507 Hit::No => None,
508 }
509 })
510 .collect();
511 processes.sort_by_key(|p| p.pid);
514
515 if processes.is_empty() {
516 return Err(ProcError::name_not_found(pattern, in_command_line_only));
517 }
518
519 Ok(processes)
520 }
521
522 pub fn find_by_pid(pid: u32) -> Result<Option<Process>> {
524 let mut sys = System::new_all();
525 sys.refresh_all();
526
527 let sysinfo_pid = Pid::from_u32(pid);
528
529 Ok(sys
530 .processes()
531 .get(&sysinfo_pid)
532 .map(|proc| Process::from_sysinfo(sysinfo_pid, proc)))
533 }
534
535 pub fn find_all() -> Result<Vec<Process>> {
537 let mut sys = System::new_all();
538 sys.refresh_all();
539
540 let self_pid = sysinfo::Pid::from_u32(std::process::id());
541 #[allow(unused_mut)]
542 let mut processes: Vec<Process> = sys
543 .processes()
544 .iter()
545 .filter(|(pid, proc)| **pid != self_pid && !is_thread(proc))
546 .map(|(pid, proc)| Process::from_sysinfo(*pid, proc))
547 .collect();
548
549 #[cfg(target_os = "macos")]
550 {
551 let known: std::collections::HashSet<u32> = processes.iter().map(|p| p.pid).collect();
552 processes.extend(macos_zombies(&known));
553 }
554
555 processes.sort_by_key(|p| p.pid);
559
560 Ok(processes)
561 }
562
563 pub fn find_by_exe_path(path: &std::path::Path) -> Result<Vec<Process>> {
565 let all = Self::find_all()?;
566 let path_str = path.to_string_lossy();
567
568 Ok(all
569 .into_iter()
570 .filter(|p| {
571 if let Some(ref exe) = p.exe_path {
572 exe == &*path_str || std::path::Path::new(exe) == path
574 } else {
575 false
576 }
577 })
578 .collect())
579 }
580
581 #[cfg(unix)]
583 pub fn find_by_open_file(path: &std::path::Path) -> Result<Vec<Process>> {
584 Self::find_by_open_file_with("lsof", path)
585 }
586
587 #[cfg(unix)]
589 fn find_by_open_file_with(lsof: &str, path: &std::path::Path) -> Result<Vec<Process>> {
590 use std::process::Command;
591
592 let output = crate::ui::interrupt::unblocked_in_child(&mut Command::new(lsof))
593 .args(["-t", &path.to_string_lossy()]) .output();
595
596 let output = match output {
597 Ok(o) => o,
598 Err(_) => return Ok(vec![]), };
600
601 if !output.status.success() {
602 return Ok(vec![]); }
604
605 let pids: Vec<u32> = String::from_utf8_lossy(&output.stdout)
606 .lines()
607 .filter_map(|line| line.trim().parse().ok())
608 .collect();
609
610 let mut processes = Vec::new();
611 for pid in pids {
612 if let Ok(Some(proc)) = Self::find_by_pid(pid) {
613 processes.push(proc);
614 }
615 }
616
617 Ok(processes)
618 }
619
620 #[cfg(not(unix))]
622 pub fn find_by_open_file(_path: &std::path::Path) -> Result<Vec<Process>> {
623 Ok(vec![])
625 }
626
627 pub fn find_stuck(timeout: Duration) -> Result<Vec<Process>> {
633 let timeout_secs = timeout.as_secs();
634 let mut read_at = std::time::Instant::now();
636 let mut sys = System::new_all();
637 let mut used = cpu_used(&sys);
638 let mut cpu: std::collections::HashMap<Pid, f32> = std::collections::HashMap::new();
639 let mut stuck: Option<std::collections::HashSet<Pid>> = None;
640
641 for _ in 0..STUCK_READINGS {
642 std::thread::sleep(STUCK_READING_GAP);
643 let started = std::time::Instant::now();
644 sys.refresh_processes(sysinfo::ProcessesToUpdate::All, true);
645 let wall = started - read_at;
646 read_at = started;
647 cpu = sys
649 .processes()
650 .iter()
651 .filter_map(|(pid, p)| {
652 let before = used.get(pid)?;
653 Some((
654 *pid,
655 cpu_percent_between(*before, p.accumulated_cpu_time(), wall),
656 ))
657 })
658 .collect();
659 used = cpu_used(&sys);
660 let now: std::collections::HashSet<Pid> = sys
661 .processes()
662 .iter()
663 .filter(|(pid, p)| {
664 let busy = cpu.get(pid).copied().unwrap_or(0.0);
665 counts_as_stuck(
666 is_thread(p),
667 p.run_time(),
668 timeout_secs,
669 looks_stuck(busy, p.status()),
670 )
671 })
672 .map(|(pid, _)| *pid)
673 .collect();
674 stuck = Some(match stuck {
675 None => now,
676 Some(before) => before.intersection(&now).copied().collect(),
677 });
678 }
679
680 let stuck = stuck.unwrap_or_default();
681 let mut found: Vec<Process> = sys
682 .processes()
683 .iter()
684 .filter(|(pid, _)| stuck.contains(pid))
685 .map(|(pid, p)| {
686 let mut process = Process::from_sysinfo(*pid, p);
687 if let Some(&busy) = cpu.get(pid) {
689 process.cpu_percent = busy;
690 }
691 process
692 })
693 .collect();
694 found.sort_by_key(|p| p.pid);
696 Ok(found)
697 }
698
699 pub fn looks_stuck_now(&self) -> bool {
702 let pid = Pid::from_u32(self.pid);
703 let mut sys = System::new();
704 let mut read_at = std::time::Instant::now();
705 sys.refresh_processes(sysinfo::ProcessesToUpdate::Some(&[pid]), true);
706 let mut used = sys
707 .process(pid)
708 .filter(|p| self.is_same(p))
709 .map(|p| p.accumulated_cpu_time());
710 (0..STUCK_READINGS).all(|_| {
711 std::thread::sleep(STUCK_READING_GAP);
712 let started = std::time::Instant::now();
713 sys.refresh_processes(sysinfo::ProcessesToUpdate::Some(&[pid]), true);
714 let wall = started - read_at;
715 read_at = started;
716 let (Some(before), Some(p)) = (used, sys.process(pid).filter(|p| self.is_same(p)))
717 else {
718 return false;
719 };
720 let after = p.accumulated_cpu_time();
721 used = Some(after);
722 looks_stuck(cpu_percent_between(before, after, wall), p.status())
723 })
724 }
725
726 #[cfg(unix)]
733 pub fn cpu_over(&self, window: Duration) -> Option<f32> {
734 let pid = Pid::from_u32(self.pid);
735 let mut sys = System::new();
736 let started = std::time::Instant::now();
737 sys.refresh_processes(sysinfo::ProcessesToUpdate::Some(&[pid]), true);
738 let before = sys
739 .process(pid)
740 .filter(|p| self.is_same(p))
741 .map(|p| p.accumulated_cpu_time())?;
742 std::thread::sleep(window.max(sysinfo::MINIMUM_CPU_UPDATE_INTERVAL));
743 let wall = started.elapsed();
744 sys.refresh_processes(sysinfo::ProcessesToUpdate::Some(&[pid]), true);
745 sys.process(pid)
746 .filter(|p| self.is_same(p))
747 .map(|p| cpu_percent_between(before, p.accumulated_cpu_time(), wall))
748 }
749
750 pub fn measure_cpu(processes: &mut [Process]) {
761 if processes.is_empty() {
762 return;
763 }
764 let pids: Vec<Pid> = processes.iter().map(|p| Pid::from_u32(p.pid)).collect();
765 let which = sysinfo::ProcessesToUpdate::Some(&pids);
766 let cpu_only = sysinfo::ProcessRefreshKind::nothing().with_cpu();
767 let mut sys = System::new();
768
769 let first = std::time::Instant::now();
770 sys.refresh_processes_specifics(which, true, cpu_only);
771 let before = cpu_used(&sys);
772 std::thread::sleep(sysinfo::MINIMUM_CPU_UPDATE_INTERVAL);
773 let wall = first.elapsed();
776 sys.refresh_processes_specifics(which, true, cpu_only);
777
778 for process in processes.iter_mut() {
779 let pid = Pid::from_u32(process.pid);
780 let (Some(current), Some(&cpu_before)) = (sys.process(pid), before.get(&pid)) else {
781 continue;
782 };
783 if process.is_same(current) {
784 process.cpu_percent =
785 cpu_percent_between(cpu_before, current.accumulated_cpu_time(), wall);
786 }
787 }
788 }
789
790 pub fn kill(&self) -> Result<()> {
792 let mut sys = System::new();
793 sys.refresh_processes(
794 sysinfo::ProcessesToUpdate::Some(&[Pid::from_u32(self.pid)]),
795 true,
796 );
797
798 match sys.process(Pid::from_u32(self.pid)) {
799 Some(proc) if self.is_same(proc) => kill_outcome(proc.kill(), self.pid),
800 _ => Err(ProcError::ProcessNotFound(self.pid.to_string())),
802 }
803 }
804
805 fn is_same(&self, current: &sysinfo::Process) -> bool {
808 match self.start_time {
809 Some(ours) if current.start_time() > 0 => current.start_time().abs_diff(ours) <= 1,
810 _ => true,
811 }
812 }
813
814 fn ensure_same_process(&self) -> Result<()> {
818 let mut sys = System::new();
819 let pid = Pid::from_u32(self.pid);
820 sys.refresh_processes(sysinfo::ProcessesToUpdate::Some(&[pid]), true);
821 match sys.process(pid) {
822 Some(current) if self.is_same(current) => Ok(()),
823 _ => Err(ProcError::ProcessNotFound(self.pid.to_string())),
824 }
825 }
826
827 pub fn kill_and_wait(&self) -> Result<()> {
833 self.kill_and_wait_within(Duration::from_secs(5))
834 }
835
836 fn kill_and_wait_within(&self, wait: Duration) -> Result<()> {
838 self.kill_then_wait(wait, Process::kill)
839 }
840
841 fn kill_then_wait(&self, wait: Duration, kill: impl Fn(&Process) -> Result<()>) -> Result<()> {
845 match kill(self) {
846 Err(ProcError::ProcessNotFound(_)) => return Ok(()),
848 other => other?,
849 }
850 let deadline = std::time::Instant::now() + wait;
851 while std::time::Instant::now() < deadline {
852 if !self.is_running() {
853 return Ok(());
854 }
855 std::thread::sleep(Duration::from_millis(50));
856 }
857 Err(ProcError::SignalError(format!(
858 "Process {} is still running {} seconds after SIGKILL",
859 self.pid,
860 wait.as_secs()
861 )))
862 }
863
864 #[cfg(unix)]
866 pub fn terminate(&self) -> Result<()> {
867 use nix::sys::signal::{kill, Signal};
868 use nix::unistd::Pid as NixPid;
869
870 self.ensure_same_process()?;
871 kill(NixPid::from_raw(self.pid as i32), Signal::SIGTERM)
872 .map_err(|e| ProcError::SignalError(e.to_string()))
873 }
874
875 #[cfg(windows)]
880 pub fn terminate(&self) -> Result<()> {
881 use std::process::Command;
882
883 self.ensure_same_process()?;
884 let output = Command::new("taskkill")
885 .args(["/PID", &self.pid.to_string()])
886 .output()
887 .map_err(|e| ProcError::SystemError(e.to_string()))?;
888
889 taskkill_outcome(&output)
890 }
891
892 #[cfg(unix)]
894 pub fn send_signal(&self, signal: nix::sys::signal::Signal) -> Result<()> {
895 use nix::sys::signal::kill;
896 use nix::unistd::Pid as NixPid;
897 self.ensure_same_process()?;
898 kill(NixPid::from_raw(self.pid as i32), signal)
899 .map_err(|e| ProcError::SignalError(format!("{}: {}", signal, e)))
900 }
901
902 #[cfg(unix)]
911 pub fn find_orphans() -> Result<Vec<Process>> {
912 Ok(Self::orphans_among(Self::find_all()?))
913 }
914
915 #[cfg(not(unix))]
917 pub fn find_orphans() -> Result<Vec<Process>> {
918 Err(orphans_not_supported())
919 }
920
921 #[cfg(unix)]
923 fn orphans_among(all: Vec<Process>) -> Vec<Process> {
924 all.into_iter()
925 .filter(|p| {
926 if let Some(ppid) = p.parent_pid {
927 ppid == 1 && p.pid != 1 && !Self::is_system_process(p)
928 } else {
929 false
930 }
931 })
932 .collect()
933 }
934
935 #[cfg(unix)]
941 fn is_system_process(p: &Process) -> bool {
942 if p.cwd.is_none() || p.cwd.as_deref() == Some("/") {
943 if let Some(ref exe) = p.exe_path {
944 if exe.starts_with("/System/") || exe.starts_with("/usr/libexec/") {
946 return true;
947 }
948 if exe.starts_with("/usr/sbin/")
950 || exe.starts_with("/sbin/")
951 || exe.starts_with("/usr/bin/")
952 || exe.starts_with("/usr/lib/")
953 || exe.starts_with("/usr/lib64/")
954 || exe.starts_with("/lib/")
955 || exe.starts_with("/lib64/")
956 || exe.starts_with("/opt/")
957 || exe.starts_with("/snap/")
958 {
959 return true;
960 }
961 }
962 return true; }
964 false
965 }
966
967 pub fn exists(&self) -> bool {
971 let mut sys = System::new();
972 let pid = Pid::from_u32(self.pid);
973 sys.refresh_processes(sysinfo::ProcessesToUpdate::Some(&[pid]), true);
974 match sys.process(pid) {
975 None => false,
976 Some(current) if current.status() == SysProcessStatus::Zombie => false,
977 Some(current) => self.is_same(current),
978 }
979 }
980
981 pub fn is_running(&self) -> bool {
983 self.exists()
984 }
985
986 pub(crate) fn from_sysinfo(pid: Pid, proc: &sysinfo::Process) -> Self {
988 let cmd_vec = proc.cmd();
989 let raw_command = (!cmd_vec.is_empty()).then(|| {
993 cmd_vec
994 .iter()
995 .map(|s| s.to_string_lossy())
996 .collect::<Vec<_>>()
997 .join(" ")
998 });
999 let raw_name = proc.name().to_string_lossy();
1000 let raw_exe = proc.exe().map(|p| p.to_string_lossy());
1001 let raw_cwd = proc
1002 .cwd()
1003 .map(|p| strip_deleted_marker(p.to_string_lossy().to_string()));
1004 let changed = |text: &str| matches!(escape_control(text), std::borrow::Cow::Owned(_));
1005 let raw = (changed(&raw_name)
1006 || raw_exe.as_deref().is_some_and(changed)
1007 || raw_command.as_deref().is_some_and(changed)
1008 || raw_cwd.as_deref().is_some_and(changed))
1009 .then(|| {
1010 Box::new(RawText {
1011 name: raw_name.to_string(),
1012 exe_path: raw_exe.as_ref().map(|exe| exe.to_string()),
1013 command: raw_command.clone(),
1014 cwd: raw_cwd.clone(),
1015 })
1016 });
1017 let command = raw_command
1018 .as_deref()
1019 .map(|joined| escape_control(joined).into_owned());
1020
1021 let exe_path = raw_exe
1022 .as_deref()
1023 .map(|exe| escape_control(exe).into_owned());
1024 let cwd = raw_cwd
1025 .as_deref()
1026 .map(|path| escape_control(path).into_owned());
1027
1028 Process {
1029 pid: pid.as_u32(),
1030 name: escape_control(&raw_name).into_owned(),
1031 exe_path,
1032 cwd,
1033 command,
1034 argv0: cmd_vec
1035 .first()
1036 .map(|arg| arg.to_string_lossy().into_owned()),
1037 raw,
1038 cpu_percent: proc.cpu_usage(),
1039 memory_mb: proc.memory() as f64 / 1024.0 / 1024.0,
1040 status: ProcessStatus::from(proc.status()),
1041 user: proc
1042 .user_id()
1043 .map(|uid| escape_control(&user_name(uid)).into_owned()),
1044 parent_pid: proc.parent().map(|p| p.as_u32()),
1045 start_time: (proc.start_time() > 0).then(|| proc.start_time()),
1048 uptime_secs: (proc.start_time() > 0).then(|| proc.run_time()),
1049 }
1050 }
1051}
1052
1053#[cfg(unix)]
1058pub fn parse_signal_name(name: &str) -> Result<nix::sys::signal::Signal> {
1059 use nix::sys::signal::Signal;
1060
1061 let upper = name.to_uppercase();
1062 let upper = upper.trim_start_matches("SIG");
1063 match upper {
1064 "HUP" => Ok(Signal::SIGHUP),
1065 "INT" => Ok(Signal::SIGINT),
1066 "QUIT" => Ok(Signal::SIGQUIT),
1067 "ABRT" => Ok(Signal::SIGABRT),
1068 "KILL" => Ok(Signal::SIGKILL),
1069 "TERM" => Ok(Signal::SIGTERM),
1070 "STOP" => Ok(Signal::SIGSTOP),
1071 "CONT" => Ok(Signal::SIGCONT),
1072 "USR1" => Ok(Signal::SIGUSR1),
1073 "USR2" => Ok(Signal::SIGUSR2),
1074 _ => Err(ProcError::InvalidInput(format!(
1075 "Unknown signal: '{}'. Valid signals: HUP, INT, QUIT, ABRT, KILL, TERM, STOP, CONT, USR1, USR2",
1076 name
1077 ))),
1078 }
1079}
1080
1081#[cfg(test)]
1082mod tests {
1083 use super::*;
1084
1085 #[cfg(target_os = "macos")]
1090 #[test]
1091 fn an_account_whose_shell_is_false_is_named() {
1092 let passwd = std::fs::read_to_string("/etc/passwd").unwrap();
1093 let f = passwd
1094 .lines()
1095 .filter(|line| !line.starts_with('#'))
1096 .map(|line| line.split(':').collect::<Vec<_>>())
1097 .find(|f| f.len() == 7 && f[6].ends_with("/false") && f[2].parse::<u32>().is_ok())
1098 .expect("macOS has daemon accounts with a false shell");
1099 let uid = Uid::try_from(f[2].parse::<usize>().unwrap()).unwrap();
1100 assert_eq!(user_name(&uid), f[0]);
1101 }
1102
1103 #[cfg(unix)]
1104 #[test]
1105 fn a_user_id_nobody_has_is_shown_as_the_number() {
1106 let uid = Uid::try_from(4_000_000_000usize).unwrap();
1107 assert_eq!(user_name(&uid), "4000000000");
1108 }
1109
1110 #[test]
1111 fn cpu_is_the_cpu_time_used_over_the_wall_time() {
1112 let second = Duration::from_secs(1);
1113 assert_eq!(cpu_percent_between(5_000, 6_000, second), 100.0);
1115 assert_eq!(cpu_percent_between(5_000, 5_500, second), 50.0);
1116 assert_eq!(cpu_percent_between(5_000, 5_000, second), 0.0);
1117 assert_eq!(cpu_percent_between(0, 2_000, second), 200.0);
1119 assert_eq!(cpu_percent_between(6_000, 5_000, second), 0.0);
1121 assert_eq!(cpu_percent_between(0, 1_000, Duration::ZERO), 0.0);
1122 }
1123
1124 #[test]
1125 fn a_failed_command_is_explained_in_its_own_words() {
1126 let reason = "ERROR: The process with PID 7 could not be terminated.";
1127 assert_eq!(
1128 command_failure(b"", reason.as_bytes(), "exit code: 1"),
1129 reason
1130 );
1131 assert_eq!(
1133 command_failure(b" not here\r\n", b"", "exit code: 1"),
1134 "not here"
1135 );
1136 assert_eq!(
1138 command_failure(
1139 b"",
1140 b"ERROR: first part\r\n second part.\r\n\r\n",
1141 "exit code: 1"
1142 ),
1143 "ERROR: first part second part."
1144 );
1145 assert_eq!(
1146 command_failure(b"", b" \n", "exit code: 128"),
1147 "exited with exit code: 128"
1148 );
1149 }
1150
1151 #[test]
1152 fn busy_means_over_half_a_core_or_blocked_in_the_kernel() {
1153 assert!(looks_stuck(50.1, SysProcessStatus::Run));
1154 assert!(!looks_stuck(50.0, SysProcessStatus::Run));
1155 assert!(!looks_stuck(0.0, SysProcessStatus::Sleep));
1156 assert!(looks_stuck(0.0, SysProcessStatus::UninterruptibleDiskSleep));
1157 }
1158
1159 #[test]
1160 fn a_reading_counts_as_stuck_from_the_timeout_on() {
1161 assert!(counts_as_stuck(false, 0, 0, true));
1162 assert!(!counts_as_stuck(false, 59, 60, true));
1163 assert!(counts_as_stuck(false, 60, 60, true));
1164 assert!(counts_as_stuck(false, 61, 60, true));
1165 assert!(!counts_as_stuck(false, 60, 60, false));
1167 assert!(!counts_as_stuck(true, 60, 60, true));
1168 }
1169
1170 #[test]
1171 fn a_process_that_ran_the_timeout_has_run_long_enough() {
1172 assert!(ran_long_enough(0, 0));
1174 assert!(ran_long_enough(5, 5));
1175 assert!(ran_long_enough(6, 5));
1176 assert!(!ran_long_enough(4, 5));
1177 }
1178
1179 #[test]
1180 fn test_find_all_processes() {
1181 let processes = Process::find_all().unwrap();
1182 assert!(!processes.is_empty(), "Should find at least one process");
1183 }
1184
1185 #[test]
1186 fn every_process_source_lists_pid_ascending() {
1187 use crate::test_support::{end, idle_child, record_of};
1188
1189 let mut children: Vec<_> = (0..3).map(|_| idle_child()).collect();
1190 let mine: Vec<u32> = children.iter().map(|c| record_of(c).pid).collect();
1191 let name = record_of(&children[0]).name;
1192
1193 let ascending = |what: &str, list: &[Process]| {
1194 let pids: Vec<u32> = list.iter().map(|p| p.pid).collect();
1195 let mut sorted = pids.clone();
1196 sorted.sort_unstable();
1197 sorted.dedup();
1198 assert_eq!(pids, sorted, "{what} is not PID ascending");
1199 pids
1200 };
1201 let all = ascending("find_all", &Process::find_all().unwrap());
1202 let named = ascending(
1203 "find_by_name",
1204 &Process::find_by_name(&name, NameMatch::Exact).unwrap(),
1205 );
1206 ascending("find_stuck", &Process::find_stuck(Duration::ZERO).unwrap());
1207 for pid in &mine {
1208 assert!(all.contains(pid), "find_all has {pid}");
1209 assert!(named.contains(pid), "find_by_name has {pid}");
1210 }
1211 children.iter_mut().for_each(end);
1212 }
1213
1214 #[test]
1215 fn test_find_by_pid_self() {
1216 let pid = std::process::id();
1217 let process = Process::find_by_pid(pid).unwrap();
1218 assert!(process.is_some(), "Should find own process");
1219 }
1220
1221 #[test]
1222 fn test_is_running_tells_a_reused_pid_apart() {
1223 let mut me = Process::find_by_pid(std::process::id()).unwrap().unwrap();
1224 assert!(me.is_running());
1225 let start = me.start_time.expect("a running process has a start time");
1227 me.start_time = Some(start - 3600);
1228 assert!(!me.is_running(), "a reused PID counted as the same process");
1229 }
1230
1231 #[test]
1232 fn test_escape_control_makes_terminal_input_harmless() {
1233 assert_eq!(escape_control("node server.js"), "node server.js");
1235 assert_eq!(escape_control("café ✓ 日本"), "café ✓ 日本");
1236 assert_eq!(escape_control("a\x1b[31mb"), "a\\x1b[31mb");
1238 assert_eq!(escape_control("\x1b]0;pwned\x07"), "\\x1b]0;pwned\\x07");
1239 assert_eq!(escape_control("one\ntwo\tthree\r"), "one\\ntwo\\tthree\\r");
1241 assert_eq!(escape_control("x\x7fy\u{9b}2J"), "x\\x7fy\\x9b2J");
1243 assert_eq!(escape_control("ab\u{202e}cd"), "ab\\u{202e}cd");
1245 }
1246
1247 #[cfg(unix)]
1248 #[test]
1249 fn test_looks_stuck_needs_sustained_cpu() {
1250 let spawn = |script: &str| {
1251 std::process::Command::new("sh")
1252 .args(["-c", script])
1253 .spawn()
1254 .unwrap()
1255 };
1256 let mut spinning = spawn("while :; do :; done");
1257 let mut idle = spawn("exec sleep 300");
1258 std::thread::sleep(Duration::from_millis(300));
1259 let record = |pid: u32| Process::find_by_pid(pid).unwrap().unwrap();
1260
1261 let spinning_stuck = record(spinning.id()).looks_stuck_now();
1262 let idle_stuck = record(idle.id()).looks_stuck_now();
1263 for child in [&mut spinning, &mut idle] {
1264 let _ = child.kill();
1265 let _ = child.wait();
1266 }
1267 assert!(
1268 spinning_stuck,
1269 "a process spinning at 100% CPU wasn't stuck"
1270 );
1271 assert!(!idle_stuck, "an idle process counted as stuck");
1272 }
1273
1274 #[test]
1275 fn test_signals_never_reach_a_process_that_reused_the_pid() {
1276 let mut child = crate::test_support::idle_child();
1277 let mut stale = crate::test_support::record_of(&child);
1278 stale.start_time = stale.start_time.map(|t| t - 3600);
1280
1281 let kill = stale.kill();
1282 let term = stale.terminate();
1283 #[cfg(unix)]
1284 let sig = stale.send_signal(nix::sys::signal::Signal::SIGKILL);
1285 let alive = child.try_wait().unwrap().is_none();
1286 let _ = child.kill();
1287 let _ = child.wait();
1288
1289 assert!(
1290 alive,
1291 "a stale record signalled the process now using its PID"
1292 );
1293 assert!(
1294 matches!(kill, Err(ProcError::ProcessNotFound(_))),
1295 "{kill:?}"
1296 );
1297 assert!(
1298 matches!(term, Err(ProcError::ProcessNotFound(_))),
1299 "{term:?}"
1300 );
1301 #[cfg(unix)]
1302 assert!(matches!(sig, Err(ProcError::ProcessNotFound(_))), "{sig:?}");
1303 }
1304
1305 #[cfg(unix)]
1309 #[test]
1310 fn text_with_control_characters_matches_as_typed_and_displays_escaped() {
1311 use crate::core::matches_by_filter;
1312 use std::os::unix::process::CommandExt;
1313
1314 let word = format!("zz\u{7}ctl{}x", std::process::id());
1315 let mut child = std::process::Command::new("sh")
1319 .arg0(&word)
1320 .args(["-c", "read line"])
1321 .stdin(std::process::Stdio::piped())
1322 .spawn()
1323 .unwrap();
1324 let pid = child.id();
1325 let record = std::iter::repeat_with(|| {
1330 std::thread::sleep(Duration::from_millis(5));
1331 Process::find_by_pid(pid).unwrap().unwrap()
1332 })
1333 .take(1000)
1334 .find(|record| record.argv0.is_some());
1335 let part = format!("\u{7}ctl{}", std::process::id());
1336
1337 let found = |pattern: &str, mode| {
1338 Process::find_by_name(pattern, mode)
1339 .map(|found| found.iter().any(|p| p.pid == pid))
1340 .unwrap_or(false)
1341 };
1342 let named = found(&word, NameMatch::Exact);
1343 let named_in_command = found(&part, NameMatch::Command);
1344 let _ = child.kill();
1345 let _ = child.wait();
1346
1347 let record = record.expect("the child's command line never appeared");
1348 assert!(named, "find_by_name read the escaped first argument");
1349 assert!(
1350 named_in_command,
1351 "find_by_name read the escaped command line"
1352 );
1353 assert!(
1354 matches_by_filter(&record, &word, NameMatch::Exact),
1355 "--by read the escaped first argument"
1356 );
1357 assert!(
1358 matches_by_filter(&record, &part, NameMatch::Command),
1359 "--by --cmd read the escaped command line"
1360 );
1361 let shown = record.command.as_deref().unwrap();
1363 assert!(shown.contains("\\x07ctl"), "{shown}");
1364 assert!(!shown.contains('\u{7}'), "{shown:?}");
1365 }
1366
1367 #[cfg(windows)]
1371 #[test]
1372 fn a_control_character_in_a_windows_command_line_matches_as_typed_and_displays_escaped() {
1373 use crate::core::matches_by_filter;
1374
1375 let mut exe = std::env::current_exe().unwrap();
1376 exe.pop();
1377 exe.pop();
1378 exe.push("examples");
1379 exe.push("listener.exe");
1380 assert!(exe.exists(), "build the example first: {}", exe.display());
1381 let part = format!("\u{7}ctl{}", std::process::id());
1382 let mut child = std::process::Command::new(&exe)
1383 .arg(&part)
1384 .stdin(std::process::Stdio::null())
1385 .stdout(std::process::Stdio::null())
1386 .spawn()
1387 .unwrap();
1388 let pid = child.id();
1389 let record = std::iter::repeat_with(|| {
1390 std::thread::sleep(Duration::from_millis(20));
1391 Process::find_by_pid(pid).unwrap()
1392 })
1393 .take(500)
1394 .flatten()
1395 .find(|record| record.command.is_some())
1396 .expect("the child's command line never appeared");
1397 let found = Process::find_by_name(&part, NameMatch::Command)
1398 .map(|found| found.iter().any(|p| p.pid == pid))
1399 .unwrap_or(false);
1400 let kept = matches_by_filter(&record, &part, NameMatch::Command);
1401 crate::test_support::end(&mut child);
1402
1403 assert!(found, "find_by_name read the escaped command line");
1404 assert!(kept, "--by --cmd read the escaped command line");
1405 let shown = record.command.as_deref().unwrap();
1406 assert!(shown.contains("\\x07ctl"), "{shown}");
1407 assert!(!shown.contains('\u{7}'), "{shown:?}");
1408 }
1409
1410 #[test]
1411 fn a_name_is_read_from_the_raw_text_when_escaping_changed_it() {
1412 let record = Process {
1413 name: "a\\x07b".to_string(),
1414 raw: Some(Box::new(RawText {
1415 name: "a\u{7}b".to_string(),
1416 exe_path: Some("c\u{7}d".to_string()),
1417 command: Some("e\u{7}f".to_string()),
1418 cwd: None,
1419 })),
1420 argv0: Some("g".to_string()),
1421 ..ghost()
1422 };
1423 let names = record.names();
1424 assert_eq!(names.name, "a\u{7}b");
1425 assert_eq!(names.exe, Some("c\u{7}d"));
1426 assert_eq!(names.argv0, Some("g"));
1427 assert_eq!(names.command, Some("e\u{7}f"));
1428 }
1429
1430 #[test]
1431 fn the_ancestors_stop_where_the_process_list_does() {
1432 let me = Pid::from_u32(std::process::id());
1435 let mut sys = System::new();
1436 sys.refresh_processes(sysinfo::ProcessesToUpdate::Some(&[me]), true);
1437 let parent = sys.process(me).and_then(|p| p.parent());
1438 let expected: std::collections::HashSet<Pid> = std::iter::once(me).chain(parent).collect();
1439 assert_eq!(self_and_ancestors(&sys), expected);
1440 }
1441
1442 #[test]
1443 fn test_strip_deleted_marker() {
1444 assert_eq!(
1445 strip_deleted_marker("/nonexistent/proc-gone (deleted)".to_string()),
1446 "/nonexistent/proc-gone"
1447 );
1448 assert_eq!(
1449 strip_deleted_marker("/tmp/project".to_string()),
1450 "/tmp/project"
1451 );
1452
1453 let real = std::env::temp_dir().join(format!("proc-real-{} (deleted)", std::process::id()));
1455 std::fs::create_dir_all(&real).unwrap();
1456 let path = real.to_string_lossy().to_string();
1457 assert_eq!(strip_deleted_marker(path.clone()), path);
1458 std::fs::remove_dir(&real).unwrap();
1459 }
1460
1461 #[test]
1462 fn a_name_nothing_has_is_plain_not_found_in_both_modes() {
1463 for mode in [NameMatch::Exact, NameMatch::Command] {
1464 let result = Process::find_by_name("nonexistent_process_12345", mode);
1465 assert!(
1466 matches!(&result, Err(ProcError::ProcessNotFound(n)) if n == "nonexistent_process_12345"),
1467 "{result:?}"
1468 );
1469 }
1470 }
1471
1472 #[cfg(unix)]
1476 fn waiting_shell_with(marker: &str) -> std::process::Child {
1477 std::process::Command::new("sh")
1478 .args(["-c", &format!("read line; : {marker}")])
1479 .stdin(std::process::Stdio::piped())
1480 .spawn()
1481 .unwrap()
1482 }
1483
1484 #[cfg(unix)]
1485 #[test]
1486 fn a_word_only_in_a_command_line_is_found_with_cmd_and_hinted_without() {
1487 let marker = format!("hintmarker{}", std::process::id());
1488 let mut child = waiting_shell_with(&marker);
1489 let pid = child.id();
1490 let exact = Process::find_by_name(&marker, NameMatch::Exact);
1491 let cmd = Process::find_by_name(&marker, NameMatch::Command);
1492 let _ = child.kill();
1493 let _ = child.wait();
1494
1495 assert!(
1498 matches!(&exact, Err(ProcError::NameNotFound { name, in_command_line })
1499 if name == &marker && *in_command_line >= 1),
1500 "{exact:?}"
1501 );
1502 let found = cmd.unwrap();
1504 assert_eq!(found.iter().map(|p| p.pid).collect::<Vec<_>>(), vec![pid]);
1505 }
1506
1507 #[cfg(unix)]
1508 #[test]
1509 fn test_find_orphans_returns_ok() {
1510 let result = Process::find_orphans();
1512 assert!(result.is_ok());
1513 }
1514
1515 #[cfg(not(unix))]
1516 #[test]
1517 fn find_orphans_is_not_supported_on_windows() {
1518 let result = Process::find_orphans();
1519 assert!(
1520 matches!(&result, Err(ProcError::NotSupported(m)) if m.starts_with("orphans is not supported on Windows")),
1521 "{result:?}"
1522 );
1523 }
1524
1525 #[cfg(unix)]
1526 #[test]
1527 fn test_is_system_process_system_paths() {
1528 let make_proc = |exe: Option<&str>, cwd: Option<&str>| Process {
1529 pid: 100,
1530 name: "test".to_string(),
1531 exe_path: exe.map(String::from),
1532 cwd: cwd.map(String::from),
1533 command: None,
1534 argv0: None,
1535 raw: None,
1536 cpu_percent: 0.0,
1537 memory_mb: 0.0,
1538 status: ProcessStatus::Running,
1539 user: None,
1540 parent_pid: Some(1),
1541 start_time: None,
1542 uptime_secs: None,
1543 };
1544
1545 assert!(Process::is_system_process(&make_proc(
1547 Some("/usr/sbin/sshd"),
1548 Some("/")
1549 )));
1550 assert!(Process::is_system_process(&make_proc(
1551 Some("/System/Library/foo"),
1552 Some("/")
1553 )));
1554 assert!(Process::is_system_process(&make_proc(
1555 Some("/usr/bin/systemd"),
1556 Some("/")
1557 )));
1558 assert!(Process::is_system_process(&make_proc(
1559 Some("/usr/lib/snapd/snapd"),
1560 Some("/")
1561 )));
1562
1563 assert!(Process::is_system_process(&make_proc(None, Some("/"))));
1565
1566 assert!(Process::is_system_process(&make_proc(
1568 Some("/usr/bin/foo"),
1569 None
1570 )));
1571
1572 for exe in [
1574 "/System/x",
1575 "/usr/libexec/x",
1576 "/usr/sbin/x",
1577 "/sbin/x",
1578 "/usr/bin/x",
1579 "/usr/lib/x",
1580 "/usr/lib64/x",
1581 "/lib/x",
1582 "/lib64/x",
1583 "/opt/x",
1584 "/snap/x",
1585 ] {
1586 assert!(
1587 Process::is_system_process(&make_proc(Some(exe), Some("/"))),
1588 "{exe}"
1589 );
1590 }
1591 assert!(Process::is_system_process(&make_proc(
1593 Some("/srv/x"),
1594 Some("/")
1595 )));
1596
1597 assert!(!Process::is_system_process(&make_proc(
1599 Some("/usr/bin/node"),
1600 Some("/home/user/project")
1601 )));
1602 assert!(!Process::is_system_process(&make_proc(
1603 Some("/home/user/.local/bin/app"),
1604 Some("/home/user")
1605 )));
1606 }
1607
1608 #[cfg(unix)]
1609 #[test]
1610 fn test_parse_signal_name_valid() {
1611 use nix::sys::signal::Signal;
1612
1613 assert_eq!(parse_signal_name("HUP").unwrap(), Signal::SIGHUP);
1614 assert_eq!(parse_signal_name("hup").unwrap(), Signal::SIGHUP);
1615 assert_eq!(parse_signal_name("SIGHUP").unwrap(), Signal::SIGHUP);
1616 assert_eq!(parse_signal_name("sighup").unwrap(), Signal::SIGHUP);
1617 assert_eq!(parse_signal_name("INT").unwrap(), Signal::SIGINT);
1618 assert_eq!(parse_signal_name("QUIT").unwrap(), Signal::SIGQUIT);
1619 assert_eq!(parse_signal_name("ABRT").unwrap(), Signal::SIGABRT);
1620 assert_eq!(parse_signal_name("KILL").unwrap(), Signal::SIGKILL);
1621 assert_eq!(parse_signal_name("TERM").unwrap(), Signal::SIGTERM);
1622 assert_eq!(parse_signal_name("STOP").unwrap(), Signal::SIGSTOP);
1623 assert_eq!(parse_signal_name("CONT").unwrap(), Signal::SIGCONT);
1624 assert_eq!(parse_signal_name("USR1").unwrap(), Signal::SIGUSR1);
1625 assert_eq!(parse_signal_name("USR2").unwrap(), Signal::SIGUSR2);
1626 }
1627
1628 #[cfg(unix)]
1629 #[test]
1630 fn test_parse_signal_name_invalid() {
1631 assert!(parse_signal_name("INVALID").is_err());
1632 assert!(parse_signal_name("FOO").is_err());
1633 assert!(parse_signal_name("").is_err());
1634 }
1635
1636 #[cfg(unix)]
1637 #[test]
1638 fn test_parse_signal_name_case_insensitive() {
1639 use nix::sys::signal::Signal;
1640
1641 assert_eq!(parse_signal_name("term").unwrap(), Signal::SIGTERM);
1642 assert_eq!(parse_signal_name("Term").unwrap(), Signal::SIGTERM);
1643 assert_eq!(parse_signal_name("TERM").unwrap(), Signal::SIGTERM);
1644 assert_eq!(parse_signal_name("sigterm").unwrap(), Signal::SIGTERM);
1645 assert_eq!(parse_signal_name("SigTerm").unwrap(), Signal::SIGTERM);
1646 }
1647
1648 #[cfg(unix)]
1649 #[test]
1650 fn test_send_signal_nonexistent_process() {
1651 use nix::sys::signal::Signal;
1652
1653 let proc = Process {
1655 pid: 99999999,
1656 name: "ghost".to_string(),
1657 exe_path: None,
1658 cwd: None,
1659 command: None,
1660 argv0: None,
1661 raw: None,
1662 cpu_percent: 0.0,
1663 memory_mb: 0.0,
1664 status: ProcessStatus::Running,
1665 user: None,
1666 parent_pid: None,
1667 start_time: None,
1668 uptime_secs: None,
1669 };
1670
1671 let result = proc.send_signal(Signal::SIGCONT);
1672 assert!(result.is_err());
1673 }
1674
1675 fn ghost() -> Process {
1677 Process {
1678 pid: 99999999,
1679 name: "ghost".to_string(),
1680 exe_path: None,
1681 cwd: None,
1682 command: None,
1683 argv0: None,
1684 raw: None,
1685 cpu_percent: 0.0,
1686 memory_mb: 0.0,
1687 status: ProcessStatus::Running,
1688 user: None,
1689 parent_pid: None,
1690 start_time: None,
1691 uptime_secs: None,
1692 }
1693 }
1694
1695 #[test]
1696 fn every_sysinfo_status_maps_to_ours() {
1697 for (sys, ours) in [
1698 (SysProcessStatus::Run, ProcessStatus::Running),
1699 (SysProcessStatus::Sleep, ProcessStatus::Sleeping),
1700 (SysProcessStatus::Stop, ProcessStatus::Stopped),
1701 (SysProcessStatus::Zombie, ProcessStatus::Zombie),
1702 (SysProcessStatus::Dead, ProcessStatus::Dead),
1703 (SysProcessStatus::Idle, ProcessStatus::Unknown),
1704 ] {
1705 assert_eq!(ProcessStatus::from(sys), ours, "{sys:?}");
1706 }
1707 }
1708
1709 #[test]
1710 fn a_process_that_is_gone_does_not_look_stuck() {
1711 assert!(!ghost().looks_stuck_now());
1712 }
1713
1714 #[test]
1715 fn killing_a_process_that_is_already_gone_is_success() {
1716 assert!(matches!(ghost().kill(), Err(ProcError::ProcessNotFound(_))));
1717 ghost().kill_and_wait().unwrap();
1718 }
1719
1720 #[test]
1721 fn a_kill_that_was_not_delivered_is_an_error() {
1722 kill_outcome(true, 7).unwrap();
1723 let err = kill_outcome(false, 7).unwrap_err();
1724 assert!(
1725 matches!(&err, ProcError::SignalError(m) if m == "Failed to kill process 7"),
1726 "{err:?}"
1727 );
1728 }
1729
1730 #[test]
1731 fn taskkill_without_force_reports_what_it_said() {
1732 #[cfg(unix)]
1733 use std::os::unix::process::ExitStatusExt;
1734 #[cfg(windows)]
1735 use std::os::windows::process::ExitStatusExt;
1736 use std::process::{ExitStatus, Output};
1737
1738 let output = |raw, stdout: &str, stderr: &str| Output {
1739 status: ExitStatus::from_raw(raw),
1740 stdout: stdout.as_bytes().to_vec(),
1741 stderr: stderr.as_bytes().to_vec(),
1742 };
1743 taskkill_outcome(&output(0, "SUCCESS: sent termination signal", "")).unwrap();
1744
1745 #[cfg(unix)]
1747 let failed = 1 << 8;
1748 #[cfg(windows)]
1749 let failed = 1;
1750 let err =
1751 taskkill_outcome(&output(failed, "", "ERROR: could not be terminated")).unwrap_err();
1752 assert!(
1753 matches!(&err, ProcError::SignalError(m) if m == "ERROR: could not be terminated"),
1754 "{err:?}"
1755 );
1756 }
1757
1758 #[cfg(unix)]
1759 #[test]
1760 fn orphans_are_the_children_of_init_that_are_not_system_daemons() {
1761 let proc = |pid: u32, ppid: Option<u32>, exe: Option<&str>, cwd: Option<&str>| Process {
1762 pid,
1763 name: format!("p{pid}"),
1764 exe_path: exe.map(String::from),
1765 cwd: cwd.map(String::from),
1766 parent_pid: ppid,
1767 ..ghost()
1768 };
1769 let orphans = Process::orphans_among(vec![
1770 proc(10, Some(1), Some("/home/z/app"), Some("/home/z")),
1772 proc(11, Some(1), Some("/usr/sbin/sshd"), Some("/")),
1774 proc(1, Some(1), Some("/sbin/init"), Some("/home/z")),
1776 proc(12, Some(500), Some("/home/z/app"), Some("/home/z")),
1777 proc(13, None, Some("/home/z/app"), Some("/home/z")),
1779 ]);
1780 let pids: Vec<u32> = orphans.iter().map(|p| p.pid).collect();
1781 assert_eq!(pids, vec![10]);
1782 }
1783
1784 #[cfg(unix)]
1785 #[test]
1786 fn a_missing_lsof_finds_no_open_file_owners() {
1787 let found = Process::find_by_open_file_with("proc-no-such-lsof", std::path::Path::new("/"));
1788 assert!(found.unwrap().is_empty());
1789 }
1790
1791 #[test]
1792 fn a_wait_for_a_process_that_stays_is_a_wait_and_then_an_error() {
1793 let mut child = crate::test_support::idle_child();
1794 let record = crate::test_support::record_of(&child);
1795 let started = std::time::Instant::now();
1796 let result = record.kill_then_wait(Duration::from_millis(300), |_| Ok(()));
1799 let waited = started.elapsed();
1800 let still_there = child.try_wait().unwrap().is_none();
1801 crate::test_support::end(&mut child);
1802 assert!(
1803 matches!(&result, Err(ProcError::SignalError(_))),
1804 "{result:?}"
1805 );
1806 assert!(waited >= Duration::from_millis(300), "{waited:?}");
1807 assert!(still_there);
1808 }
1809
1810 #[test]
1811 fn a_process_still_alive_when_the_wait_ends_is_an_error() {
1812 let mut child = crate::test_support::idle_child();
1813 let record = crate::test_support::record_of(&child);
1814 let result = record.kill_and_wait_within(Duration::ZERO);
1816 let _ = child.kill();
1817 let _ = child.wait();
1818 assert!(
1819 matches!(&result, Err(ProcError::SignalError(m))
1820 if m == &format!("Process {} is still running 0 seconds after SIGKILL", record.pid)),
1821 "{result:?}"
1822 );
1823 }
1824
1825 #[cfg(target_os = "macos")]
1827 fn kinfo(pid: u32, stat: u8, comm: &str, ruid: u32, ppid: u32, start: u64) -> Vec<u8> {
1828 let mut rec = vec![0u8; KINFO_PROC_SIZE];
1829 rec[0..8].copy_from_slice(&start.to_ne_bytes());
1830 rec[36] = stat;
1831 rec[40..44].copy_from_slice(&pid.to_ne_bytes());
1832 rec[243..243 + comm.len()].copy_from_slice(comm.as_bytes());
1833 rec[392..396].copy_from_slice(&ruid.to_ne_bytes());
1834 rec[560..564].copy_from_slice(&ppid.to_ne_bytes());
1835 rec
1836 }
1837
1838 #[cfg(target_os = "macos")]
1839 #[test]
1840 fn zombies_are_read_from_the_kernels_table() {
1841 let known = std::collections::HashSet::from([30]);
1842 let table: Vec<u8> = [
1843 kinfo(10, 5, "defunct-app", 0, 99, 1_700_000_000),
1844 kinfo(11, 5, "", 0, 1, 0),
1846 kinfo(20, 2, "alive", 0, 1, 1_700_000_000),
1848 kinfo(30, 5, "known", 0, 1, 1_700_000_000),
1850 kinfo(0, 5, "kernel", 0, 0, 0),
1852 ]
1853 .concat();
1854
1855 let zombies = zombies_in(&table, &known);
1856 let pids: Vec<u32> = zombies.iter().map(|z| z.pid).collect();
1857 assert_eq!(pids, vec![10, 11]);
1858
1859 let named = &zombies[0];
1860 assert_eq!(named.name, "defunct-app");
1861 assert_eq!(named.status, ProcessStatus::Zombie);
1862 assert_eq!(named.parent_pid, Some(99));
1863 assert_eq!(named.start_time, Some(1_700_000_000));
1864 assert!(named.uptime_secs.unwrap() > 0);
1865 assert_eq!(named.user.as_deref(), Some("root"));
1866
1867 let unnamed = &zombies[1];
1868 assert_eq!(unnamed.name, "<defunct>");
1869 assert_eq!(unnamed.start_time, None);
1870 assert_eq!(unnamed.uptime_secs, None);
1871 }
1872
1873 #[cfg(target_os = "macos")]
1874 #[test]
1875 fn a_table_of_an_unknown_layout_shows_no_zombies() {
1876 let known = std::collections::HashSet::new();
1877 let mut table = kinfo(10, 5, "z", 0, 1, 0);
1878 table.push(0);
1879 assert!(zombies_in(&table, &known).is_empty());
1880 }
1881
1882 #[cfg(target_os = "macos")]
1883 #[test]
1884 fn a_refused_sysctl_gives_an_empty_table() {
1885 use std::cell::Cell;
1886
1887 assert!(read_proc_table(|_, _| -1).is_empty());
1889
1890 let calls = Cell::new(0);
1892 let table = read_proc_table(|_, size| {
1893 calls.set(calls.get() + 1);
1894 if calls.get() == 1 {
1895 *size = 2 * KINFO_PROC_SIZE;
1896 0
1897 } else {
1898 -1
1899 }
1900 });
1901 assert_eq!(calls.get(), 2);
1902 assert!(table.is_empty());
1903
1904 let calls = Cell::new(0);
1906 let table = read_proc_table(|buf, size| {
1907 calls.set(calls.get() + 1);
1908 if calls.get() == 1 {
1909 *size = 2 * KINFO_PROC_SIZE;
1910 } else {
1911 assert_eq!(*size, 66 * KINFO_PROC_SIZE, "room for 64 newcomers");
1912 unsafe { buf.cast::<u8>().write(7) };
1914 *size = KINFO_PROC_SIZE;
1915 }
1916 0
1917 });
1918 assert_eq!(table.len(), KINFO_PROC_SIZE);
1919 assert_eq!(table[0], 7);
1920 }
1921}