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;
13
14pub(crate) fn sysinfo_locked<T>(refresh: impl FnOnce() -> T) -> T {
26 #[cfg(target_os = "freebsd")]
27 {
28 static LOCK: std::sync::Mutex<()> = std::sync::Mutex::new(());
29 let _held = LOCK.lock().unwrap_or_else(|poisoned| poisoned.into_inner());
30 let _quiet = QuietStderr::new();
31 refresh()
32 }
33 #[cfg(not(target_os = "freebsd"))]
34 refresh()
35}
36
37#[cfg(target_os = "freebsd")]
40struct QuietStderr {
41 saved: Option<libc::c_int>,
42}
43
44#[cfg(target_os = "freebsd")]
45impl QuietStderr {
46 fn new() -> Self {
47 unsafe {
50 let saved = libc::dup(libc::STDERR_FILENO);
51 if saved < 0 {
52 return QuietStderr { saved: None };
53 }
54 let null = libc::open(c"/dev/null".as_ptr(), libc::O_WRONLY | libc::O_CLOEXEC);
55 if null < 0 {
56 libc::close(saved);
57 return QuietStderr { saved: None };
58 }
59 libc::dup2(null, libc::STDERR_FILENO);
60 libc::close(null);
61 QuietStderr { saved: Some(saved) }
62 }
63 }
64}
65
66#[cfg(target_os = "freebsd")]
67impl Drop for QuietStderr {
68 fn drop(&mut self) {
69 if let Some(saved) = self.saved {
70 unsafe {
72 libc::dup2(saved, libc::STDERR_FILENO);
73 libc::close(saved);
74 }
75 }
76 }
77}
78
79#[cfg(any(target_os = "freebsd", target_os = "netbsd", test))]
84pub(crate) fn names_by_pid(pids: &[u32]) -> std::collections::HashMap<u32, String> {
85 let wanted: Vec<Pid> = pids.iter().map(|&pid| Pid::from_u32(pid)).collect();
86 let mut sys = System::new();
87 sysinfo_locked(|| sys.refresh_processes(sysinfo::ProcessesToUpdate::Some(&wanted), true));
88 sys.processes()
89 .iter()
90 .map(|(pid, proc)| (pid.as_u32(), Process::from_sysinfo(*pid, proc).name))
91 .collect()
92}
93
94pub(crate) fn refreshed_system() -> System {
97 sysinfo_locked(|| {
98 let mut sys = System::new_all();
99 sys.refresh_all();
100 sys
101 })
102}
103use sysinfo::{Pid, ProcessStatus as SysProcessStatus, System, Uid};
104
105#[cfg(unix)]
112fn user_name(uid: &Uid) -> String {
113 use std::collections::HashMap;
114 use std::sync::Mutex;
115
116 static NAMES: OnceLock<Mutex<HashMap<u32, String>>> = OnceLock::new();
117 let id: u32 = **uid;
118 let mut names = NAMES.get_or_init(Default::default).lock().unwrap();
119 names
120 .entry(id)
121 .or_insert_with(|| {
122 nix::unistd::User::from_uid(nix::unistd::Uid::from_raw(id))
123 .ok()
124 .flatten()
125 .map(|user| user.name)
126 .unwrap_or_else(|| id.to_string())
127 })
128 .clone()
129}
130
131#[cfg(windows)]
134fn user_name(uid: &Uid) -> String {
135 static USERS: OnceLock<Users> = OnceLock::new();
136 USERS
137 .get_or_init(Users::new_with_refreshed_list)
138 .get_user_by_id(uid)
139 .map(|user| user.name().to_string())
140 .unwrap_or_else(|| uid.to_string())
141}
142
143pub fn escape_control(text: &str) -> std::borrow::Cow<'_, str> {
155 fn needs_escape(c: char) -> bool {
156 c.is_control()
157 || matches!(c, '\u{200e}' | '\u{200f}' | '\u{202a}'..='\u{202e}' | '\u{2066}'..='\u{2069}')
158 }
159 if !text.chars().any(needs_escape) {
160 return std::borrow::Cow::Borrowed(text);
161 }
162 let mut out = String::with_capacity(text.len() + 8);
163 for c in text.chars() {
164 match c {
165 '\n' => out.push_str("\\n"),
166 '\r' => out.push_str("\\r"),
167 '\t' => out.push_str("\\t"),
168 c if c.is_control() => out.push_str(&format!("\\x{:02x}", c as u32)),
169 c if needs_escape(c) => out.push_str(&format!("\\u{{{:x}}}", c as u32)),
170 c => out.push(c),
171 }
172 }
173 std::borrow::Cow::Owned(out)
174}
175
176const STUCK_READINGS: usize = 2;
179const STUCK_READING_GAP: Duration = Duration::from_secs(1);
180
181fn looks_stuck(cpu_percent: f32, status: SysProcessStatus) -> bool {
184 cpu_percent > 50.0 || status == SysProcessStatus::UninterruptibleDiskSleep
185}
186
187fn cpu_percent_between(used_before_ms: u64, used_after_ms: u64, wall: Duration) -> f32 {
193 let wall_ms = wall.as_secs_f64() * 1000.0;
194 if wall_ms <= 0.0 {
195 return 0.0;
196 }
197 (used_after_ms.saturating_sub(used_before_ms) as f64 / wall_ms * 100.0) as f32
198}
199
200#[cfg(not(unix))]
202pub(crate) fn orphans_not_supported() -> ProcError {
203 ProcError::NotSupported(
204 "orphans is not supported on Windows: Windows doesn't re-parent processes, \
205 so there is no reliable way to tell an orphan"
206 .to_string(),
207 )
208}
209
210#[cfg(any(windows, test))]
214fn command_failure(stdout: &[u8], stderr: &[u8], status: &str) -> String {
215 [stderr, stdout]
216 .iter()
217 .map(|out| {
218 String::from_utf8_lossy(out)
220 .lines()
221 .map(str::trim)
222 .filter(|line| !line.is_empty())
223 .collect::<Vec<_>>()
224 .join(" ")
225 })
226 .find(|text| !text.is_empty())
227 .unwrap_or_else(|| format!("exited with {status}"))
228}
229
230fn kill_outcome(delivered: bool, pid: u32) -> Result<()> {
233 if delivered {
234 Ok(())
235 } else {
236 Err(ProcError::SignalError(format!(
237 "Failed to kill process {}",
238 pid
239 )))
240 }
241}
242
243fn same_start(recorded: Option<u64>, current: u64) -> bool {
251 match recorded {
252 Some(ours) => current > 0 && current.abs_diff(ours) <= 1,
253 None => true,
254 }
255}
256
257#[cfg(any(target_os = "freebsd", target_os = "netbsd"))]
259pub(crate) const OPEN_FILE_PROGRAM: &str = "fstat";
260#[cfg(all(unix, not(any(target_os = "freebsd", target_os = "netbsd"))))]
261pub(crate) const OPEN_FILE_PROGRAM: &str = "lsof";
262
263#[cfg(any(all(unix, not(any(target_os = "freebsd", target_os = "netbsd"))), test))]
265fn lsof_pids(stdout: &str) -> Vec<u32> {
266 stdout
267 .lines()
268 .filter_map(|line| line.trim().parse().ok())
269 .collect()
270}
271
272#[cfg(any(target_os = "freebsd", target_os = "netbsd", test))]
283fn fstat_pids(stdout: &str) -> Vec<u32> {
284 let mut pids: Vec<u32> = stdout
285 .lines()
286 .filter_map(|line| {
287 let fields: Vec<&str> = line.split_whitespace().collect();
288 let mount = fields.iter().position(|field| field.starts_with('/'))?;
289 let pid = fields.get(mount.checked_sub(2).filter(|&at| at >= 2)?)?;
291 pid.bytes()
292 .all(|b| b.is_ascii_digit())
293 .then(|| pid.parse().ok())?
294 })
295 .collect();
296 pids.sort_unstable();
297 pids.dedup();
298 pids
299}
300
301const START_TIME_READS: u32 = 10;
303
304const START_TIME_READ_GAP: Duration = Duration::from_millis(2);
306
307fn read_until_started(wants_start: bool, mut read: impl FnMut() -> Option<u64>) {
319 let mut reads = 1;
320 while read() == Some(0) && wants_start && reads < START_TIME_READS {
321 std::thread::sleep(START_TIME_READ_GAP);
322 reads += 1;
323 }
324}
325
326#[cfg(any(windows, test))]
329fn taskkill_outcome(output: &std::process::Output) -> Result<()> {
330 if output.status.success() {
331 Ok(())
332 } else {
333 Err(ProcError::SignalError(command_failure(
334 &output.stdout,
335 &output.stderr,
336 &output.status.to_string(),
337 )))
338 }
339}
340
341fn cpu_used(sys: &System) -> std::collections::HashMap<Pid, u64> {
343 sys.processes()
344 .iter()
345 .map(|(pid, p)| (*pid, cpu_time_ms(*pid, p)))
346 .collect()
347}
348
349fn cpu_time_ms(pid: Pid, process: &sysinfo::Process) -> u64 {
355 #[cfg(target_os = "netbsd")]
356 {
357 let _ = process;
358 netbsd_cpu_time_ms(pid.as_u32()).unwrap_or(0)
359 }
360 #[cfg(not(target_os = "netbsd"))]
361 {
362 let _ = pid;
363 process.accumulated_cpu_time()
364 }
365}
366
367#[cfg(target_os = "netbsd")]
372fn netbsd_cpu_time_ms(pid: u32) -> Option<u64> {
373 let mut last = None;
374 read_until_started(true, || {
375 last = netbsd_kinfo(pid);
376 last.map(|info| u64::from(info.p_ustart_sec))
377 });
378 last.filter(|info| info.p_uvalid != 0)
379 .map(|info| u64::from(info.p_rtime_sec) * 1000 + u64::from(info.p_rtime_usec) / 1000)
380}
381
382#[cfg(target_os = "netbsd")]
384fn netbsd_kinfo(pid: u32) -> Option<libc::kinfo_proc2> {
385 let size = std::mem::size_of::<libc::kinfo_proc2>();
386 let mib = [
387 libc::CTL_KERN,
388 libc::KERN_PROC2,
389 libc::KERN_PROC_PID,
390 pid as libc::c_int,
391 size as libc::c_int,
392 1,
393 ];
394 let mut info: libc::kinfo_proc2 = unsafe { std::mem::zeroed() };
396 let mut len = size;
397 let read = unsafe {
400 libc::sysctl(
401 mib.as_ptr(),
402 mib.len() as libc::c_uint,
403 (&mut info as *mut libc::kinfo_proc2).cast(),
404 &mut len,
405 std::ptr::null(),
406 0,
407 )
408 };
409 (read == 0 && len == size).then_some(info)
410}
411
412fn ran_long_enough(run_time: u64, timeout_secs: u64) -> bool {
416 run_time >= timeout_secs
417}
418
419fn counts_as_stuck(is_thread: bool, run_time: u64, timeout_secs: u64, looks_stuck: bool) -> bool {
423 !is_thread && ran_long_enough(run_time, timeout_secs) && looks_stuck
424}
425
426#[cfg(target_os = "macos")]
430fn macos_zombies(known: &std::collections::HashSet<u32>) -> Vec<Process> {
431 let table = read_proc_table(|buf, size| {
432 let mut mib = [libc::CTL_KERN, libc::KERN_PROC, libc::KERN_PROC_ALL, 0];
433 let null = std::ptr::null_mut();
434 unsafe { libc::sysctl(mib.as_mut_ptr(), 4, buf, size, null, 0) }
437 });
438 zombies_in(&table, known)
439}
440
441#[cfg(target_os = "macos")]
444const KINFO_PROC_SIZE: usize = 648;
445
446#[cfg(target_os = "macos")]
450fn read_proc_table(
451 sysctl: impl Fn(*mut libc::c_void, &mut libc::size_t) -> libc::c_int,
452) -> Vec<u8> {
453 let mut size: libc::size_t = 0;
454 if sysctl(std::ptr::null_mut(), &mut size) != 0 {
456 return Vec::new();
457 }
458 size += 64 * KINFO_PROC_SIZE;
460 let mut buf: Vec<u8> = vec![0; size];
461 if sysctl(buf.as_mut_ptr().cast(), &mut size) != 0 {
462 return Vec::new();
463 }
464 buf.truncate(size);
465 buf
466}
467
468#[cfg(target_os = "macos")]
470fn zombies_in(buf: &[u8], known: &std::collections::HashSet<u32>) -> Vec<Process> {
471 const P_STARTTIME_SEC: usize = 0;
472 const P_STAT: usize = 36;
473 const P_PID: usize = 40;
474 const P_COMM: usize = 243; const P_RUID: usize = 392;
476 const E_PPID: usize = 560;
477 const SZOMB: u8 = 5;
478
479 if !buf.len().is_multiple_of(KINFO_PROC_SIZE) {
480 return Vec::new(); }
482
483 let read_u32 = |p: &[u8], at: usize| u32::from_ne_bytes(p[at..at + 4].try_into().unwrap());
484 let now = std::time::SystemTime::now()
485 .duration_since(std::time::UNIX_EPOCH)
486 .map(|d| d.as_secs())
487 .unwrap_or(0);
488
489 buf.as_chunks::<KINFO_PROC_SIZE>()
490 .0
491 .iter()
492 .filter(|p| p[P_STAT] == SZOMB)
493 .filter_map(|p| {
494 let pid = read_u32(p, P_PID);
495 if pid == 0 || known.contains(&pid) {
496 return None;
497 }
498 let comm = &p[P_COMM..P_COMM + 17];
499 let comm = &comm[..comm.iter().position(|&c| c == 0).unwrap_or(comm.len())];
500 let name = escape_control(&String::from_utf8_lossy(comm)).into_owned();
501 let start =
502 u64::from_ne_bytes(p[P_STARTTIME_SEC..P_STARTTIME_SEC + 8].try_into().unwrap());
503 Some(Process {
504 pid,
505 name: if name.is_empty() {
506 "<defunct>".to_string()
507 } else {
508 name
509 },
510 exe_path: None,
511 cwd: None,
512 command: None,
513 argv0: None,
514 raw: None,
515 cpu_percent: 0.0,
516 memory_mb: 0.0,
517 status: ProcessStatus::Zombie,
518 user: Uid::try_from(read_u32(p, P_RUID) as usize)
519 .ok()
520 .map(|uid| escape_control(&user_name(&uid)).into_owned()),
521 parent_pid: Some(read_u32(p, E_PPID)),
522 start_time: (start > 0).then_some(start),
523 uptime_secs: (start > 0).then(|| now.saturating_sub(start)),
524 })
525 })
526 .collect()
527}
528
529pub fn is_thread(proc: &sysinfo::Process) -> bool {
533 proc.thread_kind().is_some()
534}
535
536fn strip_deleted_marker(cwd: String) -> String {
540 match cwd.strip_suffix(" (deleted)") {
541 Some(path) if !std::path::Path::new(&cwd).exists() => path.to_string(),
542 _ => cwd,
543 }
544}
545
546#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
548#[serde(rename_all = "lowercase")]
549pub enum ProcessStatus {
550 Running,
552 Sleeping,
554 Stopped,
556 Zombie,
558 Dead,
560 Unknown,
562}
563
564impl From<SysProcessStatus> for ProcessStatus {
565 fn from(status: SysProcessStatus) -> Self {
566 match status {
567 SysProcessStatus::Run => ProcessStatus::Running,
568 SysProcessStatus::Sleep => ProcessStatus::Sleeping,
569 SysProcessStatus::Stop => ProcessStatus::Stopped,
570 SysProcessStatus::Zombie => ProcessStatus::Zombie,
571 SysProcessStatus::Dead => ProcessStatus::Dead,
572 _ => ProcessStatus::Unknown,
573 }
574 }
575}
576
577#[derive(Debug, Clone, Serialize, Deserialize)]
579pub struct Process {
580 pub pid: u32,
582 pub name: String,
584 #[serde(skip_serializing_if = "Option::is_none")]
586 pub exe_path: Option<String>,
587 #[serde(skip_serializing_if = "Option::is_none")]
589 pub cwd: Option<String>,
590 #[serde(skip_serializing_if = "Option::is_none")]
592 pub command: Option<String>,
593 #[serde(skip)]
597 pub argv0: Option<String>,
598 #[serde(skip)]
604 pub raw: Option<Box<RawText>>,
605 pub cpu_percent: f32,
607 pub memory_mb: f64,
609 pub status: ProcessStatus,
611 #[serde(skip_serializing_if = "Option::is_none")]
613 pub user: Option<String>,
614 #[serde(skip_serializing_if = "Option::is_none")]
616 pub parent_pid: Option<u32>,
617 #[serde(skip_serializing_if = "Option::is_none")]
619 pub start_time: Option<u64>,
620 #[serde(skip_serializing_if = "Option::is_none")]
622 pub uptime_secs: Option<u64>,
623}
624
625pub(crate) fn self_and_ancestors(sys: &System) -> std::collections::HashSet<Pid> {
629 let self_pid = Pid::from_u32(std::process::id());
630 let mut pids = std::collections::HashSet::new();
631 pids.insert(self_pid);
632 let mut current = self_pid;
633 for _ in 0..10 {
634 if let Some(parent) = sys.process(current).and_then(|p| p.parent()) {
635 pids.insert(parent);
636 current = parent;
637 } else {
638 break;
639 }
640 }
641 pids
642}
643
644#[derive(Debug, Clone)]
647pub struct RawText {
648 pub name: String,
650 pub exe_path: Option<String>,
652 pub command: Option<String>,
654 pub cwd: Option<String>,
656}
657
658pub(crate) fn classify_sysinfo(pattern: &str, proc: &sysinfo::Process, mode: NameMatch) -> Hit {
662 let name = proc.name().to_string_lossy();
663 let exe = proc.exe().map(|exe| exe.to_string_lossy());
664 let argv0 = proc.cmd().first().map(|arg| arg.to_string_lossy());
665 let command = (!proc.cmd().is_empty()).then(|| {
667 proc.cmd()
668 .iter()
669 .map(|arg| arg.to_string_lossy())
670 .collect::<Vec<_>>()
671 .join(" ")
672 });
673 classify(
674 pattern,
675 &Names {
676 name: &name,
677 exe: exe.as_deref(),
678 argv0: argv0.as_deref(),
679 command: command.as_deref(),
680 },
681 mode,
682 )
683}
684
685impl Process {
686 pub fn names(&self) -> Names<'_> {
688 match &self.raw {
689 Some(raw) => Names {
690 name: &raw.name,
691 exe: raw.exe_path.as_deref(),
692 argv0: self.argv0.as_deref(),
693 command: raw.command.as_deref(),
694 },
695 None => Names {
696 name: &self.name,
697 exe: self.exe_path.as_deref(),
698 argv0: self.argv0.as_deref(),
699 command: self.command.as_deref(),
700 },
701 }
702 }
703
704 pub fn raw_cwd(&self) -> Option<&str> {
709 match &self.raw {
710 Some(raw) => raw.cwd.as_deref(),
711 None => self.cwd.as_deref(),
712 }
713 }
714
715 pub fn find_by_name(pattern: &str, mode: NameMatch) -> Result<Vec<Process>> {
722 let sys = refreshed_system();
723
724 let ancestor_pids = self_and_ancestors(&sys);
727 let mut in_command_line_only = 0;
728 let mut processes: Vec<Process> = sys
729 .processes()
730 .iter()
731 .filter_map(|(pid, proc)| {
732 if ancestor_pids.contains(pid) || is_thread(proc) {
734 return None;
735 }
736 match classify_sysinfo(pattern, proc, mode) {
737 Hit::Match => Some(Process::from_sysinfo(*pid, proc)),
738 Hit::CommandLineOnly => {
739 in_command_line_only += 1;
740 None
741 }
742 Hit::No => None,
743 }
744 })
745 .collect();
746 processes.sort_by_key(|p| p.pid);
749
750 if processes.is_empty() {
751 return Err(ProcError::name_not_found(pattern, in_command_line_only));
752 }
753
754 Ok(processes)
755 }
756
757 pub fn find_by_pid(pid: u32) -> Result<Option<Process>> {
759 let sys = refreshed_system();
760
761 let sysinfo_pid = Pid::from_u32(pid);
762
763 Ok(sys
764 .processes()
765 .get(&sysinfo_pid)
766 .map(|proc| Process::from_sysinfo(sysinfo_pid, proc)))
767 }
768
769 pub fn find_all() -> Result<Vec<Process>> {
771 let sys = refreshed_system();
772
773 let self_pid = sysinfo::Pid::from_u32(std::process::id());
774 #[allow(unused_mut)]
775 let mut processes: Vec<Process> = sys
776 .processes()
777 .iter()
778 .filter(|(pid, proc)| **pid != self_pid && !is_thread(proc))
779 .map(|(pid, proc)| Process::from_sysinfo(*pid, proc))
780 .collect();
781
782 #[cfg(target_os = "macos")]
783 {
784 let known: std::collections::HashSet<u32> = processes.iter().map(|p| p.pid).collect();
785 processes.extend(macos_zombies(&known));
786 }
787
788 processes.sort_by_key(|p| p.pid);
792
793 Ok(processes)
794 }
795
796 pub fn find_by_exe_path(path: &std::path::Path) -> Result<Vec<Process>> {
798 let all = Self::find_all()?;
799 let path_str = path.to_string_lossy();
800
801 Ok(all
802 .into_iter()
803 .filter(|p| {
804 if let Some(ref exe) = p.exe_path {
805 exe == &*path_str || std::path::Path::new(exe) == path
807 } else {
808 false
809 }
810 })
811 .collect())
812 }
813
814 #[cfg(unix)]
819 pub fn find_by_open_file(path: &std::path::Path) -> Result<Vec<Process>> {
820 Self::find_by_open_file_with(OPEN_FILE_PROGRAM, path)
821 }
822
823 #[cfg(unix)]
825 fn find_by_open_file_with(program: &str, path: &std::path::Path) -> Result<Vec<Process>> {
826 use std::process::Command;
827
828 let mut command = Command::new(program);
829 #[cfg(not(any(target_os = "freebsd", target_os = "netbsd")))]
831 command.arg("-t");
832 let output = crate::ui::interrupt::unblocked_in_child(&mut command)
833 .arg(&*path.to_string_lossy())
834 .output();
835
836 let output = match output {
837 Ok(o) => o,
838 Err(_) => return Ok(vec![]), };
840
841 if !output.status.success() {
842 return Ok(vec![]); }
844
845 let stdout = String::from_utf8_lossy(&output.stdout);
846 #[cfg(any(target_os = "freebsd", target_os = "netbsd"))]
847 let pids = fstat_pids(&stdout);
848 #[cfg(not(any(target_os = "freebsd", target_os = "netbsd")))]
849 let pids = lsof_pids(&stdout);
850
851 let mut processes = Vec::new();
852 for pid in pids {
853 if let Ok(Some(proc)) = Self::find_by_pid(pid) {
854 processes.push(proc);
855 }
856 }
857
858 Ok(processes)
859 }
860
861 #[cfg(not(unix))]
863 pub fn find_by_open_file(_path: &std::path::Path) -> Result<Vec<Process>> {
864 Ok(vec![])
866 }
867
868 pub fn find_stuck(timeout: Duration) -> Result<Vec<Process>> {
874 let timeout_secs = timeout.as_secs();
875 let mut read_at = std::time::Instant::now();
877 let mut sys = sysinfo_locked(System::new_all);
878 let mut used = cpu_used(&sys);
879 let mut cpu: std::collections::HashMap<Pid, f32> = std::collections::HashMap::new();
880 let mut stuck: Option<std::collections::HashSet<Pid>> = None;
881
882 for _ in 0..STUCK_READINGS {
883 std::thread::sleep(STUCK_READING_GAP);
884 let started = std::time::Instant::now();
885 sysinfo_locked(|| sys.refresh_processes(sysinfo::ProcessesToUpdate::All, true));
886 let wall = started - read_at;
887 read_at = started;
888 cpu = sys
890 .processes()
891 .iter()
892 .filter_map(|(pid, p)| {
893 let before = used.get(pid)?;
894 Some((
895 *pid,
896 cpu_percent_between(*before, cpu_time_ms(*pid, p), wall),
897 ))
898 })
899 .collect();
900 used = cpu_used(&sys);
901 let now: std::collections::HashSet<Pid> = sys
902 .processes()
903 .iter()
904 .filter(|(pid, p)| {
905 let busy = cpu.get(pid).copied().unwrap_or(0.0);
906 counts_as_stuck(
907 is_thread(p),
908 p.run_time(),
909 timeout_secs,
910 looks_stuck(busy, p.status()),
911 )
912 })
913 .map(|(pid, _)| *pid)
914 .collect();
915 stuck = Some(match stuck {
916 None => now,
917 Some(before) => before.intersection(&now).copied().collect(),
918 });
919 }
920
921 let stuck = stuck.unwrap_or_default();
922 let mut found: Vec<Process> = sys
923 .processes()
924 .iter()
925 .filter(|(pid, _)| stuck.contains(pid))
926 .map(|(pid, p)| {
927 let mut process = Process::from_sysinfo(*pid, p);
928 if let Some(&busy) = cpu.get(pid) {
930 process.cpu_percent = busy;
931 }
932 process
933 })
934 .collect();
935 found.sort_by_key(|p| p.pid);
937 Ok(found)
938 }
939
940 pub fn looks_stuck_now(&self) -> bool {
943 let pid = Pid::from_u32(self.pid);
944 let mut sys = System::new();
945 let mut read_at = std::time::Instant::now();
946 self.refresh_for_check(&mut sys);
947 let mut used = sys
948 .process(pid)
949 .filter(|p| self.is_same(p))
950 .map(|p| cpu_time_ms(pid, p));
951 (0..STUCK_READINGS).all(|_| {
952 std::thread::sleep(STUCK_READING_GAP);
953 let started = std::time::Instant::now();
954 self.refresh_for_check(&mut sys);
955 let wall = started - read_at;
956 read_at = started;
957 let (Some(before), Some(p)) = (used, sys.process(pid).filter(|p| self.is_same(p)))
958 else {
959 return false;
960 };
961 let after = cpu_time_ms(pid, p);
962 used = Some(after);
963 looks_stuck(cpu_percent_between(before, after, wall), p.status())
964 })
965 }
966
967 #[cfg(unix)]
974 pub fn cpu_over(&self, window: Duration) -> Option<f32> {
975 let pid = Pid::from_u32(self.pid);
976 let mut sys = System::new();
977 let started = std::time::Instant::now();
978 self.refresh_for_check(&mut sys);
979 let before = sys
980 .process(pid)
981 .filter(|p| self.is_same(p))
982 .map(|p| cpu_time_ms(pid, p))?;
983 std::thread::sleep(window.max(sysinfo::MINIMUM_CPU_UPDATE_INTERVAL));
984 let wall = started.elapsed();
985 self.refresh_for_check(&mut sys);
986 sys.process(pid)
987 .filter(|p| self.is_same(p))
988 .map(|p| cpu_percent_between(before, cpu_time_ms(pid, p), wall))
989 }
990
991 pub fn measure_cpu(processes: &mut [Process]) {
1002 if processes.is_empty() {
1003 return;
1004 }
1005 let pids: Vec<Pid> = processes.iter().map(|p| Pid::from_u32(p.pid)).collect();
1006 let which = sysinfo::ProcessesToUpdate::Some(&pids);
1007 let cpu_only = sysinfo::ProcessRefreshKind::nothing().with_cpu();
1008 let mut sys = System::new();
1009
1010 let first = std::time::Instant::now();
1011 sysinfo_locked(|| sys.refresh_processes_specifics(which, true, cpu_only));
1012 let before = cpu_used(&sys);
1013 std::thread::sleep(sysinfo::MINIMUM_CPU_UPDATE_INTERVAL);
1014 let wall = first.elapsed();
1017 sysinfo_locked(|| sys.refresh_processes_specifics(which, true, cpu_only));
1018
1019 for process in processes.iter_mut() {
1020 let pid = Pid::from_u32(process.pid);
1021 let (Some(current), Some(&cpu_before)) = (sys.process(pid), before.get(&pid)) else {
1022 continue;
1023 };
1024 if process.is_same(current) {
1025 process.cpu_percent =
1026 cpu_percent_between(cpu_before, cpu_time_ms(pid, current), wall);
1027 }
1028 }
1029 }
1030
1031 pub fn kill(&self) -> Result<()> {
1033 let mut sys = System::new();
1034 self.refresh_for_check(&mut sys);
1035
1036 match sys.process(Pid::from_u32(self.pid)) {
1037 Some(proc) if self.is_same(proc) => kill_outcome(proc.kill(), self.pid),
1038 _ => Err(ProcError::ProcessNotFound(self.pid.to_string())),
1040 }
1041 }
1042
1043 fn refresh_for_check(&self, sys: &mut System) {
1046 let pid = Pid::from_u32(self.pid);
1047 read_until_started(self.start_time.is_some(), || {
1048 sysinfo_locked(|| {
1049 sys.refresh_processes(sysinfo::ProcessesToUpdate::Some(&[pid]), true)
1050 });
1051 sys.process(pid).map(|p| p.start_time())
1052 });
1053 }
1054
1055 fn is_same(&self, current: &sysinfo::Process) -> bool {
1058 same_start(self.start_time, current.start_time())
1059 }
1060
1061 fn ensure_same_process(&self) -> Result<()> {
1065 let mut sys = System::new();
1066 let pid = Pid::from_u32(self.pid);
1067 self.refresh_for_check(&mut sys);
1068 match sys.process(pid) {
1069 Some(current) if self.is_same(current) => Ok(()),
1070 _ => Err(ProcError::ProcessNotFound(self.pid.to_string())),
1071 }
1072 }
1073
1074 pub fn kill_and_wait(&self) -> Result<()> {
1080 self.kill_and_wait_within(Duration::from_secs(5))
1081 }
1082
1083 fn kill_and_wait_within(&self, wait: Duration) -> Result<()> {
1085 self.kill_then_wait(wait, Process::kill)
1086 }
1087
1088 fn kill_then_wait(&self, wait: Duration, kill: impl Fn(&Process) -> Result<()>) -> Result<()> {
1092 match kill(self) {
1093 Err(ProcError::ProcessNotFound(_)) => return Ok(()),
1095 other => other?,
1096 }
1097 let deadline = std::time::Instant::now() + wait;
1098 while std::time::Instant::now() < deadline {
1099 if !self.is_running() {
1100 return Ok(());
1101 }
1102 std::thread::sleep(Duration::from_millis(50));
1103 }
1104 Err(ProcError::SignalError(format!(
1105 "Process {} is still running {} seconds after SIGKILL",
1106 self.pid,
1107 wait.as_secs()
1108 )))
1109 }
1110
1111 #[cfg(unix)]
1113 pub fn terminate(&self) -> Result<()> {
1114 use nix::sys::signal::{kill, Signal};
1115 use nix::unistd::Pid as NixPid;
1116
1117 self.ensure_same_process()?;
1118 kill(NixPid::from_raw(self.pid as i32), Signal::SIGTERM)
1119 .map_err(|e| ProcError::SignalError(e.to_string()))
1120 }
1121
1122 #[cfg(windows)]
1127 pub fn terminate(&self) -> Result<()> {
1128 use std::process::Command;
1129
1130 self.ensure_same_process()?;
1131 let output = Command::new("taskkill")
1132 .args(["/PID", &self.pid.to_string()])
1133 .output()
1134 .map_err(|e| ProcError::SystemError(e.to_string()))?;
1135
1136 taskkill_outcome(&output)
1137 }
1138
1139 #[cfg(unix)]
1141 pub fn send_signal(&self, signal: nix::sys::signal::Signal) -> Result<()> {
1142 use nix::sys::signal::kill;
1143 use nix::unistd::Pid as NixPid;
1144 self.ensure_same_process()?;
1145 kill(NixPid::from_raw(self.pid as i32), signal)
1146 .map_err(|e| ProcError::SignalError(format!("{}: {}", signal, e)))
1147 }
1148
1149 #[cfg(unix)]
1158 pub fn find_orphans() -> Result<Vec<Process>> {
1159 Ok(Self::orphans_among(Self::find_all()?))
1160 }
1161
1162 #[cfg(not(unix))]
1164 pub fn find_orphans() -> Result<Vec<Process>> {
1165 Err(orphans_not_supported())
1166 }
1167
1168 #[cfg(unix)]
1170 fn orphans_among(all: Vec<Process>) -> Vec<Process> {
1171 all.into_iter()
1172 .filter(|p| {
1173 if let Some(ppid) = p.parent_pid {
1174 ppid == 1 && p.pid != 1 && !Self::is_system_process(p)
1175 } else {
1176 false
1177 }
1178 })
1179 .collect()
1180 }
1181
1182 #[cfg(unix)]
1188 fn is_system_process(p: &Process) -> bool {
1189 if p.cwd.is_none() || p.cwd.as_deref() == Some("/") {
1190 if let Some(ref exe) = p.exe_path {
1191 if exe.starts_with("/System/") || exe.starts_with("/usr/libexec/") {
1193 return true;
1194 }
1195 if exe.starts_with("/usr/sbin/")
1197 || exe.starts_with("/sbin/")
1198 || exe.starts_with("/usr/bin/")
1199 || exe.starts_with("/usr/lib/")
1200 || exe.starts_with("/usr/lib64/")
1201 || exe.starts_with("/lib/")
1202 || exe.starts_with("/lib64/")
1203 || exe.starts_with("/opt/")
1204 || exe.starts_with("/snap/")
1205 {
1206 return true;
1207 }
1208 }
1209 return true; }
1211 false
1212 }
1213
1214 pub fn exists(&self) -> bool {
1218 let mut sys = System::new();
1219 let pid = Pid::from_u32(self.pid);
1220 self.refresh_for_check(&mut sys);
1221 match sys.process(pid) {
1222 None => false,
1223 Some(current) if current.status() == SysProcessStatus::Zombie => false,
1224 Some(current) => self.is_same(current),
1225 }
1226 }
1227
1228 pub fn is_running(&self) -> bool {
1230 self.exists()
1231 }
1232
1233 pub(crate) fn from_sysinfo(pid: Pid, proc: &sysinfo::Process) -> Self {
1235 let cmd_vec = proc.cmd();
1236 let raw_command = (!cmd_vec.is_empty()).then(|| {
1240 cmd_vec
1241 .iter()
1242 .map(|s| s.to_string_lossy())
1243 .collect::<Vec<_>>()
1244 .join(" ")
1245 });
1246 let raw_name = proc.name().to_string_lossy();
1247 let raw_exe = proc.exe().map(|p| p.to_string_lossy());
1248 let raw_cwd = proc
1249 .cwd()
1250 .map(|p| strip_deleted_marker(p.to_string_lossy().to_string()));
1251 let changed = |text: &str| matches!(escape_control(text), std::borrow::Cow::Owned(_));
1252 let raw = (changed(&raw_name)
1253 || raw_exe.as_deref().is_some_and(changed)
1254 || raw_command.as_deref().is_some_and(changed)
1255 || raw_cwd.as_deref().is_some_and(changed))
1256 .then(|| {
1257 Box::new(RawText {
1258 name: raw_name.to_string(),
1259 exe_path: raw_exe.as_ref().map(|exe| exe.to_string()),
1260 command: raw_command.clone(),
1261 cwd: raw_cwd.clone(),
1262 })
1263 });
1264 let command = raw_command
1265 .as_deref()
1266 .map(|joined| escape_control(joined).into_owned());
1267
1268 let exe_path = raw_exe
1269 .as_deref()
1270 .map(|exe| escape_control(exe).into_owned());
1271 let cwd = raw_cwd
1272 .as_deref()
1273 .map(|path| escape_control(path).into_owned());
1274
1275 Process {
1276 pid: pid.as_u32(),
1277 name: escape_control(&raw_name).into_owned(),
1278 exe_path,
1279 cwd,
1280 command,
1281 argv0: cmd_vec
1282 .first()
1283 .map(|arg| arg.to_string_lossy().into_owned()),
1284 raw,
1285 cpu_percent: proc.cpu_usage(),
1286 memory_mb: proc.memory() as f64 / 1024.0 / 1024.0,
1287 status: ProcessStatus::from(proc.status()),
1288 user: proc
1289 .user_id()
1290 .map(|uid| escape_control(&user_name(uid)).into_owned()),
1291 parent_pid: proc.parent().map(|p| p.as_u32()),
1292 start_time: (proc.start_time() > 0).then(|| proc.start_time()),
1295 uptime_secs: (proc.start_time() > 0).then(|| proc.run_time()),
1296 }
1297 }
1298}
1299
1300#[cfg(unix)]
1305pub fn parse_signal_name(name: &str) -> Result<nix::sys::signal::Signal> {
1306 use nix::sys::signal::Signal;
1307
1308 let upper = name.to_uppercase();
1309 let upper = upper.trim_start_matches("SIG");
1310 match upper {
1311 "HUP" => Ok(Signal::SIGHUP),
1312 "INT" => Ok(Signal::SIGINT),
1313 "QUIT" => Ok(Signal::SIGQUIT),
1314 "ABRT" => Ok(Signal::SIGABRT),
1315 "KILL" => Ok(Signal::SIGKILL),
1316 "TERM" => Ok(Signal::SIGTERM),
1317 "STOP" => Ok(Signal::SIGSTOP),
1318 "CONT" => Ok(Signal::SIGCONT),
1319 "USR1" => Ok(Signal::SIGUSR1),
1320 "USR2" => Ok(Signal::SIGUSR2),
1321 _ => Err(ProcError::InvalidInput(format!(
1322 "Unknown signal: '{}'. Valid signals: HUP, INT, QUIT, ABRT, KILL, TERM, STOP, CONT, USR1, USR2",
1323 name
1324 ))),
1325 }
1326}
1327
1328#[cfg(test)]
1329mod tests {
1330 use super::*;
1331
1332 #[test]
1333 fn names_by_pid_names_a_process_as_the_process_list_does() {
1334 let me = std::process::id();
1335 let names = names_by_pid(&[me, 4_000_000]);
1336 let record = Process::find_by_pid(me).unwrap().unwrap();
1337 assert_eq!(names.get(&me), Some(&record.name));
1338 assert!(!names.contains_key(&4_000_000), "a PID nobody holds");
1339 }
1340
1341 #[test]
1342 fn a_signal_needs_the_start_time_the_record_saw() {
1343 assert!(same_start(Some(1_000), 1_000));
1345 assert!(same_start(Some(1_000), 1_001));
1346 assert!(!same_start(Some(1_000), 4_600));
1348 assert!(!same_start(Some(1_000), 0));
1350 assert!(same_start(None, 0));
1352 assert!(same_start(None, 1_000));
1353 }
1354
1355 #[test]
1356 fn a_start_time_that_reads_0_is_read_again_until_it_is_there() {
1357 let reads = |wants_start: bool, answers: &[Option<u64>]| {
1359 let mut left = answers.iter();
1360 let mut count = 0;
1361 read_until_started(wants_start, || {
1362 count += 1;
1363 left.next().copied().unwrap_or(Some(0))
1365 });
1366 count
1367 };
1368 assert_eq!(reads(true, &[Some(0), Some(0), Some(1_000)]), 3);
1370 assert_eq!(reads(true, &[]), START_TIME_READS);
1372 assert_eq!(reads(true, &[Some(1_000)]), 1);
1374 assert_eq!(reads(true, &[None]), 1);
1375 assert_eq!(reads(false, &[Some(0)]), 1);
1377 }
1378
1379 #[cfg(target_os = "macos")]
1385 #[test]
1386 fn an_account_whose_shell_is_false_is_named() {
1387 let daemon = nix::unistd::User::from_name("daemon")
1388 .unwrap()
1389 .expect("macOS has a daemon account");
1390 assert!(
1391 daemon.shell.to_string_lossy().ends_with("/false"),
1392 "{:?}",
1393 daemon.shell
1394 );
1395 let uid = Uid::try_from(daemon.uid.as_raw() as usize).unwrap();
1396 assert_eq!(user_name(&uid), "daemon");
1397 }
1398
1399 #[cfg(unix)]
1400 #[test]
1401 fn a_user_id_nobody_has_is_shown_as_the_number() {
1402 let uid = Uid::try_from(4_000_000_000usize).unwrap();
1403 assert_eq!(user_name(&uid), "4000000000");
1404 }
1405
1406 #[test]
1407 fn cpu_is_the_cpu_time_used_over_the_wall_time() {
1408 let second = Duration::from_secs(1);
1409 assert_eq!(cpu_percent_between(5_000, 6_000, second), 100.0);
1411 assert_eq!(cpu_percent_between(5_000, 5_500, second), 50.0);
1412 assert_eq!(cpu_percent_between(5_000, 5_000, second), 0.0);
1413 assert_eq!(cpu_percent_between(0, 2_000, second), 200.0);
1415 assert_eq!(cpu_percent_between(6_000, 5_000, second), 0.0);
1417 assert_eq!(cpu_percent_between(0, 1_000, Duration::ZERO), 0.0);
1418 }
1419
1420 #[test]
1421 fn a_failed_command_is_explained_in_its_own_words() {
1422 let reason = "ERROR: The process with PID 7 could not be terminated.";
1423 assert_eq!(
1424 command_failure(b"", reason.as_bytes(), "exit code: 1"),
1425 reason
1426 );
1427 assert_eq!(
1429 command_failure(b" not here\r\n", b"", "exit code: 1"),
1430 "not here"
1431 );
1432 assert_eq!(
1434 command_failure(
1435 b"",
1436 b"ERROR: first part\r\n second part.\r\n\r\n",
1437 "exit code: 1"
1438 ),
1439 "ERROR: first part second part."
1440 );
1441 assert_eq!(
1442 command_failure(b"", b" \n", "exit code: 128"),
1443 "exited with exit code: 128"
1444 );
1445 }
1446
1447 #[test]
1448 fn busy_means_over_half_a_core_or_blocked_in_the_kernel() {
1449 assert!(looks_stuck(50.1, SysProcessStatus::Run));
1450 assert!(!looks_stuck(50.0, SysProcessStatus::Run));
1451 assert!(!looks_stuck(0.0, SysProcessStatus::Sleep));
1452 assert!(looks_stuck(0.0, SysProcessStatus::UninterruptibleDiskSleep));
1453 }
1454
1455 #[test]
1456 fn a_reading_counts_as_stuck_from_the_timeout_on() {
1457 assert!(counts_as_stuck(false, 0, 0, true));
1458 assert!(!counts_as_stuck(false, 59, 60, true));
1459 assert!(counts_as_stuck(false, 60, 60, true));
1460 assert!(counts_as_stuck(false, 61, 60, true));
1461 assert!(!counts_as_stuck(false, 60, 60, false));
1463 assert!(!counts_as_stuck(true, 60, 60, true));
1464 }
1465
1466 #[test]
1467 fn a_process_that_ran_the_timeout_has_run_long_enough() {
1468 assert!(ran_long_enough(0, 0));
1470 assert!(ran_long_enough(5, 5));
1471 assert!(ran_long_enough(6, 5));
1472 assert!(!ran_long_enough(4, 5));
1473 }
1474
1475 #[test]
1476 fn test_find_all_processes() {
1477 let processes = Process::find_all().unwrap();
1478 assert!(!processes.is_empty(), "Should find at least one process");
1479 }
1480
1481 #[test]
1482 fn every_process_source_lists_pid_ascending() {
1483 use crate::test_support::{end, idle_child, record_of};
1484
1485 let mut children: Vec<_> = (0..3).map(|_| idle_child()).collect();
1486 let mine: Vec<u32> = children.iter().map(|c| record_of(c).pid).collect();
1487 let name = record_of(&children[0]).name;
1488
1489 let ascending = |what: &str, list: &[Process]| {
1490 let pids: Vec<u32> = list.iter().map(|p| p.pid).collect();
1491 let mut sorted = pids.clone();
1492 sorted.sort_unstable();
1493 sorted.dedup();
1494 assert_eq!(pids, sorted, "{what} is not PID ascending");
1495 pids
1496 };
1497 let all = ascending("find_all", &Process::find_all().unwrap());
1498 let named = ascending(
1499 "find_by_name",
1500 &Process::find_by_name(&name, NameMatch::Exact).unwrap(),
1501 );
1502 ascending("find_stuck", &Process::find_stuck(Duration::ZERO).unwrap());
1503 for pid in &mine {
1504 assert!(all.contains(pid), "find_all has {pid}");
1505 assert!(named.contains(pid), "find_by_name has {pid}");
1506 }
1507 children.iter_mut().for_each(end);
1508 }
1509
1510 #[test]
1511 fn test_find_by_pid_self() {
1512 let pid = std::process::id();
1513 let process = Process::find_by_pid(pid).unwrap();
1514 assert!(process.is_some(), "Should find own process");
1515 }
1516
1517 #[test]
1518 fn test_is_running_tells_a_reused_pid_apart() {
1519 let mut me = Process::find_by_pid(std::process::id()).unwrap().unwrap();
1520 assert!(me.is_running());
1521 let start = me.start_time.expect("a running process has a start time");
1523 me.start_time = Some(start - 3600);
1524 assert!(!me.is_running(), "a reused PID counted as the same process");
1525 }
1526
1527 #[test]
1528 fn test_escape_control_makes_terminal_input_harmless() {
1529 assert_eq!(escape_control("node server.js"), "node server.js");
1531 assert_eq!(escape_control("café ✓ 日本"), "café ✓ 日本");
1532 assert_eq!(escape_control("a\x1b[31mb"), "a\\x1b[31mb");
1534 assert_eq!(escape_control("\x1b]0;pwned\x07"), "\\x1b]0;pwned\\x07");
1535 assert_eq!(escape_control("one\ntwo\tthree\r"), "one\\ntwo\\tthree\\r");
1537 assert_eq!(escape_control("x\x7fy\u{9b}2J"), "x\\x7fy\\x9b2J");
1539 assert_eq!(escape_control("ab\u{202e}cd"), "ab\\u{202e}cd");
1541 }
1542
1543 #[cfg(unix)]
1544 #[test]
1545 fn test_looks_stuck_needs_sustained_cpu() {
1546 let spawn = |script: &str| {
1547 std::process::Command::new("sh")
1548 .args(["-c", script])
1549 .spawn()
1550 .unwrap()
1551 };
1552 let mut spinning = spawn("while :; do :; done");
1553 let mut idle = spawn("exec sleep 300");
1554 std::thread::sleep(Duration::from_millis(300));
1555 let record = |pid: u32| Process::find_by_pid(pid).unwrap().unwrap();
1556
1557 let spinning_stuck = record(spinning.id()).looks_stuck_now();
1558 let idle_stuck = record(idle.id()).looks_stuck_now();
1559 for child in [&mut spinning, &mut idle] {
1560 let _ = child.kill();
1561 let _ = child.wait();
1562 }
1563 assert!(
1564 spinning_stuck,
1565 "a process spinning at 100% CPU wasn't stuck"
1566 );
1567 assert!(!idle_stuck, "an idle process counted as stuck");
1568 }
1569
1570 #[test]
1571 fn test_signals_never_reach_a_process_that_reused_the_pid() {
1572 let mut child = crate::test_support::idle_child();
1573 let mut stale = crate::test_support::record_of(&child);
1574 stale.start_time = stale.start_time.map(|t| t - 3600);
1576
1577 let kill = stale.kill();
1578 let term = stale.terminate();
1579 #[cfg(unix)]
1580 let sig = stale.send_signal(nix::sys::signal::Signal::SIGKILL);
1581 let alive = child.try_wait().unwrap().is_none();
1582 let _ = child.kill();
1583 let _ = child.wait();
1584
1585 assert!(
1586 alive,
1587 "a stale record signalled the process now using its PID"
1588 );
1589 assert!(
1590 matches!(kill, Err(ProcError::ProcessNotFound(_))),
1591 "{kill:?}"
1592 );
1593 assert!(
1594 matches!(term, Err(ProcError::ProcessNotFound(_))),
1595 "{term:?}"
1596 );
1597 #[cfg(unix)]
1598 assert!(matches!(sig, Err(ProcError::ProcessNotFound(_))), "{sig:?}");
1599 }
1600
1601 #[cfg(unix)]
1605 #[test]
1606 fn text_with_control_characters_matches_as_typed_and_displays_escaped() {
1607 use crate::core::matches_by_filter;
1608 use std::os::unix::process::CommandExt;
1609
1610 let word = format!("zz\u{7}ctl{}x", std::process::id());
1611 let mut child = std::process::Command::new("sh")
1615 .arg0(&word)
1616 .args(["-c", "read line"])
1617 .stdin(std::process::Stdio::piped())
1618 .spawn()
1619 .unwrap();
1620 let pid = child.id();
1621 let record = std::iter::repeat_with(|| {
1626 std::thread::sleep(Duration::from_millis(5));
1627 Process::find_by_pid(pid).unwrap().unwrap()
1628 })
1629 .take(1000)
1630 .find(|record| record.argv0.is_some());
1631 let part = format!("\u{7}ctl{}", std::process::id());
1632
1633 let found = |pattern: &str, mode| {
1634 Process::find_by_name(pattern, mode)
1635 .map(|found| found.iter().any(|p| p.pid == pid))
1636 .unwrap_or(false)
1637 };
1638 let named = found(&word, NameMatch::Exact);
1639 let named_in_command = found(&part, NameMatch::Command);
1640 let _ = child.kill();
1641 let _ = child.wait();
1642
1643 let record = record.expect("the child's command line never appeared");
1644 assert!(named, "find_by_name read the escaped first argument");
1645 assert!(
1646 named_in_command,
1647 "find_by_name read the escaped command line"
1648 );
1649 assert!(
1650 matches_by_filter(&record, &word, NameMatch::Exact),
1651 "--by read the escaped first argument"
1652 );
1653 assert!(
1654 matches_by_filter(&record, &part, NameMatch::Command),
1655 "--by --cmd read the escaped command line"
1656 );
1657 let shown = record.command.as_deref().unwrap();
1659 assert!(shown.contains("\\x07ctl"), "{shown}");
1660 assert!(!shown.contains('\u{7}'), "{shown:?}");
1661 }
1662
1663 #[cfg(windows)]
1667 #[test]
1668 fn a_control_character_in_a_windows_command_line_matches_as_typed_and_displays_escaped() {
1669 use crate::core::matches_by_filter;
1670
1671 let mut exe = std::env::current_exe().unwrap();
1672 exe.pop();
1673 exe.pop();
1674 exe.push("examples");
1675 exe.push("listener.exe");
1676 assert!(exe.exists(), "build the example first: {}", exe.display());
1677 let part = format!("\u{7}ctl{}", std::process::id());
1678 let mut child = std::process::Command::new(&exe)
1679 .arg(&part)
1680 .stdin(std::process::Stdio::null())
1681 .stdout(std::process::Stdio::null())
1682 .spawn()
1683 .unwrap();
1684 let pid = child.id();
1685 let record = std::iter::repeat_with(|| {
1686 std::thread::sleep(Duration::from_millis(20));
1687 Process::find_by_pid(pid).unwrap()
1688 })
1689 .take(500)
1690 .flatten()
1691 .find(|record| record.command.is_some())
1692 .expect("the child's command line never appeared");
1693 let found = Process::find_by_name(&part, NameMatch::Command)
1694 .map(|found| found.iter().any(|p| p.pid == pid))
1695 .unwrap_or(false);
1696 let kept = matches_by_filter(&record, &part, NameMatch::Command);
1697 crate::test_support::end(&mut child);
1698
1699 assert!(found, "find_by_name read the escaped command line");
1700 assert!(kept, "--by --cmd read the escaped command line");
1701 let shown = record.command.as_deref().unwrap();
1702 assert!(shown.contains("\\x07ctl"), "{shown}");
1703 assert!(!shown.contains('\u{7}'), "{shown:?}");
1704 }
1705
1706 #[test]
1707 fn a_name_is_read_from_the_raw_text_when_escaping_changed_it() {
1708 let record = Process {
1709 name: "a\\x07b".to_string(),
1710 raw: Some(Box::new(RawText {
1711 name: "a\u{7}b".to_string(),
1712 exe_path: Some("c\u{7}d".to_string()),
1713 command: Some("e\u{7}f".to_string()),
1714 cwd: None,
1715 })),
1716 argv0: Some("g".to_string()),
1717 ..ghost()
1718 };
1719 let names = record.names();
1720 assert_eq!(names.name, "a\u{7}b");
1721 assert_eq!(names.exe, Some("c\u{7}d"));
1722 assert_eq!(names.argv0, Some("g"));
1723 assert_eq!(names.command, Some("e\u{7}f"));
1724 }
1725
1726 #[test]
1727 fn the_ancestors_stop_where_the_process_list_does() {
1728 let me = Pid::from_u32(std::process::id());
1731 let mut sys = System::new();
1732 sysinfo_locked(|| sys.refresh_processes(sysinfo::ProcessesToUpdate::Some(&[me]), true));
1733 let parent = sys.process(me).and_then(|p| p.parent());
1734 let expected: std::collections::HashSet<Pid> = std::iter::once(me).chain(parent).collect();
1735 assert_eq!(self_and_ancestors(&sys), expected);
1736 }
1737
1738 #[test]
1739 fn test_strip_deleted_marker() {
1740 assert_eq!(
1741 strip_deleted_marker("/nonexistent/proc-gone (deleted)".to_string()),
1742 "/nonexistent/proc-gone"
1743 );
1744 assert_eq!(
1745 strip_deleted_marker("/tmp/project".to_string()),
1746 "/tmp/project"
1747 );
1748
1749 let real = std::env::temp_dir().join(format!("proc-real-{} (deleted)", std::process::id()));
1751 std::fs::create_dir_all(&real).unwrap();
1752 let path = real.to_string_lossy().to_string();
1753 assert_eq!(strip_deleted_marker(path.clone()), path);
1754 std::fs::remove_dir(&real).unwrap();
1755 }
1756
1757 #[test]
1758 fn a_name_nothing_has_is_plain_not_found_in_both_modes() {
1759 for mode in [NameMatch::Exact, NameMatch::Command] {
1760 let result = Process::find_by_name("nonexistent_process_12345", mode);
1761 assert!(
1762 matches!(&result, Err(ProcError::ProcessNotFound(n)) if n == "nonexistent_process_12345"),
1763 "{result:?}"
1764 );
1765 }
1766 }
1767
1768 #[cfg(unix)]
1772 fn waiting_shell_with(marker: &str) -> std::process::Child {
1773 std::process::Command::new("sh")
1774 .args(["-c", &format!("read line; : {marker}")])
1775 .stdin(std::process::Stdio::piped())
1776 .spawn()
1777 .unwrap()
1778 }
1779
1780 #[cfg(unix)]
1781 #[test]
1782 fn a_word_only_in_a_command_line_is_found_with_cmd_and_hinted_without() {
1783 let marker = format!("hintmarker{}", std::process::id());
1784 let mut child = waiting_shell_with(&marker);
1785 let pid = child.id();
1786 let exact = Process::find_by_name(&marker, NameMatch::Exact);
1787 let cmd = Process::find_by_name(&marker, NameMatch::Command);
1788 let _ = child.kill();
1789 let _ = child.wait();
1790
1791 assert!(
1794 matches!(&exact, Err(ProcError::NameNotFound { name, in_command_line })
1795 if name == &marker && *in_command_line >= 1),
1796 "{exact:?}"
1797 );
1798 let found = cmd.unwrap();
1800 assert_eq!(found.iter().map(|p| p.pid).collect::<Vec<_>>(), vec![pid]);
1801 }
1802
1803 #[cfg(unix)]
1804 #[test]
1805 fn test_find_orphans_returns_ok() {
1806 let result = Process::find_orphans();
1808 assert!(result.is_ok());
1809 }
1810
1811 #[cfg(not(unix))]
1812 #[test]
1813 fn find_orphans_is_not_supported_on_windows() {
1814 let result = Process::find_orphans();
1815 assert!(
1816 matches!(&result, Err(ProcError::NotSupported(m)) if m.starts_with("orphans is not supported on Windows")),
1817 "{result:?}"
1818 );
1819 }
1820
1821 #[cfg(unix)]
1822 #[test]
1823 fn test_is_system_process_system_paths() {
1824 let make_proc = |exe: Option<&str>, cwd: Option<&str>| Process {
1825 pid: 100,
1826 name: "test".to_string(),
1827 exe_path: exe.map(String::from),
1828 cwd: cwd.map(String::from),
1829 command: None,
1830 argv0: None,
1831 raw: None,
1832 cpu_percent: 0.0,
1833 memory_mb: 0.0,
1834 status: ProcessStatus::Running,
1835 user: None,
1836 parent_pid: Some(1),
1837 start_time: None,
1838 uptime_secs: None,
1839 };
1840
1841 assert!(Process::is_system_process(&make_proc(
1843 Some("/usr/sbin/sshd"),
1844 Some("/")
1845 )));
1846 assert!(Process::is_system_process(&make_proc(
1847 Some("/System/Library/foo"),
1848 Some("/")
1849 )));
1850 assert!(Process::is_system_process(&make_proc(
1851 Some("/usr/bin/systemd"),
1852 Some("/")
1853 )));
1854 assert!(Process::is_system_process(&make_proc(
1855 Some("/usr/lib/snapd/snapd"),
1856 Some("/")
1857 )));
1858
1859 assert!(Process::is_system_process(&make_proc(None, Some("/"))));
1861
1862 assert!(Process::is_system_process(&make_proc(
1864 Some("/usr/bin/foo"),
1865 None
1866 )));
1867
1868 for exe in [
1870 "/System/x",
1871 "/usr/libexec/x",
1872 "/usr/sbin/x",
1873 "/sbin/x",
1874 "/usr/bin/x",
1875 "/usr/lib/x",
1876 "/usr/lib64/x",
1877 "/lib/x",
1878 "/lib64/x",
1879 "/opt/x",
1880 "/snap/x",
1881 ] {
1882 assert!(
1883 Process::is_system_process(&make_proc(Some(exe), Some("/"))),
1884 "{exe}"
1885 );
1886 }
1887 assert!(Process::is_system_process(&make_proc(
1889 Some("/srv/x"),
1890 Some("/")
1891 )));
1892
1893 assert!(!Process::is_system_process(&make_proc(
1895 Some("/usr/bin/node"),
1896 Some("/home/user/project")
1897 )));
1898 assert!(!Process::is_system_process(&make_proc(
1899 Some("/home/user/.local/bin/app"),
1900 Some("/home/user")
1901 )));
1902 }
1903
1904 #[cfg(unix)]
1905 #[test]
1906 fn test_parse_signal_name_valid() {
1907 use nix::sys::signal::Signal;
1908
1909 assert_eq!(parse_signal_name("HUP").unwrap(), Signal::SIGHUP);
1910 assert_eq!(parse_signal_name("hup").unwrap(), Signal::SIGHUP);
1911 assert_eq!(parse_signal_name("SIGHUP").unwrap(), Signal::SIGHUP);
1912 assert_eq!(parse_signal_name("sighup").unwrap(), Signal::SIGHUP);
1913 assert_eq!(parse_signal_name("INT").unwrap(), Signal::SIGINT);
1914 assert_eq!(parse_signal_name("QUIT").unwrap(), Signal::SIGQUIT);
1915 assert_eq!(parse_signal_name("ABRT").unwrap(), Signal::SIGABRT);
1916 assert_eq!(parse_signal_name("KILL").unwrap(), Signal::SIGKILL);
1917 assert_eq!(parse_signal_name("TERM").unwrap(), Signal::SIGTERM);
1918 assert_eq!(parse_signal_name("STOP").unwrap(), Signal::SIGSTOP);
1919 assert_eq!(parse_signal_name("CONT").unwrap(), Signal::SIGCONT);
1920 assert_eq!(parse_signal_name("USR1").unwrap(), Signal::SIGUSR1);
1921 assert_eq!(parse_signal_name("USR2").unwrap(), Signal::SIGUSR2);
1922 }
1923
1924 #[cfg(unix)]
1925 #[test]
1926 fn test_parse_signal_name_invalid() {
1927 assert!(parse_signal_name("INVALID").is_err());
1928 assert!(parse_signal_name("FOO").is_err());
1929 assert!(parse_signal_name("").is_err());
1930 }
1931
1932 #[cfg(unix)]
1933 #[test]
1934 fn test_parse_signal_name_case_insensitive() {
1935 use nix::sys::signal::Signal;
1936
1937 assert_eq!(parse_signal_name("term").unwrap(), Signal::SIGTERM);
1938 assert_eq!(parse_signal_name("Term").unwrap(), Signal::SIGTERM);
1939 assert_eq!(parse_signal_name("TERM").unwrap(), Signal::SIGTERM);
1940 assert_eq!(parse_signal_name("sigterm").unwrap(), Signal::SIGTERM);
1941 assert_eq!(parse_signal_name("SigTerm").unwrap(), Signal::SIGTERM);
1942 }
1943
1944 #[cfg(unix)]
1945 #[test]
1946 fn test_send_signal_nonexistent_process() {
1947 use nix::sys::signal::Signal;
1948
1949 let proc = Process {
1951 pid: 99999999,
1952 name: "ghost".to_string(),
1953 exe_path: None,
1954 cwd: None,
1955 command: None,
1956 argv0: None,
1957 raw: None,
1958 cpu_percent: 0.0,
1959 memory_mb: 0.0,
1960 status: ProcessStatus::Running,
1961 user: None,
1962 parent_pid: None,
1963 start_time: None,
1964 uptime_secs: None,
1965 };
1966
1967 let result = proc.send_signal(Signal::SIGCONT);
1968 assert!(result.is_err());
1969 }
1970
1971 fn ghost() -> Process {
1973 Process {
1974 pid: 99999999,
1975 name: "ghost".to_string(),
1976 exe_path: None,
1977 cwd: None,
1978 command: None,
1979 argv0: None,
1980 raw: None,
1981 cpu_percent: 0.0,
1982 memory_mb: 0.0,
1983 status: ProcessStatus::Running,
1984 user: None,
1985 parent_pid: None,
1986 start_time: None,
1987 uptime_secs: None,
1988 }
1989 }
1990
1991 #[test]
1992 fn every_sysinfo_status_maps_to_ours() {
1993 for (sys, ours) in [
1994 (SysProcessStatus::Run, ProcessStatus::Running),
1995 (SysProcessStatus::Sleep, ProcessStatus::Sleeping),
1996 (SysProcessStatus::Stop, ProcessStatus::Stopped),
1997 (SysProcessStatus::Zombie, ProcessStatus::Zombie),
1998 (SysProcessStatus::Dead, ProcessStatus::Dead),
1999 (SysProcessStatus::Idle, ProcessStatus::Unknown),
2000 ] {
2001 assert_eq!(ProcessStatus::from(sys), ours, "{sys:?}");
2002 }
2003 }
2004
2005 #[test]
2006 fn a_process_that_is_gone_does_not_look_stuck() {
2007 assert!(!ghost().looks_stuck_now());
2008 }
2009
2010 #[test]
2011 fn killing_a_process_that_is_already_gone_is_success() {
2012 assert!(matches!(ghost().kill(), Err(ProcError::ProcessNotFound(_))));
2013 ghost().kill_and_wait().unwrap();
2014 }
2015
2016 #[test]
2017 fn a_kill_that_was_not_delivered_is_an_error() {
2018 kill_outcome(true, 7).unwrap();
2019 let err = kill_outcome(false, 7).unwrap_err();
2020 assert!(
2021 matches!(&err, ProcError::SignalError(m) if m == "Failed to kill process 7"),
2022 "{err:?}"
2023 );
2024 }
2025
2026 #[test]
2027 fn taskkill_without_force_reports_what_it_said() {
2028 #[cfg(unix)]
2029 use std::os::unix::process::ExitStatusExt;
2030 #[cfg(windows)]
2031 use std::os::windows::process::ExitStatusExt;
2032 use std::process::{ExitStatus, Output};
2033
2034 let output = |raw, stdout: &str, stderr: &str| Output {
2035 status: ExitStatus::from_raw(raw),
2036 stdout: stdout.as_bytes().to_vec(),
2037 stderr: stderr.as_bytes().to_vec(),
2038 };
2039 taskkill_outcome(&output(0, "SUCCESS: sent termination signal", "")).unwrap();
2040
2041 #[cfg(unix)]
2043 let failed = 1 << 8;
2044 #[cfg(windows)]
2045 let failed = 1;
2046 let err =
2047 taskkill_outcome(&output(failed, "", "ERROR: could not be terminated")).unwrap_err();
2048 assert!(
2049 matches!(&err, ProcError::SignalError(m) if m == "ERROR: could not be terminated"),
2050 "{err:?}"
2051 );
2052 }
2053
2054 #[cfg(unix)]
2055 #[test]
2056 fn orphans_are_the_children_of_init_that_are_not_system_daemons() {
2057 let proc = |pid: u32, ppid: Option<u32>, exe: Option<&str>, cwd: Option<&str>| Process {
2058 pid,
2059 name: format!("p{pid}"),
2060 exe_path: exe.map(String::from),
2061 cwd: cwd.map(String::from),
2062 parent_pid: ppid,
2063 ..ghost()
2064 };
2065 let orphans = Process::orphans_among(vec![
2066 proc(10, Some(1), Some("/home/z/app"), Some("/home/z")),
2068 proc(11, Some(1), Some("/usr/sbin/sshd"), Some("/")),
2070 proc(1, Some(1), Some("/sbin/init"), Some("/home/z")),
2072 proc(12, Some(500), Some("/home/z/app"), Some("/home/z")),
2073 proc(13, None, Some("/home/z/app"), Some("/home/z")),
2075 ]);
2076 let pids: Vec<u32> = orphans.iter().map(|p| p.pid).collect();
2077 assert_eq!(pids, vec![10]);
2078 }
2079
2080 #[cfg(unix)]
2081 #[test]
2082 fn a_missing_lsof_finds_no_open_file_owners() {
2083 let found = Process::find_by_open_file_with("proc-no-such-lsof", std::path::Path::new("/"));
2084 assert!(found.unwrap().is_empty());
2085 }
2086
2087 #[test]
2088 fn lsof_prints_one_pid_a_line() {
2089 assert_eq!(lsof_pids("1851\n2614\n"), vec![1851, 2614]);
2090 assert!(lsof_pids("").is_empty());
2091 }
2092
2093 #[test]
2094 fn fstat_rows_give_the_pids_that_hold_the_file() {
2095 let freebsd = "USER CMD PID FD MOUNT INUM MODE SZ|DV R/W NAME\n\
2097 proc sleep 2614 3 / 1445173 -rw-r--r-- 5 r /home/proc/held.txt\n";
2098 let netbsd = "USER CMD PID FD MOUNT INUM MODE SZ|DV R/W NAME\n\
2099 proc sleep 1851 3 / 425799 -rw-r--r-- 5 r /home/proc/held.txt\n";
2100 assert_eq!(fstat_pids(freebsd), vec![2614]);
2101 assert_eq!(fstat_pids(netbsd), vec![1851]);
2102 assert!(fstat_pids(
2104 "USER CMD PID FD MOUNT INUM MODE SZ|DV R/W NAME\n"
2105 )
2106 .is_empty());
2107 let rows =
2110 "proc Web Content 4021 12 / 77 -rw-r--r-- 5 r /home/proc/2026 notes.txt\n\
2111 proc Web Content 4021 13 / 77 -rw-r--r-- 5 r /home/proc/2026 notes.txt\n\
2112 root sshd 90 text / 12 -r-xr-xr-x 9 r /home/proc/2026 notes.txt\n";
2113 assert_eq!(fstat_pids(rows), vec![90, 4021]);
2114 let numeric =
2117 "proc 4021 777 3 /home 77 -rw-r--r-- 5 r /home/proc/held.txt\n\
2118 proc Web 4021 778 3 /home 77 -rw-r--r-- 5 r /home/proc/held.txt\n";
2119 assert_eq!(fstat_pids(numeric), vec![777, 778]);
2120 assert!(
2122 fstat_pids("proc sleep 2614 3 - 77 -rw-r--r-- 5 r held.txt\n").is_empty()
2123 );
2124 assert!(fstat_pids("proc 2614 3 / 77 -rw-r--r-- 5 r /h\n").is_empty());
2125 }
2126
2127 #[test]
2128 fn a_wait_for_a_process_that_stays_is_a_wait_and_then_an_error() {
2129 let mut child = crate::test_support::idle_child();
2130 let record = crate::test_support::record_of(&child);
2131 let started = std::time::Instant::now();
2132 let result = record.kill_then_wait(Duration::from_millis(300), |_| Ok(()));
2135 let waited = started.elapsed();
2136 let still_there = child.try_wait().unwrap().is_none();
2137 crate::test_support::end(&mut child);
2138 assert!(
2139 matches!(&result, Err(ProcError::SignalError(_))),
2140 "{result:?}"
2141 );
2142 assert!(waited >= Duration::from_millis(300), "{waited:?}");
2143 assert!(still_there);
2144 }
2145
2146 #[test]
2147 fn a_process_still_alive_when_the_wait_ends_is_an_error() {
2148 let mut child = crate::test_support::idle_child();
2149 let record = crate::test_support::record_of(&child);
2150 let result = record.kill_and_wait_within(Duration::ZERO);
2152 let _ = child.kill();
2153 let _ = child.wait();
2154 assert!(
2155 matches!(&result, Err(ProcError::SignalError(m))
2156 if m == &format!("Process {} is still running 0 seconds after SIGKILL", record.pid)),
2157 "{result:?}"
2158 );
2159 }
2160
2161 #[cfg(target_os = "macos")]
2163 fn kinfo(pid: u32, stat: u8, comm: &str, ruid: u32, ppid: u32, start: u64) -> Vec<u8> {
2164 let mut rec = vec![0u8; KINFO_PROC_SIZE];
2165 rec[0..8].copy_from_slice(&start.to_ne_bytes());
2166 rec[36] = stat;
2167 rec[40..44].copy_from_slice(&pid.to_ne_bytes());
2168 rec[243..243 + comm.len()].copy_from_slice(comm.as_bytes());
2169 rec[392..396].copy_from_slice(&ruid.to_ne_bytes());
2170 rec[560..564].copy_from_slice(&ppid.to_ne_bytes());
2171 rec
2172 }
2173
2174 #[cfg(target_os = "macos")]
2175 #[test]
2176 fn zombies_are_read_from_the_kernels_table() {
2177 let known = std::collections::HashSet::from([30]);
2178 let table: Vec<u8> = [
2179 kinfo(10, 5, "defunct-app", 0, 99, 1_700_000_000),
2180 kinfo(11, 5, "", 0, 1, 0),
2182 kinfo(20, 2, "alive", 0, 1, 1_700_000_000),
2184 kinfo(30, 5, "known", 0, 1, 1_700_000_000),
2186 kinfo(0, 5, "kernel", 0, 0, 0),
2188 ]
2189 .concat();
2190
2191 let zombies = zombies_in(&table, &known);
2192 let pids: Vec<u32> = zombies.iter().map(|z| z.pid).collect();
2193 assert_eq!(pids, vec![10, 11]);
2194
2195 let named = &zombies[0];
2196 assert_eq!(named.name, "defunct-app");
2197 assert_eq!(named.status, ProcessStatus::Zombie);
2198 assert_eq!(named.parent_pid, Some(99));
2199 assert_eq!(named.start_time, Some(1_700_000_000));
2200 assert!(named.uptime_secs.unwrap() > 0);
2201 assert_eq!(named.user.as_deref(), Some("root"));
2202
2203 let unnamed = &zombies[1];
2204 assert_eq!(unnamed.name, "<defunct>");
2205 assert_eq!(unnamed.start_time, None);
2206 assert_eq!(unnamed.uptime_secs, None);
2207 }
2208
2209 #[cfg(target_os = "macos")]
2210 #[test]
2211 fn a_table_of_an_unknown_layout_shows_no_zombies() {
2212 let known = std::collections::HashSet::new();
2213 let mut table = kinfo(10, 5, "z", 0, 1, 0);
2214 table.push(0);
2215 assert!(zombies_in(&table, &known).is_empty());
2216 }
2217
2218 #[cfg(target_os = "macos")]
2219 #[test]
2220 fn a_refused_sysctl_gives_an_empty_table() {
2221 use std::cell::Cell;
2222
2223 assert!(read_proc_table(|_, _| -1).is_empty());
2225
2226 let calls = Cell::new(0);
2228 let table = read_proc_table(|_, size| {
2229 calls.set(calls.get() + 1);
2230 if calls.get() == 1 {
2231 *size = 2 * KINFO_PROC_SIZE;
2232 0
2233 } else {
2234 -1
2235 }
2236 });
2237 assert_eq!(calls.get(), 2);
2238 assert!(table.is_empty());
2239
2240 let calls = Cell::new(0);
2242 let table = read_proc_table(|buf, size| {
2243 calls.set(calls.get() + 1);
2244 if calls.get() == 1 {
2245 *size = 2 * KINFO_PROC_SIZE;
2246 } else {
2247 assert_eq!(*size, 66 * KINFO_PROC_SIZE, "room for 64 newcomers");
2248 unsafe { buf.cast::<u8>().write(7) };
2250 *size = KINFO_PROC_SIZE;
2251 }
2252 0
2253 });
2254 assert_eq!(table.len(), KINFO_PROC_SIZE);
2255 assert_eq!(table[0], 7);
2256 }
2257}