1use anyhow::Result;
2use serde::{Deserialize, Serialize};
3
4#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
8pub struct ProcessIdentity {
9 pub start_marker: u64,
10 pub executable: String,
11}
12
13pub fn run_with_timeout(
25 mut cmd: std::process::Command,
26 timeout: std::time::Duration,
27) -> Option<std::process::Output> {
28 use std::process::Stdio;
29 use std::time::Instant;
30
31 let mut child = cmd
32 .stdin(Stdio::null())
33 .stdout(Stdio::piped())
34 .stderr(Stdio::piped())
35 .spawn()
36 .ok()?;
37
38 let start = Instant::now();
39 loop {
40 match child.try_wait() {
41 Ok(Some(_)) => return child.wait_with_output().ok(),
43 Ok(None) => {
44 if start.elapsed() >= timeout {
45 let _ = child.kill();
46 let _ = child.wait();
47 return None;
48 }
49 std::thread::sleep(std::time::Duration::from_millis(50));
50 }
51 Err(_) => return None,
52 }
53 }
54}
55
56pub use super::detached::{DetachedChild, DetachedStderr, spawn_detached};
60
61pub use lean_ctx_fs::process::{
62 MAX_PID, PidFileError, PidRefusal, group_target, is_alive, parse_pid_file, signal_target,
63};
64
65pub fn read_pid_file(path: &std::path::Path) -> Option<u32> {
71 let text = std::fs::read_to_string(path).ok()?;
72 match parse_pid_file(&text) {
73 Ok(pid) => Some(pid),
74 Err(error) => {
75 tracing::warn!(
76 "removing stale PID file {}: {error}; no process is signalled",
77 path.display()
78 );
79 let _ = std::fs::remove_file(path);
80 None
81 }
82 }
83}
84
85pub fn identity(pid: u32) -> Option<ProcessIdentity> {
92 #[cfg(target_os = "macos")]
93 {
94 macos_process_identity(pid)
95 }
96 #[cfg(target_os = "linux")]
97 {
98 linux_process_identity(pid)
99 }
100 #[cfg(windows)]
101 {
102 windows_process_identity(pid)
103 }
104 #[cfg(target_os = "freebsd")]
105 {
106 freebsd_process_identity(pid)
107 }
108 #[cfg(not(any(
109 target_os = "macos",
110 target_os = "linux",
111 target_os = "freebsd",
112 windows
113 )))]
114 {
115 let _ = pid;
116 None
117 }
118}
119
120#[cfg(target_os = "freebsd")]
125fn freebsd_process_identity(pid: u32) -> Option<ProcessIdentity> {
126 let info = freebsd_kinfo(pid)?;
127 let pid = info.ki_pid;
128 let start_marker = u64::try_from(info.ki_start.tv_sec)
129 .ok()?
130 .checked_mul(1_000_000)?
131 .checked_add(u64::try_from(info.ki_start.tv_usec).ok()?)?;
132 freebsd_identity_with_path(pid, start_marker)
133}
134
135#[cfg(target_os = "freebsd")]
136fn freebsd_kinfo(pid: u32) -> Option<libc::kinfo_proc> {
137 let pid = libc::c_int::try_from(pid).ok()?;
138
139 let mut info = std::mem::MaybeUninit::<libc::kinfo_proc>::zeroed();
140 let mut info_len = std::mem::size_of::<libc::kinfo_proc>();
141 let mib = [libc::CTL_KERN, libc::KERN_PROC, libc::KERN_PROC_PID, pid];
142 let rc = unsafe {
145 libc::sysctl(
146 mib.as_ptr(),
147 4,
148 info.as_mut_ptr().cast(),
149 &mut info_len,
150 std::ptr::null(),
151 0,
152 )
153 };
154 if rc != 0 || info_len != std::mem::size_of::<libc::kinfo_proc>() {
155 return None;
156 }
157 let info = unsafe { info.assume_init() };
159 (info.ki_pid == pid).then_some(info)
160}
161
162#[cfg(target_os = "freebsd")]
163fn freebsd_identity_with_path(pid: libc::c_int, start_marker: u64) -> Option<ProcessIdentity> {
164 let mut path = [0_u8; libc::PATH_MAX as usize];
165 let mut path_len = path.len();
166 let mib = [
167 libc::CTL_KERN,
168 libc::KERN_PROC,
169 libc::KERN_PROC_PATHNAME,
170 pid,
171 ];
172 let rc = unsafe {
174 libc::sysctl(
175 mib.as_ptr(),
176 4,
177 path.as_mut_ptr().cast(),
178 &mut path_len,
179 std::ptr::null(),
180 0,
181 )
182 };
183 if rc != 0 || path_len == 0 {
184 return None;
185 }
186 let bytes = path.get(..path_len)?;
188 let bytes = bytes.strip_suffix(&[0]).unwrap_or(bytes);
189 if bytes.is_empty() {
190 return None;
191 }
192 Some(ProcessIdentity {
193 start_marker,
194 executable: String::from_utf8_lossy(bytes).into_owned(),
195 })
196}
197
198pub fn matches_identity(pid: u32, expected: &ProcessIdentity) -> bool {
201 identity(pid).as_ref() == Some(expected)
202}
203
204#[must_use]
213pub fn installed_executable_path(path: &str) -> String {
214 let path = path.strip_suffix(" (deleted)").unwrap_or(path);
215 let name_at = path.rfind(['/', '\\']).map_or(0, |i| i + 1);
216 let (dir, name) = path.split_at(name_at);
217 let lower = name.to_ascii_lowercase();
218 let sidecar = lower
219 .strip_prefix("lean-ctx.old")
220 .and_then(|rest| rest.strip_suffix(".exe"))
221 .is_some_and(is_parked_sidecar_suffix);
222 if sidecar {
223 format!("{dir}lean-ctx.exe")
224 } else {
225 path.to_string()
226 }
227}
228
229fn is_parked_sidecar_suffix(middle: &str) -> bool {
231 if middle.is_empty() {
232 return true;
233 }
234 let Some(rest) = middle.strip_prefix('-') else {
235 return false;
236 };
237 let mut parts = rest.split('-');
238 let digits =
239 |p: Option<&str>| p.is_some_and(|p| !p.is_empty() && p.bytes().all(|b| b.is_ascii_digit()));
240 digits(parts.next()) && digits(parts.next()) && parts.next().is_none()
241}
242
243#[must_use]
247pub fn same_process(expected: &ProcessIdentity, live: &ProcessIdentity) -> bool {
248 if expected.start_marker != live.start_marker {
249 return false;
250 }
251 let a = installed_executable_path(&expected.executable);
252 let b = installed_executable_path(&live.executable);
253 a == b || (cfg!(windows) && a.eq_ignore_ascii_case(&b))
254}
255
256#[must_use]
258pub fn is_same_process(pid: u32, expected: &ProcessIdentity) -> bool {
259 identity(pid).is_some_and(|live| same_process(expected, &live))
260}
261
262#[must_use]
267pub fn owned_by_current_user(pid: u32) -> bool {
268 #[cfg(target_os = "macos")]
269 {
270 let me = unsafe { libc::geteuid() };
272 macos_bsd_info(pid).is_some_and(|info| info.pbi_uid == me)
273 }
274 #[cfg(target_os = "linux")]
275 {
276 let me = unsafe { libc::geteuid() };
278 std::fs::read_to_string(format!("/proc/{pid}/status"))
279 .ok()
280 .and_then(|status| {
281 let line = status.lines().find(|l| l.starts_with("Uid:"))?;
282 line.split_whitespace().nth(2)?.parse::<u32>().ok()
283 })
284 .is_some_and(|euid| euid == me)
285 }
286 #[cfg(target_os = "freebsd")]
287 {
288 let me = unsafe { libc::geteuid() };
290 freebsd_kinfo(pid).is_some_and(|info| info.ki_uid == me)
291 }
292 #[cfg(windows)]
293 {
294 windows_owned_by_current_user(pid)
295 }
296 #[cfg(not(any(
297 target_os = "macos",
298 target_os = "linux",
299 target_os = "freebsd",
300 windows
301 )))]
302 {
303 let _ = pid;
304 false
305 }
306}
307
308#[derive(Clone, Copy, Debug, PartialEq, Eq)]
310pub enum StopSignal {
311 Terminate,
314 Kill,
316}
317
318pub fn signal_same_process(pid: u32, expected: &ProcessIdentity, signal: StopSignal) -> bool {
331 let Ok(raw_pid) = signal_target(pid) else {
332 return false;
333 };
334 #[cfg(windows)]
335 {
336 let _ = (signal, raw_pid);
338 windows_terminate_same_process(pid, expected)
339 }
340 #[cfg(unix)]
341 {
342 let sig = match signal {
343 StopSignal::Terminate => libc::SIGTERM,
344 StopSignal::Kill => libc::SIGKILL,
345 };
346 #[cfg(target_os = "linux")]
347 if let Some(delivered) = linux_pidfd_signal(raw_pid, expected, sig) {
348 return delivered;
349 }
350 is_same_process(pid, expected) && unsafe { libc::kill(raw_pid, sig) } == 0
354 }
355 #[cfg(not(any(unix, windows)))]
356 {
357 let _ = (expected, signal, raw_pid);
358 false
359 }
360}
361
362#[cfg(target_os = "linux")]
364fn linux_pidfd_signal(
365 pid: libc::pid_t,
366 expected: &ProcessIdentity,
367 sig: libc::c_int,
368) -> Option<bool> {
369 use std::os::fd::{AsRawFd, FromRawFd, OwnedFd};
370
371 let fd = unsafe { libc::syscall(libc::SYS_pidfd_open, pid, 0) };
374 if fd < 0 {
375 return match std::io::Error::last_os_error().raw_os_error() {
376 Some(libc::ESRCH) => Some(false),
378 _ => None,
380 };
381 }
382 let fd = libc::c_int::try_from(fd).ok()?;
383 let pidfd = unsafe { OwnedFd::from_raw_fd(fd) };
385 if !is_same_process(u32::try_from(pid).ok()?, expected) {
389 return Some(false);
390 }
391 let rc = unsafe {
393 libc::syscall(
394 libc::SYS_pidfd_send_signal,
395 pidfd.as_raw_fd(),
396 sig,
397 std::ptr::null::<libc::siginfo_t>(),
398 0,
399 )
400 };
401 Some(rc == 0)
402}
403
404#[cfg(target_os = "macos")]
405#[repr(C)]
406struct ProcBsdInfo {
407 pbi_flags: u32,
408 pbi_status: u32,
409 pbi_xstatus: u32,
410 pbi_pid: u32,
411 pbi_ppid: u32,
412 pbi_uid: u32,
413 pbi_gid: u32,
414 pbi_ruid: u32,
415 pbi_rgid: u32,
416 pbi_svuid: u32,
417 pbi_svgid: u32,
418 rfu_1: u32,
419 pbi_comm: [std::ffi::c_char; 16],
420 pbi_name: [std::ffi::c_char; 32],
421 pbi_nfiles: u32,
422 pbi_pgid: u32,
423 pbi_pjobc: u32,
424 e_tdev: u32,
425 e_tpgid: u32,
426 pbi_nice: i32,
427 pbi_start_tvsec: u64,
428 pbi_start_tvusec: u64,
429}
430
431#[cfg(target_os = "macos")]
432const PROC_PIDTBSDINFO: std::ffi::c_int = 3;
433
434#[cfg(target_os = "macos")]
435#[link(name = "proc")]
436unsafe extern "C" {
437 fn proc_pidinfo(
438 pid: std::ffi::c_int,
439 flavor: std::ffi::c_int,
440 arg: u64,
441 buffer: *mut std::ffi::c_void,
442 buffersize: std::ffi::c_int,
443 ) -> std::ffi::c_int;
444 fn proc_pidpath(
445 pid: std::ffi::c_int,
446 buffer: *mut std::ffi::c_void,
447 buffersize: u32,
448 ) -> std::ffi::c_int;
449}
450
451#[cfg(target_os = "macos")]
452fn macos_bsd_info(pid: u32) -> Option<ProcBsdInfo> {
453 let mut info = std::mem::MaybeUninit::<ProcBsdInfo>::zeroed();
454 let size = i32::try_from(std::mem::size_of::<ProcBsdInfo>()).ok()?;
455 let read = unsafe {
457 proc_pidinfo(
458 i32::try_from(pid).ok()?,
459 PROC_PIDTBSDINFO,
460 0,
461 info.as_mut_ptr().cast(),
462 size,
463 )
464 };
465 if read != size {
466 return None;
467 }
468 Some(unsafe { info.assume_init() })
470}
471
472#[cfg(target_os = "macos")]
473fn macos_process_identity(pid: u32) -> Option<ProcessIdentity> {
474 let info = macos_bsd_info(pid)?;
475 let mut path = [0_u8; libc::PATH_MAX as usize];
476 let path_len = unsafe {
478 proc_pidpath(
479 i32::try_from(pid).ok()?,
480 path.as_mut_ptr().cast(),
481 u32::try_from(path.len()).ok()?,
482 )
483 };
484 if path_len <= 0 {
485 return None;
486 }
487 let path_len = usize::try_from(path_len).ok()?;
488 let executable = String::from_utf8_lossy(path.get(..path_len)?).into_owned();
489 let start_marker = info
490 .pbi_start_tvsec
491 .checked_mul(1_000_000)?
492 .checked_add(info.pbi_start_tvusec)?;
493 Some(ProcessIdentity {
494 start_marker,
495 executable,
496 })
497}
498
499#[cfg(target_os = "linux")]
500fn linux_process_identity(pid: u32) -> Option<ProcessIdentity> {
501 let stat = std::fs::read_to_string(format!("/proc/{pid}/stat")).ok()?;
502 let (_, fields) = stat.rsplit_once(')')?;
503 let start_marker = fields.split_whitespace().nth(19)?.parse().ok()?;
504 let executable = std::fs::read_link(format!("/proc/{pid}/exe"))
505 .ok()?
506 .to_string_lossy()
507 .into_owned();
508 Some(ProcessIdentity {
509 start_marker,
510 executable,
511 })
512}
513
514#[cfg(windows)]
516struct ProcessHandle(windows_sys::Win32::Foundation::HANDLE);
517
518#[cfg(windows)]
519impl ProcessHandle {
520 fn open(pid: u32, access: u32) -> Option<Self> {
521 use windows_sys::Win32::System::Threading::OpenProcess;
522 let handle = unsafe { OpenProcess(access, 0, pid) };
524 (!handle.is_null()).then_some(Self(handle))
525 }
526}
527
528#[cfg(windows)]
529impl Drop for ProcessHandle {
530 fn drop(&mut self) {
531 unsafe { windows_sys::Win32::Foundation::CloseHandle(self.0) };
533 }
534}
535
536#[cfg(windows)]
537fn windows_process_identity(pid: u32) -> Option<ProcessIdentity> {
538 use windows_sys::Win32::System::Threading::PROCESS_QUERY_LIMITED_INFORMATION;
539 let handle = ProcessHandle::open(pid, PROCESS_QUERY_LIMITED_INFORMATION)?;
540 windows_identity_of(&handle)
541}
542
543#[cfg(windows)]
546fn windows_terminate_same_process(pid: u32, expected: &ProcessIdentity) -> bool {
547 use windows_sys::Win32::System::Threading::{
548 PROCESS_QUERY_LIMITED_INFORMATION, PROCESS_TERMINATE, TerminateProcess,
549 };
550 let Some(handle) =
551 ProcessHandle::open(pid, PROCESS_QUERY_LIMITED_INFORMATION | PROCESS_TERMINATE)
552 else {
553 return false;
554 };
555 if !windows_identity_of(&handle).is_some_and(|live| same_process(expected, &live)) {
556 return false;
557 }
558 unsafe { TerminateProcess(handle.0, 1) != 0 }
560}
561
562#[cfg(windows)]
564pub(crate) fn windows_token_user_sid(
565 process: windows_sys::Win32::Foundation::HANDLE,
566) -> Option<Vec<u8>> {
567 use windows_sys::Win32::Foundation::CloseHandle;
568 use windows_sys::Win32::Security::{
569 GetLengthSid, GetTokenInformation, IsValidSid, TOKEN_QUERY, TOKEN_USER, TokenUser,
570 };
571 use windows_sys::Win32::System::Threading::OpenProcessToken;
572
573 let mut token = std::ptr::null_mut();
574 if unsafe { OpenProcessToken(process, TOKEN_QUERY, &raw mut token) } == 0 {
576 return None;
577 }
578 let sid = (|| {
579 let mut needed = 0_u32;
580 unsafe { GetTokenInformation(token, TokenUser, std::ptr::null_mut(), 0, &raw mut needed) };
582 if needed < u32::try_from(std::mem::size_of::<TOKEN_USER>()).ok()? || needed > 4096 {
583 return None;
584 }
585 let mut buf = vec![0_u64; (needed as usize).div_ceil(8)];
586 let buf_bytes = buf.len() * 8;
587 let ok = unsafe {
589 GetTokenInformation(
590 token,
591 TokenUser,
592 buf.as_mut_ptr().cast(),
593 needed,
594 &raw mut needed,
595 )
596 };
597 if ok == 0 {
598 return None;
599 }
600 let sid = unsafe { &*buf.as_ptr().cast::<TOKEN_USER>() }.User.Sid;
602 if sid.is_null() || unsafe { IsValidSid(sid) } == 0 {
604 return None;
605 }
606 let len = unsafe { GetLengthSid(sid) } as usize;
608 let (start, base) = (sid as usize, buf.as_ptr() as usize);
609 if start < base || start + len > base + buf_bytes {
610 return None;
611 }
612 Some(unsafe { std::slice::from_raw_parts(sid.cast::<u8>(), len) }.to_vec())
614 })();
615 unsafe { CloseHandle(token) };
617 sid
618}
619
620#[cfg(windows)]
621fn windows_owned_by_current_user(pid: u32) -> bool {
622 use windows_sys::Win32::System::Threading::{
623 GetCurrentProcess, PROCESS_QUERY_LIMITED_INFORMATION,
624 };
625 let Some(handle) = ProcessHandle::open(pid, PROCESS_QUERY_LIMITED_INFORMATION) else {
626 return false;
627 };
628 let mine = windows_token_user_sid(unsafe { GetCurrentProcess() });
630 let theirs = windows_token_user_sid(handle.0);
631 mine.is_some() && mine == theirs
632}
633
634#[cfg(windows)]
635fn windows_identity_of(handle: &ProcessHandle) -> Option<ProcessIdentity> {
636 use std::os::windows::ffi::OsStringExt;
637 use windows_sys::Win32::Foundation::FILETIME;
638 use windows_sys::Win32::System::Threading::{GetProcessTimes, QueryFullProcessImageNameW};
639
640 let handle = handle.0;
641 let mut created = FILETIME::default();
642 let mut exited = FILETIME::default();
643 let mut kernel = FILETIME::default();
644 let mut user = FILETIME::default();
645 let times_ok = unsafe {
647 GetProcessTimes(
648 handle,
649 &raw mut created,
650 &raw mut exited,
651 &raw mut kernel,
652 &raw mut user,
653 )
654 } != 0;
655 let mut path = vec![0_u16; 32_768];
656 let mut path_len = u32::try_from(path.len()).ok()?;
657 let path_ok =
659 unsafe { QueryFullProcessImageNameW(handle, 0, path.as_mut_ptr(), &raw mut path_len) != 0 };
660 if !times_ok || !path_ok {
661 return None;
662 }
663 let executable = std::ffi::OsString::from_wide(path.get(..usize::try_from(path_len).ok()?)?)
664 .to_string_lossy()
665 .into_owned();
666 let start_marker = (u64::from(created.dwHighDateTime) << 32) | u64::from(created.dwLowDateTime);
667 Some(ProcessIdentity {
668 start_marker,
669 executable,
670 })
671}
672
673pub fn terminate_gracefully(pid: u32) -> Result<()> {
676 let raw =
677 signal_target(pid).map_err(|refusal| anyhow::anyhow!("not sending SIGTERM: {refusal}"))?;
678 #[cfg(unix)]
679 {
680 let ret = unsafe { libc::kill(raw, libc::SIGTERM) };
684 if ret != 0 {
685 anyhow::bail!(
686 "Failed to send SIGTERM to PID {pid}: {}",
687 std::io::Error::last_os_error()
688 );
689 }
690 Ok(())
691 }
692 #[cfg(windows)]
693 {
694 let _ = raw;
695 force_kill(pid)
696 }
697}
698
699pub fn force_kill(pid: u32) -> Result<()> {
702 let raw =
703 signal_target(pid).map_err(|refusal| anyhow::anyhow!("not sending SIGKILL: {refusal}"))?;
704 #[cfg(unix)]
705 {
706 let ret = unsafe { libc::kill(raw, libc::SIGKILL) };
710 if ret != 0 {
711 anyhow::bail!(
712 "Failed to send SIGKILL to PID {pid}: {}",
713 std::io::Error::last_os_error()
714 );
715 }
716 Ok(())
717 }
718 #[cfg(windows)]
719 {
720 use windows_sys::Win32::Foundation::CloseHandle;
721 use windows_sys::Win32::System::Threading::{
722 OpenProcess, PROCESS_TERMINATE, TerminateProcess,
723 };
724 let _ = raw;
725
726 unsafe {
729 let handle = OpenProcess(PROCESS_TERMINATE, 0, pid);
730 if handle.is_null() {
731 anyhow::bail!(
732 "Failed to open PID {pid} for termination: {}",
733 std::io::Error::last_os_error()
734 );
735 }
736 let ok = TerminateProcess(handle, 1);
737 CloseHandle(handle);
738 if ok == 0 {
739 anyhow::bail!(
740 "Failed to terminate PID {pid}: {}",
741 std::io::Error::last_os_error()
742 );
743 }
744 Ok(())
745 }
746 }
747}
748
749fn protected_self_pids() -> std::collections::HashSet<u32> {
765 let mut protected = std::collections::HashSet::new();
766 protected.insert(std::process::id());
767 #[cfg(unix)]
768 {
769 let mut pid = std::process::id();
770 for _ in 0..16 {
771 let Ok(output) = std::process::Command::new("ps")
772 .args(["-o", "ppid=", "-p", &pid.to_string()])
773 .output()
774 else {
775 break;
776 };
777 let Ok(ppid) = String::from_utf8_lossy(&output.stdout)
778 .trim()
779 .parse::<u32>()
780 else {
781 break;
782 };
783 if ppid <= 1 || !protected.insert(ppid) {
784 break;
785 }
786 pid = ppid;
787 }
788
789 let own_pgid = unsafe { libc::getpgrp() };
791 if own_pgid > 0 {
792 protected.extend(process_group_pids(own_pgid));
793 }
794 }
795 protected
796}
797
798#[cfg(unix)]
799fn process_group_pids(pgid: libc::pid_t) -> Vec<u32> {
800 std::process::Command::new("pgrep")
801 .args(["-g", &pgid.to_string()])
802 .output()
803 .ok()
804 .map(|output| {
805 String::from_utf8_lossy(&output.stdout)
806 .lines()
807 .filter_map(|line| line.trim().parse::<u32>().ok())
808 .collect()
809 })
810 .unwrap_or_default()
811}
812
813pub fn find_pids_by_name(name: &str) -> Vec<u32> {
816 let protected = protected_self_pids();
817 let mut pids = Vec::new();
818
819 #[cfg(unix)]
820 {
821 let uid = unsafe { libc::getuid() }.to_string();
825
826 if let Ok(output) = std::process::Command::new("pgrep")
828 .args(["-U", &uid, "-x", name])
829 .output()
830 {
831 collect_pids(&output.stdout, &protected, &mut pids);
832 }
833
834 if let Ok(output) = std::process::Command::new("pgrep")
837 .args(["-U", &uid])
838 .arg("-f")
839 .arg(format!("/{name}(\\s|$)"))
840 .output()
841 {
842 collect_pids(&output.stdout, &protected, &mut pids);
843 }
844
845 pids.sort_unstable();
846 pids.dedup();
847 }
848
849 #[cfg(windows)]
850 {
851 if let Ok(output) = std::process::Command::new("tasklist")
852 .args([
853 "/FI",
854 &format!("IMAGENAME eq {name}.exe"),
855 "/FO",
856 "CSV",
857 "/NH",
858 ])
859 .output()
860 {
861 let stdout = String::from_utf8_lossy(&output.stdout);
862 for line in stdout.lines() {
863 let parts: Vec<&str> = line.split(',').collect();
864 if parts.len() >= 2 {
865 let pid_str = parts[1].trim().trim_matches('"');
866 if let Ok(pid) = pid_str.parse::<u32>() {
867 if !protected.contains(&pid) {
868 pids.push(pid);
869 }
870 }
871 }
872 }
873 }
874 }
875
876 pids
877}
878
879#[cfg(unix)]
880fn collect_pids(stdout: &[u8], protected: &std::collections::HashSet<u32>, out: &mut Vec<u32>) {
881 let text = String::from_utf8_lossy(stdout);
882 for line in text.lines() {
883 if let Ok(pid) = line.trim().parse::<u32>()
884 && !protected.contains(&pid)
885 {
886 out.push(pid);
887 }
888 }
889}
890
891mod services;
892pub use services::*;
893
894#[cfg(test)]
895mod tests;