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