1use crate::Result;
2use crate::settings::settings;
3#[cfg(windows)]
4use crate::shell::HideConsoleWindow;
5use miette::IntoDiagnostic;
6use once_cell::sync::Lazy;
7use std::collections::HashMap;
8#[cfg(target_os = "linux")]
9use std::os::fd::{AsRawFd, FromRawFd, OwnedFd};
10use std::sync::Mutex;
11use std::time::{Duration, Instant};
12use sysinfo::ProcessesToUpdate;
13#[cfg(windows)]
14use windows_sys::Win32::Foundation::{CloseHandle, FILETIME, HANDLE};
15#[cfg(windows)]
16use windows_sys::Win32::System::Threading::{
17 GetProcessTimes, OpenProcess, PROCESS_QUERY_LIMITED_INFORMATION,
18};
19
20type ParentToChildren = HashMap<u32, Vec<u32>>;
22
23type ProcessNames = HashMap<u32, (String, Option<String>)>;
25
26const FULL_REFRESH_INTERVAL: Duration = Duration::from_secs(5);
32
33pub struct Procs {
34 system: Mutex<sysinfo::System>,
35 last_full_refresh: Mutex<Option<Instant>>,
38}
39
40pub static PROCS: Lazy<Procs> = Lazy::new(Procs::new);
41
42pub const PROCESS_GROUP_SIGKILL_WAIT: std::time::Duration = std::time::Duration::from_secs(2);
45
46pub const JOB_EXIT_WAIT: std::time::Duration = if cfg!(windows) {
50 std::time::Duration::from_secs(5)
51} else {
52 std::time::Duration::ZERO
53};
54
55impl Default for Procs {
56 fn default() -> Self {
57 Self::new()
58 }
59}
60
61impl Procs {
62 pub fn new() -> Self {
63 Self {
76 system: Mutex::new(sysinfo::System::new()),
77 last_full_refresh: Mutex::new(None),
78 }
79 }
80
81 fn lock_system(&self) -> std::sync::MutexGuard<'_, sysinfo::System> {
82 self.system.lock().unwrap_or_else(|poisoned| {
83 warn!("System mutex was poisoned, recovering");
84 poisoned.into_inner()
85 })
86 }
87
88 pub fn title(&self, pid: u32) -> Option<String> {
89 self.lock_system()
90 .process(sysinfo::Pid::from_u32(pid))
91 .map(|p| p.name().to_string_lossy().to_string())
92 }
93
94 pub fn boot_time(&self) -> u64 {
101 sysinfo::System::boot_time()
102 }
103
104 pub fn start_time(&self, pid: u32) -> Option<u64> {
110 process_start_token(pid)
111 }
112
113 #[cfg(not(windows))]
114 fn start_time_matches(&self, pid: u32, expected: u64) -> bool {
115 self.start_time(pid) == Some(expected)
116 }
117
118 pub fn is_running(&self, pid: u32) -> bool {
119 #[cfg(unix)]
125 {
126 unsafe {
127 if libc::kill(pid as i32, 0) == 0 {
128 return true;
129 }
130 std::io::Error::last_os_error().raw_os_error() != Some(libc::ESRCH)
131 }
132 }
133 #[cfg(not(unix))]
134 {
135 self.refresh_pids(&[pid]);
136 self.lock_system()
137 .process(sysinfo::Pid::from_u32(pid))
138 .is_some()
139 }
140 }
141
142 #[allow(dead_code)]
145 pub fn all_children(&self, pid: u32) -> Vec<u32> {
146 let system = self.lock_system();
147 let all = system.processes();
148 let mut children = vec![];
149 for (child_pid, process) in all {
150 let mut process = process;
151 while let Some(parent) = process.parent() {
152 if parent == sysinfo::Pid::from_u32(pid) {
153 children.push(child_pid.as_u32());
154 break;
155 }
156 match system.process(parent) {
157 Some(p) => process = p,
158 None => break,
159 }
160 }
161 }
162 children
163 }
164 pub fn collect_process_tree_info(&self) -> (ParentToChildren, ProcessNames) {
169 let system = self.lock_system();
170 let all = system.processes();
171 let mut parent_to_children: ParentToChildren = HashMap::new();
172 let mut process_info: ProcessNames = HashMap::new();
173
174 for (pid, proc) in all {
175 let pid_u32 = pid.as_u32();
176 process_info.insert(
177 pid_u32,
178 (
179 proc.name().to_string_lossy().to_string(),
180 proc.exe().map(|e| e.to_string_lossy().to_string()),
181 ),
182 );
183
184 if let Some(ppid) = proc.parent() {
185 parent_to_children
186 .entry(ppid.as_u32())
187 .or_default()
188 .push(pid_u32);
189 }
190 }
191
192 (parent_to_children, process_info)
193 }
194 pub async fn kill_process_group_async(
195 &self,
196 pid: u32,
197 stop_signal: i32,
198 stop_timeout: Option<std::time::Duration>,
199 ) -> Result<bool> {
200 tokio::task::spawn_blocking(move || {
201 PROCS.kill_process_group(pid, stop_signal, stop_timeout, None)
202 })
203 .await
204 .into_diagnostic()?
205 }
206
207 pub async fn kill_process_group_if_start_time_matches_async(
217 &self,
218 pid: u32,
219 expected_start_time: Option<u64>,
220 stop_signal: i32,
221 stop_timeout: Option<std::time::Duration>,
222 ) -> Result<bool> {
223 let Some(expected_start_time) = expected_start_time else {
224 warn!(
225 "no recorded start time for pid {pid}; refusing to signal it (identity cannot be bound to a process generation)"
226 );
227 return Ok(false);
228 };
229 tokio::task::spawn_blocking(move || {
230 PROCS.kill_process_group(pid, stop_signal, stop_timeout, Some(expected_start_time))
231 })
232 .await
233 .into_diagnostic()?
234 }
235
236 pub async fn kill_if_start_time_matches_async(
256 &self,
257 pid: u32,
258 expected_start_time: Option<u64>,
259 stop_signal: i32,
260 stop_timeout: Option<std::time::Duration>,
261 ) -> Result<bool> {
262 let Some(expected_start_time) = expected_start_time else {
263 warn!(
264 "no recorded start time for pid {pid}; refusing to signal it (identity cannot be bound to a process generation)"
265 );
266 return Ok(false);
267 };
268 tokio::task::spawn_blocking(move || {
269 PROCS.kill_if_start_time_matches(pid, expected_start_time, stop_signal, stop_timeout)
270 })
271 .await
272 .into_diagnostic()?
273 }
274
275 #[cfg(target_os = "linux")]
276 fn kill_if_start_time_matches(
277 &self,
278 pid: u32,
279 expected_start_time: u64,
280 stop_signal: i32,
281 stop_timeout: Option<std::time::Duration>,
282 ) -> Result<bool> {
283 let pidfd = match open_pidfd(pid) {
284 Ok(pidfd) => pidfd,
285 Err(err) if err.raw_os_error() == Some(libc::ESRCH) => {
286 debug!("process {pid} no longer exists");
287 return Ok(false);
288 }
289 Err(err) => {
290 return Err(miette::miette!(
291 "cannot securely identify process {pid}: {err}"
292 ));
293 }
294 };
295 if !self.verify_start_time_before_signal(pid, expected_start_time)? {
300 return Ok(false);
301 }
302 let target = [(pid, pidfd)];
303 let signal_name = signal_name(stop_signal);
304 debug!("sending {signal_name} to pinned process {pid}");
305 signal_pidfds(&target, stop_signal, signal_name)?;
306
307 let stop_timeout = stop_timeout.unwrap_or_else(|| settings().supervisor_stop_timeout());
310 let fast_ms = 10u64;
311 let slow_ms = 50u64;
312 let total_ms = stop_timeout.as_millis().max(1) as u64;
313 let fast_count = ((total_ms / fast_ms) as usize).min(10);
314 let fast_total_ms = fast_ms * fast_count as u64;
315 let slow_count = (total_ms.saturating_sub(fast_total_ms) / slow_ms) as usize;
316 for i in 0..fast_count {
317 std::thread::sleep(std::time::Duration::from_millis(fast_ms));
318 if !pidfd_is_running(&target[0].1) {
319 debug!(
320 "process {pid} terminated after {signal_name} ({} ms)",
321 (i + 1) as u64 * fast_ms
322 );
323 return Ok(true);
324 }
325 }
326 for i in 0..slow_count {
327 std::thread::sleep(std::time::Duration::from_millis(slow_ms));
328 if !pidfd_is_running(&target[0].1) {
329 debug!(
330 "process {pid} terminated after {signal_name} ({} ms)",
331 fast_total_ms + (i + 1) as u64 * slow_ms
332 );
333 return Ok(true);
334 }
335 }
336
337 warn!(
338 "process {pid} did not respond to {signal_name} after {}ms, sending SIGKILL",
339 stop_timeout.as_millis()
340 );
341 signal_pidfds(&target, libc::SIGKILL, "SIGKILL")?;
342 std::thread::sleep(std::time::Duration::from_millis(100));
344 Ok(true)
345 }
346
347 #[cfg(not(target_os = "linux"))]
348 fn kill_if_start_time_matches(
349 &self,
350 pid: u32,
351 expected_start_time: u64,
352 stop_signal: i32,
353 stop_timeout: Option<std::time::Duration>,
354 ) -> Result<bool> {
355 #[cfg(windows)]
359 let _pin = open_process_handle(pid).ok();
360
361 if !self.verify_start_time_before_signal(pid, expected_start_time)? {
362 return Ok(false);
363 }
364 self.kill(pid, stop_signal, stop_timeout, Some(expected_start_time))
365 }
366
367 fn verify_start_time_before_signal(&self, pid: u32, expected_start_time: u64) -> Result<bool> {
379 match self.start_time(pid) {
380 Some(current) if current == expected_start_time => Ok(true),
381 Some(current) => {
382 warn!(
383 "pid {pid} is not the recorded process (start time {current}, expected {expected_start_time}); not signalling it"
384 );
385 Ok(false)
386 }
387 None if self.is_running(pid) => Err(miette::miette!(
388 "cannot verify the identity of pid {pid}: its start time is unreadable; not signalling it"
389 )),
390 None => {
391 debug!("process {pid} no longer exists");
392 Ok(false)
393 }
394 }
395 }
396
397 #[cfg(unix)]
414 fn kill_process_group(
415 &self,
416 pid: u32,
417 stop_signal: i32,
418 stop_timeout: Option<std::time::Duration>,
419 expected_start_time: Option<u64>,
420 ) -> Result<bool> {
421 let pgid = pid as i32;
422 let signal_name = signal_name(stop_signal);
423
424 #[cfg(target_os = "linux")]
425 if let Some(expected) = expected_start_time {
426 return self.kill_process_group_with_pidfds(pid, expected, stop_signal, stop_timeout);
427 }
428
429 #[cfg(not(target_os = "linux"))]
441 if let Some(expected) = expected_start_time
442 && !self.start_time_matches(pid, expected)
443 {
444 debug!("process {pid} identity changed before killpg; refusing to signal it");
445 return Ok(false);
446 }
447
448 debug!("killing process group {pgid} with {signal_name}");
449
450 let ret = unsafe { libc::killpg(pgid, stop_signal) };
455 if ret == -1 {
456 let err = std::io::Error::last_os_error();
457 if err.raw_os_error() == Some(libc::ESRCH) {
458 debug!("process group {pgid} no longer exists");
459 return Ok(false);
460 }
461 if err.raw_os_error() == Some(libc::EPERM) {
462 return Err(miette::miette!(
463 "failed to send {signal_name} to process group {pgid}: permission denied"
464 ));
465 }
466 warn!("failed to send {signal_name} to process group {pgid}: {err}");
467 }
468
469 let stop_timeout = stop_timeout.unwrap_or_else(|| settings().supervisor_stop_timeout());
479 let fast_ms = 10u64;
480 let slow_ms = 50u64;
481 let total_ms = stop_timeout.as_millis().max(1) as u64;
482 let fast_count = ((total_ms / fast_ms) as usize).min(10);
483 let fast_total_ms = fast_ms * fast_count as u64;
484 let remaining_ms = total_ms.saturating_sub(fast_total_ms);
485 let slow_count = (remaining_ms / slow_ms) as usize;
486
487 let fast_checks =
488 std::iter::repeat_n(std::time::Duration::from_millis(fast_ms), fast_count);
489 let slow_checks =
490 std::iter::repeat_n(std::time::Duration::from_millis(slow_ms), slow_count);
491 let mut elapsed_ms = 0u64;
492
493 for sleep_duration in fast_checks.chain(slow_checks) {
494 std::thread::sleep(sleep_duration);
495 elapsed_ms += sleep_duration.as_millis() as u64;
496 if process_group_terminated(pgid) {
497 debug!("process group {pgid} terminated after {signal_name} ({elapsed_ms} ms)",);
498 return Ok(true);
499 }
500 }
501
502 warn!(
504 "process group {pgid} did not respond to {signal_name} after {}ms, sending SIGKILL",
505 stop_timeout.as_millis()
506 );
507 let ret = unsafe { libc::killpg(pgid, libc::SIGKILL) };
508 if ret == -1 {
509 let err = std::io::Error::last_os_error();
510 if err.raw_os_error() != Some(libc::ESRCH) {
511 warn!("failed to send SIGKILL to process group {pgid}: {err}");
512 }
513 }
514
515 let sigkill_polls = PROCESS_GROUP_SIGKILL_WAIT.as_millis() / 50;
519 for _ in 0..sigkill_polls {
520 std::thread::sleep(std::time::Duration::from_millis(50));
521 if process_group_terminated(pgid) {
522 return Ok(true);
523 }
524 }
525 Err(miette::miette!(
528 "process group {pgid} still has members after SIGKILL \
529 (possibly stuck in uninterruptible sleep)"
530 ))
531 }
532
533 pub fn process_group_alive(&self, pid: u32) -> bool {
536 #[cfg(unix)]
537 {
538 !process_group_terminated(pid as i32)
539 }
540 #[cfg(not(unix))]
541 {
542 self.is_running(pid)
543 }
544 }
545
546 pub fn is_live(&self, pid: u32) -> bool {
550 if !self.is_running(pid) {
551 return false;
552 }
553 self.refresh_pids(&[pid]);
554 self.lock_system()
555 .process(sysinfo::Pid::from_u32(pid))
556 .is_none_or(|p| !is_exited_status(p.status()))
557 }
558
559 pub fn process_group_has_live_member(&self, pgid: u32) -> bool {
564 #[cfg(unix)]
565 {
566 if unsafe { libc::killpg(pgid as i32, 0) } != 0
569 && std::io::Error::last_os_error().raw_os_error() == Some(libc::ESRCH)
570 {
571 return false;
572 }
573 self.refresh_processes();
574 self.lock_system().processes().iter().any(|(pid, p)| {
575 !is_exited_status(p.status())
576 && unsafe { libc::getpgid(pid.as_u32() as i32) } == pgid as i32
577 })
578 }
579 #[cfg(not(unix))]
580 {
581 self.is_live(pgid)
582 }
583 }
584
585 #[cfg(target_os = "linux")]
586 fn kill_process_group_with_pidfds(
587 &self,
588 pid: u32,
589 expected_start_time: u64,
590 _stop_signal: i32,
591 stop_timeout: Option<std::time::Duration>,
592 ) -> Result<bool> {
593 let leader = match open_pidfd(pid) {
594 Ok(pidfd) => pidfd,
595 Err(err) => {
596 warn!("cannot securely identify process group {pid}: {err}");
597 return Ok(false);
598 }
599 };
600 if !self.start_time_matches(pid, expected_start_time) {
601 debug!("process group {pid} leader identity changed before signaling");
602 return Ok(false);
603 }
604
605 let mut members = vec![(pid, leader)];
606 if let Err(err) = stop_pidfds(&members) {
607 let _ = signal_pidfds(&members, libc::SIGCONT, "SIGCONT");
608 return Err(err);
609 }
610 if !pidfd_is_running(&members[0].1) {
611 debug!("process group {pid} leader exited before it could be frozen");
612 return Ok(false);
613 }
614
615 loop {
619 let known_members = members.len();
620 let added = match extend_process_group_pidfds(pid as i32, &mut members) {
621 Ok(added) => added,
622 Err(err) => {
623 let _ = signal_pidfds(&members, libc::SIGCONT, "SIGCONT");
624 return Err(miette::miette!(
625 "failed to scan pinned process group {pid}: {err}"
626 ));
627 }
628 };
629 if added == 0 {
630 break;
631 }
632 if let Err(err) = stop_pidfds(&members[known_members..]) {
633 let _ = signal_pidfds(&members, libc::SIGCONT, "SIGCONT");
634 return Err(err);
635 }
636 }
637
638 warn!(
639 "force-terminating {} pinned orphan process(es) in group {pid}",
640 members.len()
641 );
642 if let Err(err) = signal_pidfds(&members, libc::SIGKILL, "SIGKILL") {
643 let _ = signal_pidfds(&members, libc::SIGCONT, "SIGCONT");
644 return Err(err);
645 }
646
647 let exit_timeout = stop_timeout.unwrap_or_else(|| settings().supervisor_stop_timeout());
648 let checks = exit_timeout.as_millis().max(1).div_ceil(50) as usize;
649 for _ in 0..checks {
650 if members.iter().all(|(_, pidfd)| !pidfd_is_running(pidfd)) {
651 return Ok(true);
652 }
653 std::thread::sleep(std::time::Duration::from_millis(50));
654 }
655 if members.iter().all(|(_, pidfd)| !pidfd_is_running(pidfd)) {
656 return Ok(true);
657 }
658
659 warn!("one or more pinned processes in orphan group {pid} remained alive after SIGKILL");
660 let _ = signal_pidfds(&members, libc::SIGCONT, "SIGCONT");
661 Ok(false)
662 }
663
664 #[cfg(not(unix))]
665 fn kill_process_group(
666 &self,
667 pid: u32,
668 stop_signal: i32,
669 stop_timeout: Option<std::time::Duration>,
670 expected_start_time: Option<u64>,
671 ) -> Result<bool> {
672 #[cfg(windows)]
675 let _identity_handle = if let Some(expected) = expected_start_time {
676 let handle = match open_process_handle(pid) {
677 Ok(handle) => handle,
678 Err(err) => {
679 warn!("cannot securely identify process {pid}: {err}");
680 return Ok(false);
681 }
682 };
683 if process_start_token_from_handle(handle.0) != Some(expected) {
684 debug!("process {pid} identity changed before taskkill");
685 return Ok(false);
686 }
687 Some(handle)
688 } else {
689 None
690 };
691
692 #[cfg(not(windows))]
693 if let Some(expected) = expected_start_time
694 && !self.start_time_matches(pid, expected)
695 {
696 debug!("process {pid} identity changed before termination");
697 return Ok(false);
698 }
699
700 self.kill(pid, stop_signal, stop_timeout, expected_start_time)
701 }
702
703 #[cfg(not(target_os = "linux"))]
721 fn kill(
722 &self,
723 pid: u32,
724 stop_signal: i32,
725 stop_timeout: Option<std::time::Duration>,
726 expected_start_time: Option<u64>,
727 ) -> Result<bool> {
728 debug!("killing process {pid}");
729
730 #[cfg(windows)]
731 {
732 let _ = expected_start_time;
735 let job = open_process_handle(pid)
744 .ok()
745 .and_then(|process| crate::win_job::DaemonJob::open(pid, process.0));
746 if stop_signal == crate::config_types::StopSignal::SIGINT {
747 let stop_timeout =
748 stop_timeout.unwrap_or_else(|| settings().supervisor_stop_timeout());
749 if interrupt_and_wait(pid, stop_timeout) {
750 debug!("process {pid} exited after Ctrl+C");
751 if job.as_ref().is_some_and(job_outlives_termination) {
753 return Err(miette::miette!(
754 "process {pid} exited after Ctrl+C, but processes it started may \
755 still be running: its job was not terminated and emptied within {}s",
756 JOB_EXIT_WAIT.as_secs()
757 ));
758 }
759 return Ok(true);
760 }
761 }
762 let output = std::process::Command::new("taskkill")
770 .args(["/F", "/T", "/PID"])
771 .arg(pid.to_string())
772 .hide_console_window()
773 .output();
774 let taskkill_succeeded = match output {
775 Ok(o) if o.status.success() => {
776 debug!("taskkill /F /T /PID {pid} succeeded");
777 true
778 }
779 Ok(o) => {
780 debug!(
781 "taskkill /F /T /PID {pid} exited with status {}: {}",
782 o.status,
783 String::from_utf8_lossy(&o.stderr).trim()
784 );
785 false
786 }
787 Err(e) => {
788 debug!("failed to spawn taskkill for pid {pid}: {e}");
789 false
790 }
791 };
792 let job_outlived = job.as_ref().is_some_and(job_outlives_termination);
795 std::thread::sleep(std::time::Duration::from_millis(200));
799 if !taskkill_succeeded && self.is_running(pid) {
800 return Err(miette::miette!(
801 "taskkill failed and process {pid} is still running"
802 ));
803 }
804 if job_outlived {
807 return Err(miette::miette!(
808 "processes of daemon {pid} may still be running: its job was not \
809 terminated and emptied within {}s",
810 JOB_EXIT_WAIT.as_secs()
811 ));
812 }
813 Ok(true)
814 }
815
816 #[cfg(unix)]
817 {
818 let sysinfo_pid = sysinfo::Pid::from_u32(pid);
819 let signal_name = signal_name(stop_signal);
820 if let Some(expected) = expected_start_time
825 && !self.verify_start_time_before_signal(pid, expected)?
826 {
827 return Ok(false);
828 }
829 debug!("sending {signal_name} to process {pid}");
833 let ret = unsafe { libc::kill(pid as i32, stop_signal) };
834 if ret == -1 {
835 let err = std::io::Error::last_os_error();
836 if err.raw_os_error() == Some(libc::ESRCH) {
837 debug!("process {pid} no longer exists");
838 return Ok(false);
839 }
840 if err.raw_os_error() == Some(libc::EPERM) {
841 return Err(miette::miette!(
842 "failed to send {signal_name} to process {pid}: permission denied"
843 ));
844 }
845 return Err(miette::miette!(
846 "failed to send {signal_name} to process {pid}: {err}"
847 ));
848 }
849
850 let stop_timeout = stop_timeout.unwrap_or_else(|| settings().supervisor_stop_timeout());
853 let fast_ms = 10u64;
854 let slow_ms = 50u64;
855 let total_ms = stop_timeout.as_millis().max(1) as u64;
856 let fast_count = ((total_ms / fast_ms) as usize).min(10);
857 let fast_total_ms = fast_ms * fast_count as u64;
858 let remaining_ms = total_ms.saturating_sub(fast_total_ms);
859 let slow_count = (remaining_ms / slow_ms) as usize;
860
861 for i in 0..fast_count {
862 std::thread::sleep(std::time::Duration::from_millis(fast_ms));
863 self.refresh_pids(&[pid]);
864 if self.is_terminated_or_zombie(sysinfo_pid) {
865 debug!(
866 "process {pid} terminated after {signal_name} ({} ms)",
867 (i + 1) * fast_ms as usize
868 );
869 return Ok(true);
870 }
871 }
872
873 for i in 0..slow_count {
875 std::thread::sleep(std::time::Duration::from_millis(slow_ms));
876 self.refresh_pids(&[pid]);
877 if self.is_terminated_or_zombie(sysinfo_pid) {
878 debug!(
879 "process {pid} terminated after {signal_name} ({} ms)",
880 fast_total_ms + (i + 1) as u64 * slow_ms
881 );
882 return Ok(true);
883 }
884 }
885
886 if let Some(expected) = expected_start_time
891 && !self.start_time_matches(pid, expected)
892 {
893 debug!(
894 "process {pid} exited during the stop timeout and its PID was recycled; not sending SIGKILL"
895 );
896 return Ok(true);
897 }
898 warn!(
899 "process {pid} did not respond to {signal_name} after {}ms, sending SIGKILL",
900 stop_timeout.as_millis()
901 );
902 let ret = unsafe { libc::kill(pid as i32, libc::SIGKILL) };
903 if ret == -1 {
904 let err = std::io::Error::last_os_error();
905 if err.raw_os_error() != Some(libc::ESRCH) {
906 warn!("failed to send SIGKILL to process {pid}: {err}");
907 }
908 }
909
910 std::thread::sleep(std::time::Duration::from_millis(100));
912 Ok(true)
913 }
914 }
915
916 #[cfg(all(unix, not(target_os = "linux")))]
920 fn is_terminated_or_zombie(&self, sysinfo_pid: sysinfo::Pid) -> bool {
921 let system = self.lock_system();
922 match system.process(sysinfo_pid) {
923 None => true,
924 Some(process) => {
925 matches!(process.status(), sysinfo::ProcessStatus::Zombie)
926 }
927 }
928 }
929
930 pub(crate) fn refresh_processes(&self) {
931 let mut system = self.lock_system();
932 system.refresh_processes(ProcessesToUpdate::All, true);
933 #[cfg(windows)]
938 system.refresh_cpu_usage();
939 }
940
941 pub(crate) fn refresh_pids(&self, pids: &[u32]) {
944 let sysinfo_pids: Vec<sysinfo::Pid> =
945 pids.iter().map(|p| sysinfo::Pid::from_u32(*p)).collect();
946 self.lock_system()
947 .refresh_processes(ProcessesToUpdate::Some(&sysinfo_pids), true);
948 }
949
950 pub(crate) fn refresh_if_stale(&self) {
967 let mut last = self
968 .last_full_refresh
969 .lock()
970 .unwrap_or_else(|poisoned| poisoned.into_inner());
971 if last.is_none_or(|t| t.elapsed() >= FULL_REFRESH_INTERVAL) {
972 self.refresh_processes();
973 *last = Some(Instant::now());
974 }
975 }
976
977 pub fn get_batch_group_stats(&self, pids: &[u32]) -> Vec<(u32, Option<ProcessStats>)> {
983 if pids.is_empty() {
984 return Vec::new();
985 }
986
987 let system = self.lock_system();
988 let processes = system.processes();
989
990 let now = std::time::SystemTime::now()
991 .duration_since(std::time::UNIX_EPOCH)
992 .map(|d| d.as_secs())
993 .unwrap_or(0);
994
995 let mut children_map: std::collections::HashMap<sysinfo::Pid, Vec<sysinfo::Pid>> =
997 std::collections::HashMap::new();
998 for (child_pid, child) in processes {
999 if child.thread_kind().is_some() {
1002 continue;
1003 }
1004 if let Some(ppid) = child.parent() {
1005 children_map.entry(ppid).or_default().push(*child_pid);
1006 }
1007 }
1008
1009 pids.iter()
1010 .map(|&pid| {
1011 let root_pid = sysinfo::Pid::from_u32(pid);
1012 let Some(root) = processes.get(&root_pid) else {
1013 return (pid, None);
1014 };
1015
1016 let root_disk = root.disk_usage();
1017 let mut stats = ProcessStats {
1018 cpu_percent: root.cpu_usage(),
1019 memory_bytes: root.memory(),
1020 uptime_secs: now.saturating_sub(root.start_time()),
1021 disk_read_bytes: root_disk.read_bytes,
1022 disk_write_bytes: root_disk.written_bytes,
1023 };
1024
1025 let mut queue = std::collections::VecDeque::new();
1027 if let Some(direct_children) = children_map.get(&root_pid) {
1028 queue.extend(direct_children);
1029 }
1030 while let Some(child_pid) = queue.pop_front() {
1031 if let Some(child) = processes.get(&child_pid) {
1032 let disk = child.disk_usage();
1033 stats.cpu_percent += child.cpu_usage();
1034 stats.memory_bytes += child.memory();
1035 stats.disk_read_bytes += disk.read_bytes;
1036 stats.disk_write_bytes += disk.written_bytes;
1037 }
1038 if let Some(grandchildren) = children_map.get(&child_pid) {
1039 queue.extend(grandchildren);
1040 }
1041 }
1042
1043 (pid, Some(stats))
1044 })
1045 .collect()
1046 }
1047 pub fn refresh_and_get_batch_stats(&self, pids: &[u32]) -> HashMap<u32, ProcessStats> {
1057 self.refresh_processes();
1058 self.get_batch_group_stats(pids)
1059 .into_iter()
1060 .filter_map(|(pid, stats)| stats.map(|s| (pid, s)))
1061 .collect()
1062 }
1063
1064 pub fn refresh_and_get_batch_stats_if_stale(&self, pids: &[u32]) -> HashMap<u32, ProcessStats> {
1071 self.refresh_if_stale();
1072 self.get_batch_group_stats(pids)
1073 .into_iter()
1074 .filter_map(|(pid, stats)| stats.map(|s| (pid, s)))
1075 .collect()
1076 }
1077
1078 pub fn get_batch_tree_stats_map(&self, pids: &[u32]) -> HashMap<u32, ProcessStats> {
1080 self.get_batch_group_stats(pids)
1081 .into_iter()
1082 .filter_map(|(pid, stats)| stats.map(|stats| (pid, stats)))
1083 .collect()
1084 }
1085
1086 pub fn get_stats(&self, pid: u32) -> Option<ProcessStats> {
1088 self.get_batch_group_stats(&[pid])
1089 .into_iter()
1090 .next()
1091 .and_then(|(_, stats)| stats)
1092 }
1093
1094 pub fn get_extended_stats(&self, pid: u32) -> Option<ExtendedProcessStats> {
1096 let system = self.lock_system();
1097 let processes = system.processes();
1098 let root_pid = sysinfo::Pid::from_u32(pid);
1099 let p = processes.get(&root_pid)?;
1100
1101 let now = std::time::SystemTime::now()
1102 .duration_since(std::time::UNIX_EPOCH)
1103 .map(|d| d.as_secs())
1104 .unwrap_or(0);
1105
1106 let root_disk = p.disk_usage();
1107 let mut aggregate_stats = ProcessStats {
1108 cpu_percent: p.cpu_usage(),
1109 memory_bytes: p.memory(),
1110 uptime_secs: now.saturating_sub(p.start_time()),
1111 disk_read_bytes: root_disk.read_bytes,
1112 disk_write_bytes: root_disk.written_bytes,
1113 };
1114
1115 let mut children_map: HashMap<sysinfo::Pid, Vec<sysinfo::Pid>> = HashMap::new();
1116 for (child_pid, child) in processes {
1117 if let Some(ppid) = child.parent() {
1118 children_map.entry(ppid).or_default().push(*child_pid);
1119 }
1120 }
1121
1122 let mut queue = std::collections::VecDeque::new();
1123 if let Some(direct_children) = children_map.get(&root_pid) {
1124 queue.extend(direct_children);
1125 }
1126 while let Some(child_pid) = queue.pop_front() {
1127 if let Some(child) = processes.get(&child_pid) {
1128 let disk = child.disk_usage();
1129 aggregate_stats.cpu_percent += child.cpu_usage();
1130 aggregate_stats.memory_bytes += child.memory();
1131 aggregate_stats.disk_read_bytes += disk.read_bytes;
1132 aggregate_stats.disk_write_bytes += disk.written_bytes;
1133 }
1134 if let Some(grandchildren) = children_map.get(&child_pid) {
1135 queue.extend(grandchildren);
1136 }
1137 }
1138
1139 Some(ExtendedProcessStats {
1140 name: p.name().to_string_lossy().to_string(),
1141 status: format!("{:?}", p.status()),
1142 cpu_percent: aggregate_stats.cpu_percent,
1143 memory_bytes: aggregate_stats.memory_bytes,
1144 virtual_memory_bytes: p.virtual_memory(),
1145 uptime_secs: aggregate_stats.uptime_secs,
1146 thread_count: p.tasks().map(|t| t.len()).unwrap_or(0),
1147 })
1148 }
1149}
1150
1151#[cfg(target_os = "linux")]
1152fn process_start_token(pid: u32) -> Option<u64> {
1153 let stat = std::fs::read_to_string(format!("/proc/{pid}/stat")).ok()?;
1154 let command_end = stat.rfind(')')?;
1155 stat.get(command_end + 1..)?
1157 .split_whitespace()
1158 .nth(19)?
1159 .parse()
1160 .ok()
1161}
1162
1163#[cfg(target_os = "macos")]
1164fn process_start_token(pid: u32) -> Option<u64> {
1165 let mut info = std::mem::MaybeUninit::<libc::proc_bsdinfo>::zeroed();
1166 let size = std::mem::size_of::<libc::proc_bsdinfo>() as i32;
1167 let read = unsafe {
1168 libc::proc_pidinfo(
1169 pid as i32,
1170 libc::PROC_PIDTBSDINFO,
1171 0,
1172 info.as_mut_ptr().cast(),
1173 size,
1174 )
1175 };
1176 if read != size {
1177 return None;
1178 }
1179 let info = unsafe { info.assume_init() };
1180 info.pbi_start_tvsec
1181 .checked_mul(1_000_000)?
1182 .checked_add(info.pbi_start_tvusec)
1183}
1184
1185#[cfg(windows)]
1186fn process_start_token(pid: u32) -> Option<u64> {
1187 let handle = open_process_handle(pid).ok()?;
1188 process_start_token_from_handle(handle.0)
1189}
1190
1191#[cfg(windows)]
1192pub(crate) fn process_start_token_from_handle(handle: HANDLE) -> Option<u64> {
1193 let mut creation = FILETIME {
1194 dwLowDateTime: 0,
1195 dwHighDateTime: 0,
1196 };
1197 let mut exit = creation;
1198 let mut kernel = creation;
1199 let mut user = creation;
1200 let ok = unsafe { GetProcessTimes(handle, &mut creation, &mut exit, &mut kernel, &mut user) };
1201 if ok == 0 {
1202 return None;
1203 }
1204
1205 Some((u64::from(creation.dwHighDateTime) << 32) | u64::from(creation.dwLowDateTime))
1206}
1207
1208#[cfg(windows)]
1209struct ProcessHandle(HANDLE);
1210
1211#[cfg(windows)]
1212impl Drop for ProcessHandle {
1213 fn drop(&mut self) {
1214 unsafe {
1215 CloseHandle(self.0);
1216 }
1217 }
1218}
1219
1220#[cfg(windows)]
1221fn open_process_handle(pid: u32) -> std::io::Result<ProcessHandle> {
1222 let handle = unsafe { OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, 0, pid) };
1223 if handle.is_null() {
1224 return Err(std::io::Error::last_os_error());
1225 }
1226 Ok(ProcessHandle(handle))
1227}
1228
1229#[cfg(windows)]
1232fn job_outlives_termination(job: &crate::win_job::DaemonJob) -> bool {
1233 let terminated = job.terminate();
1236 let emptied = job.wait_until_empty(JOB_EXIT_WAIT);
1237 debug!("terminated a daemon's job: {terminated}; emptied: {emptied}");
1238 !emptied
1239}
1240
1241#[cfg(windows)]
1247fn interrupt_and_wait(pid: u32, timeout: Duration) -> bool {
1248 use crate::console_ctrl::EXIT_SHARED_CONSOLE;
1249 use windows_sys::Win32::Foundation::WAIT_OBJECT_0;
1250 use windows_sys::Win32::System::Threading::{PROCESS_SYNCHRONIZE, WaitForSingleObject};
1251
1252 let handle = unsafe { OpenProcess(PROCESS_SYNCHRONIZE, 0, pid) };
1255 if handle.is_null() {
1256 debug!(
1257 "cannot wait for process {pid}: {}",
1258 std::io::Error::last_os_error()
1259 );
1260 return false;
1261 }
1262 let handle = ProcessHandle(handle);
1263
1264 let deadline = Instant::now().checked_add(timeout);
1269 let millis_left = || {
1270 let Some(deadline) = deadline else {
1271 return u32::MAX - 1;
1272 };
1273 let left = deadline.saturating_duration_since(Instant::now());
1274 u32::try_from(left.as_millis()).unwrap_or(u32::MAX - 1)
1275 };
1276
1277 let mut helper = match std::process::Command::new(&*crate::env::PITCHFORK_BIN)
1278 .args(["interrupt", "--pid"])
1279 .arg(pid.to_string())
1280 .arg("--supervisor-pid")
1281 .arg(std::process::id().to_string())
1282 .stdin(std::process::Stdio::null())
1283 .stdout(std::process::Stdio::null())
1284 .stderr(std::process::Stdio::piped())
1285 .hide_console_window()
1286 .spawn()
1287 {
1288 Ok(helper) => helper,
1289 Err(e) => {
1290 debug!("failed to spawn pitchfork interrupt for pid {pid}: {e}");
1291 return false;
1292 }
1293 };
1294 let helper_handle = std::os::windows::io::AsRawHandle::as_raw_handle(&helper);
1295 if unsafe { WaitForSingleObject(helper_handle as HANDLE, millis_left()) } != WAIT_OBJECT_0 {
1296 debug!("pitchfork interrupt for pid {pid} did not finish within {timeout:?}");
1297 let _ = helper.kill();
1298 let _ = helper.wait();
1299 return false;
1300 }
1301 match helper.wait_with_output() {
1302 Ok(o) if o.status.success() => {}
1303 Ok(o) if o.status.code() == Some(EXIT_SHARED_CONSOLE) => {
1304 debug!("process {pid} shares the supervisor's console; not sending Ctrl+C");
1305 return false;
1306 }
1307 Ok(o) => {
1308 debug!(
1309 "pitchfork interrupt for pid {pid} exited with status {}: {}",
1310 o.status,
1311 String::from_utf8_lossy(&o.stderr).trim()
1312 );
1313 return false;
1314 }
1315 Err(e) => {
1316 debug!("failed to wait for pitchfork interrupt for pid {pid}: {e}");
1317 return false;
1318 }
1319 }
1320
1321 debug!("sent Ctrl+C to process {pid}, waiting up to {timeout:?} in all");
1322 unsafe { WaitForSingleObject(handle.0, millis_left()) == WAIT_OBJECT_0 }
1323}
1324
1325#[cfg(not(any(target_os = "linux", target_os = "macos", windows)))]
1326fn process_start_token(pid: u32) -> Option<u64> {
1327 let mut system = sysinfo::System::new();
1328 let sysinfo_pid = sysinfo::Pid::from_u32(pid);
1329 system.refresh_processes(ProcessesToUpdate::Some(&[sysinfo_pid]), true);
1330 system
1331 .process(sysinfo_pid)
1332 .map(|process| process.start_time())
1333}
1334
1335#[cfg(target_os = "linux")]
1336fn open_pidfd(pid: u32) -> std::io::Result<OwnedFd> {
1337 let fd = unsafe { libc::syscall(libc::SYS_pidfd_open, pid, 0) };
1338 if fd < 0 {
1339 return Err(std::io::Error::last_os_error());
1340 }
1341 Ok(unsafe { OwnedFd::from_raw_fd(fd as i32) })
1342}
1343
1344#[cfg(target_os = "linux")]
1345fn pidfd_is_running(pidfd: &OwnedFd) -> bool {
1346 match try_pidfd_is_running(pidfd) {
1347 Ok(running) => running,
1348 Err(err) => {
1349 warn!("failed to poll pidfd {}: {err}", pidfd.as_raw_fd());
1350 true
1351 }
1352 }
1353}
1354
1355#[cfg(target_os = "linux")]
1356fn try_pidfd_is_running(pidfd: &OwnedFd) -> std::io::Result<bool> {
1357 let mut pollfd = libc::pollfd {
1358 fd: pidfd.as_raw_fd(),
1359 events: libc::POLLIN,
1360 revents: 0,
1361 };
1362 let result = unsafe { libc::poll(&mut pollfd, 1, 0) };
1363 if result < 0 {
1364 return Err(std::io::Error::last_os_error());
1365 }
1366 Ok(result == 0)
1367}
1368
1369#[cfg(target_os = "linux")]
1370fn signal_pidfds(members: &[(u32, OwnedFd)], signal: i32, signal_name: &str) -> Result<()> {
1371 for (pid, pidfd) in members {
1372 if !pidfd_is_running(pidfd) {
1373 continue;
1374 }
1375 let result = unsafe {
1376 libc::syscall(
1377 libc::SYS_pidfd_send_signal,
1378 pidfd.as_raw_fd(),
1379 signal,
1380 std::ptr::null::<libc::siginfo_t>(),
1381 0,
1382 )
1383 };
1384 if result == -1 {
1385 let err = std::io::Error::last_os_error();
1386 if err.raw_os_error() == Some(libc::ESRCH) {
1387 continue;
1388 }
1389 return Err(miette::miette!(
1390 "failed to send {signal_name} to pinned process {pid}: {err}"
1391 ));
1392 }
1393 }
1394 Ok(())
1395}
1396
1397#[cfg(target_os = "linux")]
1398fn stop_pidfds(members: &[(u32, OwnedFd)]) -> Result<()> {
1399 signal_pidfds(members, libc::SIGSTOP, "SIGSTOP")?;
1400 for _ in 0..200 {
1401 if members.iter().all(|(pid, pidfd)| {
1402 !pidfd_is_running(pidfd) || matches!(linux_process_state(*pid), Some('T' | 't'))
1403 }) {
1404 return Ok(());
1405 }
1406 std::thread::sleep(std::time::Duration::from_millis(5));
1407 }
1408 Err(miette::miette!(
1409 "timed out while freezing orphan process group"
1410 ))
1411}
1412
1413#[cfg(target_os = "linux")]
1414fn extend_process_group_pidfds(
1415 pgid: i32,
1416 members: &mut Vec<(u32, OwnedFd)>,
1417) -> std::io::Result<usize> {
1418 let entries = std::fs::read_dir("/proc")?;
1419 let mut added = 0;
1420 for entry in entries {
1421 let entry = entry?;
1422 let Some(pid) = entry
1423 .file_name()
1424 .to_str()
1425 .and_then(|name| name.parse::<u32>().ok())
1426 else {
1427 continue;
1428 };
1429 let Some(observed_identity) = linux_process_identity(pid) else {
1430 continue;
1431 };
1432 if observed_identity.0 != pgid {
1433 continue;
1434 }
1435 let mut already_pinned = false;
1436 for (known_pid, pidfd) in members.iter() {
1437 if *known_pid == pid && try_pidfd_is_running(pidfd)? {
1438 already_pinned = true;
1439 break;
1440 }
1441 }
1442 if already_pinned {
1443 continue;
1444 }
1445
1446 let pidfd = match open_pidfd(pid) {
1447 Ok(pidfd) => pidfd,
1448 Err(err) if err.raw_os_error() == Some(libc::ESRCH) => continue,
1449 Err(err) => return Err(err),
1450 };
1451 if linux_process_identity(pid) != Some(observed_identity) {
1452 return Err(std::io::Error::other(format!(
1453 "process {pid} identity changed while pinning group {pgid}"
1454 )));
1455 }
1456 members.push((pid, pidfd));
1457 added += 1;
1458 }
1459 Ok(added)
1460}
1461
1462#[cfg(target_os = "linux")]
1463fn linux_process_identity(pid: u32) -> Option<(i32, u64)> {
1464 let stat = std::fs::read_to_string(format!("/proc/{pid}/stat")).ok()?;
1465 let command_end = stat.rfind(')')?;
1466 let fields: Vec<_> = stat.get(command_end + 1..)?.split_whitespace().collect();
1467 Some((fields.get(2)?.parse().ok()?, fields.get(19)?.parse().ok()?))
1470}
1471
1472#[cfg(target_os = "linux")]
1473fn linux_process_state(pid: u32) -> Option<char> {
1474 let stat = std::fs::read_to_string(format!("/proc/{pid}/stat")).ok()?;
1475 let command_end = stat.rfind(')')?;
1476 stat.get(command_end + 1..)?
1477 .split_whitespace()
1478 .next()?
1479 .chars()
1480 .next()
1481}
1482
1483#[derive(Debug, Clone, Copy)]
1484pub struct ProcessStats {
1485 pub cpu_percent: f32,
1486 pub memory_bytes: u64,
1487 pub uptime_secs: u64,
1488 pub disk_read_bytes: u64,
1489 pub disk_write_bytes: u64,
1490}
1491
1492impl ProcessStats {
1493 pub fn memory_display(&self) -> String {
1494 format_bytes(self.memory_bytes)
1495 }
1496
1497 pub fn cpu_display(&self) -> String {
1498 format!("{:.1}%", self.cpu_percent)
1499 }
1500
1501 pub fn uptime_display(&self) -> String {
1502 format_duration(self.uptime_secs)
1503 }
1504
1505 pub fn disk_read_display(&self) -> String {
1506 format_bytes_per_sec(self.disk_read_bytes)
1507 }
1508
1509 pub fn disk_write_display(&self) -> String {
1510 format_bytes_per_sec(self.disk_write_bytes)
1511 }
1512}
1513
1514#[derive(Debug, Clone)]
1515pub struct ExtendedProcessStats {
1516 pub name: String,
1517 pub status: String,
1518 pub cpu_percent: f32,
1519 pub memory_bytes: u64,
1520 pub virtual_memory_bytes: u64,
1521 pub uptime_secs: u64,
1522 pub thread_count: usize,
1523}
1524
1525fn format_bytes(bytes: u64) -> String {
1526 humanbyte::to_string(bytes, humanbyte::Format::IEC)
1527}
1528
1529pub(crate) fn format_duration(secs: u64) -> String {
1530 if secs < 60 {
1531 format!("{secs}s")
1532 } else if secs < 3600 {
1533 format!("{}m {}s", secs / 60, secs % 60)
1534 } else if secs < 86400 {
1535 let hours = secs / 3600;
1536 let mins = (secs % 3600) / 60;
1537 format!("{hours}h {mins}m")
1538 } else {
1539 let days = secs / 86400;
1540 let hours = (secs % 86400) / 3600;
1541 format!("{days}d {hours}h")
1542 }
1543}
1544
1545fn format_bytes_per_sec(bytes: u64) -> String {
1546 format!("{}/s", humanbyte::to_string(bytes, humanbyte::Format::IEC))
1547}
1548
1549#[cfg(unix)]
1565fn process_group_terminated(pgid: i32) -> bool {
1566 unsafe { libc::killpg(pgid, 0) != 0 }
1567}
1568
1569#[cfg(unix)]
1570fn signal_name(sig: i32) -> &'static str {
1571 match sig {
1572 libc::SIGHUP => "SIGHUP",
1573 libc::SIGINT => "SIGINT",
1574 libc::SIGQUIT => "SIGQUIT",
1575 libc::SIGTERM => "SIGTERM",
1576 libc::SIGUSR1 => "SIGUSR1",
1577 libc::SIGUSR2 => "SIGUSR2",
1578 libc::SIGKILL => "SIGKILL",
1579 _ => "UNKNOWN",
1580 }
1581}
1582
1583fn is_exited_status(status: sysinfo::ProcessStatus) -> bool {
1585 matches!(
1586 status,
1587 sysinfo::ProcessStatus::Zombie | sysinfo::ProcessStatus::Dead
1588 )
1589}
1590
1591#[cfg(test)]
1592mod format_tests {
1593 use super::*;
1594
1595 #[test]
1596 fn process_start_time_check_rejects_mismatch() {
1597 let procs = Procs::new();
1598 let pid = std::process::id();
1599 procs.refresh_pids(&[pid]);
1600 let actual = procs
1601 .start_time(pid)
1602 .expect("current process should have a start time");
1603
1604 assert_ne!(procs.start_time(pid), Some(actual.saturating_add(1)));
1605 }
1606
1607 #[test]
1608 fn test_format_bytes() {
1609 assert_eq!(format_bytes(512), "512 B");
1610 assert_eq!(format_bytes(1024), "1.0 KiB");
1611 assert_eq!(format_bytes(1536), "1.5 KiB");
1612 assert_eq!(format_bytes(50 * 1024 * 1024), "50.0 MiB");
1613 assert_eq!(format_bytes(3 * 1024 * 1024 * 1024), "3.0 GiB");
1614 assert_eq!(format_bytes(1100 * 1024 * 1024 * 1024), "1.1 TiB");
1616 }
1617
1618 #[test]
1619 fn test_format_bytes_per_sec() {
1620 assert_eq!(format_bytes_per_sec(512), "512 B/s");
1621 assert_eq!(format_bytes_per_sec(1536), "1.5 KiB/s");
1622 assert_eq!(format_bytes_per_sec(2 * 1024 * 1024), "2.0 MiB/s");
1623 }
1624}
1625
1626#[cfg(all(test, unix))]
1627mod tests {
1628 use super::*;
1629 use std::os::unix::process::CommandExt;
1630 use std::process::{Child, Command, Stdio};
1631 use std::time::{Duration, Instant};
1632
1633 struct ChildGuard(Child);
1634
1635 impl Drop for ChildGuard {
1636 fn drop(&mut self) {
1637 let pid = self.0.id() as i32;
1638 let _ = unsafe { libc::killpg(pid, libc::SIGKILL) };
1640 let _ = self.0.wait();
1641 }
1642 }
1643
1644 #[tokio::test]
1645 async fn orphan_identity_checked_group_kill_rejects_mismatch() {
1646 let mut command = Command::new("sleep");
1647 command
1648 .arg("30")
1649 .stdin(Stdio::null())
1650 .stdout(Stdio::null())
1651 .stderr(Stdio::null());
1652 unsafe {
1653 command.pre_exec(|| {
1654 if libc::setsid() == -1 {
1655 return Err(std::io::Error::last_os_error());
1656 }
1657 Ok(())
1658 });
1659 }
1660
1661 let child = command.spawn().expect("failed to spawn test process");
1662 let pid = child.id();
1663 let _child = ChildGuard(child);
1664
1665 PROCS.refresh_pids(&[pid]);
1666 let actual_start_time = PROCS
1667 .start_time(pid)
1668 .expect("test process should have a start time");
1669
1670 let killed = PROCS
1671 .kill_process_group_if_start_time_matches_async(
1672 pid,
1673 Some(actual_start_time.saturating_add(1)),
1674 libc::SIGTERM,
1675 Some(Duration::from_millis(100)),
1676 )
1677 .await
1678 .expect("identity-checked kill should not error");
1679
1680 assert!(!killed);
1681 assert!(PROCS.is_running(pid), "mismatched process must survive");
1682 }
1683
1684 #[cfg(all(unix, not(target_os = "linux")))]
1689 #[tokio::test]
1690 async fn identity_checked_group_kill_reverifies_inside_blocking_op() {
1691 let mut command = Command::new("sleep");
1692 command
1693 .arg("30")
1694 .stdin(Stdio::null())
1695 .stdout(Stdio::null())
1696 .stderr(Stdio::null());
1697 unsafe {
1698 command.pre_exec(|| {
1699 if libc::setsid() == -1 {
1700 return Err(std::io::Error::last_os_error());
1701 }
1702 Ok(())
1703 });
1704 }
1705
1706 let child = command.spawn().expect("failed to spawn test process");
1707 let pid = child.id();
1708 let mut child = ChildGuard(child);
1709
1710 PROCS.refresh_pids(&[pid]);
1711 let actual_start_time = PROCS
1712 .start_time(pid)
1713 .expect("test process should have a start time");
1714
1715 let killed = PROCS
1716 .kill_process_group_if_start_time_matches_async(
1717 pid,
1718 Some(actual_start_time),
1719 libc::SIGTERM,
1720 Some(Duration::from_millis(100)),
1721 )
1722 .await
1723 .expect("identity-checked kill should not error");
1724
1725 assert!(killed, "matching generation must be signalled");
1726 let deadline = Instant::now() + Duration::from_secs(2);
1729 while child.0.try_wait().unwrap().is_none() {
1730 assert!(Instant::now() < deadline, "signalled child must exit");
1731 tokio::time::sleep(Duration::from_millis(10)).await;
1732 }
1733 assert!(
1734 !PROCS.is_running(pid),
1735 "signalled process group must be gone"
1736 );
1737 }
1738
1739 #[test]
1740 fn get_stats_includes_descendant_rss() {
1741 let mut command = Command::new("sh");
1742 command
1743 .args(["-c", "sleep 30 & wait"])
1744 .stdin(Stdio::null())
1745 .stdout(Stdio::null())
1746 .stderr(Stdio::null());
1747 unsafe {
1748 command.pre_exec(|| {
1749 if libc::setsid() == -1 {
1750 return Err(std::io::Error::last_os_error());
1751 }
1752 Ok(())
1753 });
1754 }
1755
1756 let parent = command.spawn().expect("failed to spawn process tree");
1757 let parent_pid = parent.id();
1758 let _parent = ChildGuard(parent);
1759
1760 let procs = Procs::new();
1761 let deadline = Instant::now() + Duration::from_secs(5);
1762 let mut child_pids = Vec::new();
1763 while Instant::now() < deadline {
1764 procs.refresh_processes();
1765 child_pids = procs.all_children(parent_pid);
1766 if !child_pids.is_empty() {
1767 break;
1768 }
1769 std::thread::sleep(Duration::from_millis(50));
1770 }
1771 assert!(
1772 !child_pids.is_empty(),
1773 "test process tree did not appear under parent pid {parent_pid}"
1774 );
1775
1776 procs.refresh_processes();
1777 child_pids = procs.all_children(parent_pid);
1778 assert!(
1779 !child_pids.is_empty(),
1780 "test process tree disappeared under parent pid {parent_pid}"
1781 );
1782 let root_pid = sysinfo::Pid::from_u32(parent_pid);
1783 let direct_memory = {
1784 let system = procs.lock_system();
1785 system
1786 .process(root_pid)
1787 .expect("parent process should exist")
1788 .memory()
1789 };
1790 let descendant_memory = {
1791 let system = procs.lock_system();
1792 child_pids
1793 .iter()
1794 .filter_map(|pid| system.process(sysinfo::Pid::from_u32(*pid)))
1795 .map(|process| process.memory())
1796 .sum::<u64>()
1797 };
1798 assert!(
1799 descendant_memory > 0,
1800 "descendants {child_pids:?} should have nonzero RSS"
1801 );
1802
1803 let stats = procs
1804 .get_stats(parent_pid)
1805 .expect("parent process should have aggregate stats");
1806
1807 assert_eq!(
1808 stats.memory_bytes,
1809 direct_memory + descendant_memory,
1810 "get_stats should include descendant RSS for parent pid {parent_pid}; \
1811 descendants: {child_pids:?}, direct RSS: {direct_memory}, \
1812 descendant RSS: {descendant_memory}, reported RSS: {}",
1813 stats.memory_bytes
1814 );
1815 }
1816
1817 #[test]
1818 fn full_refresh_is_throttled_by_ttl() {
1819 let procs = Procs::new();
1820
1821 assert!(
1823 procs.last_full_refresh.lock().unwrap().is_none(),
1824 "fresh Procs should have no recorded refresh"
1825 );
1826 procs.refresh_if_stale();
1827 let first = procs.last_full_refresh.lock().unwrap().unwrap();
1828 assert!(first.elapsed() < FULL_REFRESH_INTERVAL);
1829
1830 procs.refresh_if_stale();
1833 let second = procs.last_full_refresh.lock().unwrap().unwrap();
1834 assert_eq!(
1835 second, first,
1836 "refresh_if_stale within TTL must skip the refresh and keep the timestamp"
1837 );
1838
1839 let expired = Instant::now()
1842 .checked_sub(FULL_REFRESH_INTERVAL + Duration::from_secs(1))
1843 .expect("system has been up long enough to backdate by 6s");
1844 *procs.last_full_refresh.lock().unwrap() = Some(expired);
1845 procs.refresh_if_stale();
1846 let third = procs.last_full_refresh.lock().unwrap().unwrap();
1847 assert!(
1848 third > expired,
1849 "refresh_if_stale after expired TTL must refresh and advance the timestamp"
1850 );
1851 assert!(
1852 third.elapsed() < FULL_REFRESH_INTERVAL,
1853 "fresh timestamp after expired-TTL refresh should be recent"
1854 );
1855 }
1856}