1use std::{
15 collections::{HashMap, HashSet},
16 ffi::OsString,
17 path::{Path, PathBuf},
18 sync::{
19 Arc, Mutex,
20 atomic::{AtomicBool, Ordering},
21 },
22 time::{Duration, SystemTime, UNIX_EPOCH},
23};
24
25use scv_client::Layout;
26use serde::{Deserialize, Serialize};
27use sha2::{Digest, Sha256};
28
29use crate::{process::ProcessGroup, sync::lock};
30
31pub const PARENT_VARIABLE: &str = "SCV_PARENT";
33pub use scv_client::DELEGATION_DEPTH_VARIABLE as DEPTH_VARIABLE;
35pub(crate) const STOP_GRACE: Duration = Duration::from_secs(2);
37const MAX_RECORD_BYTES: u64 = 64 * 1024;
39const ZOMBIE_MIN_AGE: Duration = Duration::from_secs(10);
41
42pub fn current_depth() -> u32 {
44 #[cfg(test)]
45 {
46 0
47 }
48 #[cfg(not(test))]
49 {
50 scv_client::inherited_delegation_depth().unwrap_or(0)
51 }
52}
53
54#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
56pub struct ProcessIdentity {
57 pub pid: u32,
58 pub(crate) start_time: u64,
59}
60
61impl ProcessIdentity {
62 pub fn current() -> Option<Self> {
63 Self::of(std::process::id())
64 }
65
66 pub fn of(pid: u32) -> Option<Self> {
67 process_start_time(pid).map(|start_time| Self { pid, start_time })
68 }
69
70 pub fn is_alive(&self) -> bool {
73 #[cfg(target_os = "linux")]
74 {
75 linux::stat(self.pid)
76 .is_some_and(|info| info.start_time == self.start_time && info.state != 'Z')
77 }
78 #[cfg(not(target_os = "linux"))]
79 {
80 Self::of(self.pid) == Some(*self) && !process_is_zombie(self.pid)
81 }
82 }
83}
84
85#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
87pub struct DelegationRecord {
88 pub handle: String,
89 pub agent: String,
90 pub instance: String,
91 pub session: String,
92 pub owner: ProcessIdentity,
93 pub process: ProcessIdentity,
95 pub pgid: u32,
96 pub cwd: PathBuf,
97 pub started_unix: u64,
98 pub depth: u32,
100 #[serde(default, skip_serializing_if = "Option::is_none")]
102 pub conversation: Option<String>,
103 #[serde(default, skip_serializing_if = "Option::is_none")]
104 pub turn: Option<u32>,
105 #[serde(default, skip_serializing_if = "Option::is_none")]
109 pub idle_since_unix: Option<u64>,
110 #[serde(default, skip_serializing_if = "Option::is_none")]
115 pub background_jobs: Option<u32>,
116}
117
118#[derive(Debug, Clone, PartialEq, Eq)]
120pub struct DelegationEntry {
121 pub record: DelegationRecord,
122 pub orphaned: bool,
124 pub processes: usize,
126}
127
128impl DelegationEntry {
129 pub fn working(&self) -> bool {
133 self.processes > 0
134 && (self.record.idle_since_unix.is_none()
135 || self.record.background_jobs.is_some_and(|jobs| jobs > 0))
136 }
137}
138
139#[derive(Debug, Clone, PartialEq, Eq)]
142pub struct ChainRun {
143 pub handle: String,
145 pub session: String,
147}
148
149#[derive(Debug, Clone, Default, PartialEq, Eq)]
151pub struct ReconcileReport {
152 pub reaped: Vec<String>,
154 pub removed: usize,
156 pub stale_markers: usize,
158}
159
160#[derive(Debug, Default)]
161struct Inner {
162 active: HashMap<String, Arc<AtomicBool>>,
163 reaped: u64,
164}
165
166#[derive(Debug)]
168pub struct DelegationRegistry {
169 record_dir: PathBuf,
170 conversation_dir: PathBuf,
171 instance: String,
172 owner: Option<ProcessIdentity>,
173 depth: u32,
174 chain: Option<String>,
175 inner: Mutex<Inner>,
176}
177
178pub(crate) struct PendingDelegation {
180 pub(crate) handle: String,
181 pub(crate) environment: Vec<(OsString, OsString)>,
182 agent: String,
183 session: String,
184 cwd: PathBuf,
185 conversation: Option<(String, u32)>,
186 depth: u32,
188}
189
190impl DelegationRegistry {
191 pub fn new(layout: &Layout) -> Self {
195 let digest = Sha256::digest(layout.home().as_os_str().as_encoded_bytes());
196 let instance = digest[..4]
197 .iter()
198 .map(|byte| format!("{byte:02x}"))
199 .collect();
200 Self {
201 record_dir: layout.delegations(),
202 conversation_dir: layout.conversations(),
203 instance,
204 owner: ProcessIdentity::current(),
205 depth: current_depth(),
206 chain: {
207 #[cfg(test)]
208 {
209 None
210 }
211 #[cfg(not(test))]
212 {
213 std::env::var(PARENT_VARIABLE)
214 .ok()
215 .filter(|value| !value.trim().is_empty())
216 }
217 },
218 inner: Mutex::new(Inner::default()),
219 }
220 }
221
222 #[cfg(test)]
225 pub(crate) fn for_test(layout: &Layout) -> Self {
226 Self {
227 depth: 0,
228 chain: None,
229 ..Self::new(layout)
230 }
231 }
232
233 pub(crate) fn depth(&self) -> u32 {
235 self.depth
236 }
237
238 pub fn record_dir(&self) -> &Path {
239 &self.record_dir
240 }
241
242 pub fn instance(&self) -> &str {
244 &self.instance
245 }
246
247 pub fn reaped_total(&self) -> u64 {
249 lock(&self.inner).reaped
250 }
251
252 pub fn conversation_dir(&self) -> &Path {
254 &self.conversation_dir
255 }
256
257 #[cfg(test)]
258 pub(crate) fn begin(
259 &self,
260 agent: &str,
261 session: &str,
262 cwd: &Path,
263 conversation: Option<(&str, u32)>,
264 ) -> PendingDelegation {
265 self.begin_at(self.depth, agent, session, cwd, conversation)
266 }
267
268 pub(crate) fn begin_at(
271 &self,
272 owner_depth: u32,
273 agent: &str,
274 session: &str,
275 cwd: &Path,
276 conversation: Option<(&str, u32)>,
277 ) -> PendingDelegation {
278 let suffix = uuid::Uuid::new_v4().simple().to_string();
279 let handle = format!("{agent}-{}", &suffix[..6]);
280 let entry = format!("{}/{session}/{handle}", self.instance);
281 let chain = match &self.chain {
282 Some(chain) => format!("{chain};{entry}"),
283 None => entry,
284 };
285 PendingDelegation {
286 environment: vec![
287 (PARENT_VARIABLE.into(), chain.into()),
288 (
289 DEPTH_VARIABLE.into(),
290 owner_depth.saturating_add(1).to_string().into(),
291 ),
292 ],
293 handle,
294 agent: agent.to_owned(),
295 session: session.to_owned(),
296 cwd: cwd.to_owned(),
297 conversation: conversation.map(|(handle, turn)| (handle.to_owned(), turn)),
298 depth: owner_depth.saturating_add(1),
299 }
300 }
301
302 pub(crate) fn register(
305 self: &Arc<Self>,
306 pending: PendingDelegation,
307 pid: u32,
308 ) -> std::io::Result<DelegationGuard> {
309 let killed = Arc::new(AtomicBool::new(false));
310 let record = DelegationRecord {
311 handle: pending.handle.clone(),
312 agent: pending.agent,
313 instance: self.instance.clone(),
314 session: pending.session,
315 owner: self.owner.unwrap_or(ProcessIdentity {
316 pid: std::process::id(),
317 start_time: 0,
318 }),
319 process: ProcessIdentity::of(pid).unwrap_or(ProcessIdentity { pid, start_time: 0 }),
320 pgid: pid,
321 cwd: pending.cwd,
322 started_unix: unix_now(),
323 depth: pending.depth,
324 conversation: pending
325 .conversation
326 .as_ref()
327 .map(|(handle, _)| handle.clone()),
328 turn: pending.conversation.as_ref().map(|(_, turn)| *turn),
329 idle_since_unix: None,
330 background_jobs: None,
331 };
332 lock(&self.inner)
333 .active
334 .insert(record.handle.clone(), Arc::clone(&killed));
335 if let Err(error) = write_record(&self.record_dir, &record) {
336 lock(&self.inner).active.remove(&record.handle);
337 return Err(error);
338 }
339 Ok(DelegationGuard {
340 registry: Arc::clone(self),
341 handle: record.handle,
342 pgid: pid,
343 killed,
344 finished: false,
345 })
346 }
347
348 pub fn list(&self, include_orphans: bool) -> Vec<DelegationEntry> {
351 let table = ProcessTable::snapshot();
352 let mut entries: Vec<_> = self
353 .records()
354 .into_iter()
355 .filter_map(|record| {
356 let orphaned = !self.owner_alive(&record);
357 if orphaned && !include_orphans {
358 return None;
359 }
360 let processes = table.members(&record).len();
361 Some(DelegationEntry {
362 record,
363 orphaned,
364 processes,
365 })
366 })
367 .collect();
368 entries.sort_by(|a, b| {
369 a.record
370 .started_unix
371 .cmp(&b.record.started_unix)
372 .then_with(|| a.record.handle.cmp(&b.record.handle))
373 });
374 entries
375 }
376
377 pub fn own_run(&self, chain: &str) -> Option<ChainRun> {
382 let own = std::process::id();
383 let entries = self.list(true);
384 chain.split(';').find_map(|entry| {
385 let mut parts = entry.splitn(3, '/');
386 let (instance, session, handle) = (parts.next()?, parts.next()?, parts.next()?);
387 if instance != self.instance {
388 return None;
389 }
390 entries
391 .iter()
392 .find(|running| running.record.handle == handle && running.record.owner.pid == own)
393 .map(|_| ChainRun {
394 handle: handle.to_owned(),
395 session: session.to_owned(),
396 })
397 })
398 }
399
400 pub async fn kill(&self, handle: &str) -> Result<(), String> {
402 let record = self
403 .records()
404 .into_iter()
405 .find(|record| record.handle == handle)
406 .ok_or_else(|| format!("no running delegation {handle:?}"))?;
407 let local = lock(&self.inner).active.get(handle).cloned();
408 if let Some(killed) = &local {
409 killed.store(true, Ordering::Release);
410 }
411 stop_delegation(&record).await;
412 if local.is_none() && !self.owner_alive(&record) {
413 remove_record(&self.record_dir, handle);
414 lock(&self.inner).reaped += 1;
415 }
416 Ok(())
417 }
418
419 pub async fn reconcile(&self) -> ReconcileReport {
421 let mut report = ReconcileReport::default();
422 for record in self.records() {
423 if self.owner_alive(&record) {
424 continue;
425 }
426 if stop_delegation(&record).await {
427 report.reaped.push(record.handle.clone());
428 } else {
429 report.removed += 1;
430 }
431 remove_record(&self.record_dir, &record.handle);
432 }
433 lock(&self.inner).reaped += report.reaped.len() as u64;
434 report.stale_markers =
435 crate::delegate::conversation::remove_stale_markers(self.conversation_dir());
436 report
437 }
438
439 fn owner_alive(&self, record: &DelegationRecord) -> bool {
442 if Some(record.owner) == self.owner {
443 return lock(&self.inner).active.contains_key(&record.handle);
444 }
445 record.owner.is_alive()
446 }
447
448 fn records(&self) -> Vec<DelegationRecord> {
449 let Ok(entries) = std::fs::read_dir(&self.record_dir) else {
450 return Vec::new();
451 };
452 entries
453 .filter_map(Result::ok)
454 .filter(|entry| entry.path().extension().is_some_and(|ext| ext == "json"))
455 .filter_map(|entry| read_record(&entry.path()))
456 .filter(|record| record.instance == self.instance)
457 .collect()
458 }
459
460 fn finish_local(&self, handle: &str) {
461 lock(&self.inner).active.remove(handle);
462 remove_record(&self.record_dir, handle);
463 }
464}
465
466pub(crate) struct DelegationGuard {
468 registry: Arc<DelegationRegistry>,
469 handle: String,
470 pgid: u32,
471 killed: Arc<AtomicBool>,
472 finished: bool,
473}
474
475impl DelegationGuard {
476 #[cfg(test)]
477 pub(crate) fn handle(&self) -> &str {
478 &self.handle
479 }
480
481 pub(crate) fn set_turn(&self, turn: u32) {
484 self.update(|record| {
485 record.turn = Some(turn);
486 record.idle_since_unix = None;
487 });
488 }
489
490 pub(crate) fn set_idle(&self) {
492 let now = unix_now();
493 self.update(|record| record.idle_since_unix = Some(now));
494 }
495
496 pub(crate) fn set_background_jobs(&self, jobs: usize) {
499 let jobs = (jobs > 0).then(|| u32::try_from(jobs).unwrap_or(u32::MAX));
500 self.update(|record| record.background_jobs = jobs);
501 }
502
503 fn update(&self, change: impl FnOnce(&mut DelegationRecord)) {
506 let dir = &self.registry.record_dir;
507 if let Some(mut record) = read_record(&dir.join(format!("{}.json", self.handle))) {
508 change(&mut record);
509 if let Err(error) = write_record(dir, &record) {
510 tracing::debug!(handle = %record.handle, %error, "could not update a delegation record");
511 }
512 }
513 }
514
515 pub(crate) fn was_killed(&self) -> bool {
517 self.killed.load(Ordering::Acquire)
518 }
519
520 pub(crate) async fn finish(mut self) {
522 self.finished = true;
523 stop_tagged(&self.handle).await;
524 self.registry.finish_local(&self.handle);
525 }
526}
527
528impl Drop for DelegationGuard {
529 fn drop(&mut self) {
530 if self.finished {
531 return;
532 }
533 if let Some(group) = ProcessGroup::new(self.pgid) {
536 group.signal(libc::SIGKILL);
537 }
538 self.registry.finish_local(&self.handle);
539 let handle = self.handle.clone();
540 if let Ok(runtime) = tokio::runtime::Handle::try_current() {
541 runtime.spawn(async move { stop_tagged(&handle).await });
542 } else {
543 for identity in tagged_processes(&handle) {
544 signal(identity.pid, libc::SIGKILL);
545 }
546 }
547 }
548}
549
550async fn stop_delegation(record: &DelegationRecord) -> bool {
553 let mut stopped = false;
554 let leader = ProcessIdentity::of(record.process.pid);
558 let group_is_ours = record.pgid == record.process.pid
559 && match leader {
560 Some(leader) => leader == record.process,
561 None => group_exists(record.pgid),
562 };
563 if group_is_ours && group_exists(record.pgid) {
564 stopped = true;
565 let group = ProcessGroup::new(record.pgid);
566 if let Some(group) = group {
567 group.signal(libc::SIGTERM);
568 }
569 let deadline = tokio::time::Instant::now() + STOP_GRACE;
570 while group_exists(record.pgid) && tokio::time::Instant::now() < deadline {
571 tokio::time::sleep(Duration::from_millis(50)).await;
572 }
573 if let Some(group) = group {
574 group.signal(libc::SIGKILL);
575 }
576 }
577 stopped | stop_tagged(&record.handle).await
578}
579
580async fn stop_tagged(handle: &str) -> bool {
582 let tagged = tagged_processes(handle);
583 if tagged.is_empty() {
584 return false;
585 }
586 for identity in &tagged {
587 signal(identity.pid, libc::SIGTERM);
588 }
589 let deadline = tokio::time::Instant::now() + STOP_GRACE;
590 while tagged.iter().any(ProcessIdentity::is_alive) && tokio::time::Instant::now() < deadline {
591 tokio::time::sleep(Duration::from_millis(50)).await;
592 }
593 for identity in tagged.iter().filter(|identity| identity.is_alive()) {
594 signal(identity.pid, libc::SIGKILL);
595 }
596 true
597}
598
599fn tagged_processes(handle: &str) -> Vec<ProcessIdentity> {
601 let own = std::process::id();
602 ProcessTable::snapshot()
603 .tagged
604 .into_iter()
605 .filter(|(identity, chain)| identity.pid != own && chain_names(chain, handle))
606 .map(|(identity, _)| identity)
607 .collect()
608}
609
610fn chain_names(chain: &str, handle: &str) -> bool {
611 chain
612 .split(';')
613 .any(|entry| entry.rsplit('/').next() == Some(handle))
614}
615
616fn signal(pid: u32, signal: i32) {
617 if let Ok(pid) = i32::try_from(pid)
618 && pid > 0
619 {
620 unsafe {
623 libc::kill(pid, signal);
624 }
625 }
626}
627
628pub(crate) fn group_exists(pgid: u32) -> bool {
630 let Some(group) = ProcessGroup::new(pgid) else {
631 return false;
632 };
633 let signalable = group.is_signalable();
634 #[cfg(target_os = "linux")]
635 {
636 signalable
637 && linux::all_stats()
638 .iter()
639 .any(|info| info.pgid == pgid && info.state != 'Z')
640 }
641 #[cfg(not(target_os = "linux"))]
642 {
643 signalable
644 }
645}
646
647fn unix_now() -> u64 {
648 SystemTime::now()
649 .duration_since(UNIX_EPOCH)
650 .map_or(0, |elapsed| elapsed.as_secs())
651}
652
653fn write_record(dir: &Path, record: &DelegationRecord) -> std::io::Result<()> {
654 write_private_json(dir, &format!("{}.json", record.handle), record)
655}
656
657pub(crate) fn write_private_json(
660 dir: &Path,
661 name: &str,
662 value: &impl Serialize,
663) -> std::io::Result<()> {
664 use std::os::unix::fs::PermissionsExt as _;
665 std::fs::create_dir_all(dir)?;
666 if let Some(run) = dir.parent() {
667 std::fs::set_permissions(run, std::fs::Permissions::from_mode(0o700))?;
668 }
669 std::fs::set_permissions(dir, std::fs::Permissions::from_mode(0o700))?;
670 let bytes = serde_json::to_vec_pretty(value).map_err(std::io::Error::other)?;
671 scv_client::fs::replace_private(&dir.join(name), &bytes)
672}
673
674fn read_record(path: &Path) -> Option<DelegationRecord> {
675 let bytes = match std::fs::File::open(path).and_then(|file| {
676 let mut bytes = Vec::new();
677 std::io::Read::read_to_end(&mut std::io::Read::take(file, MAX_RECORD_BYTES), &mut bytes)
678 .map(|_| bytes)
679 }) {
680 Ok(bytes) => bytes,
681 Err(error) => {
682 if error.kind() != std::io::ErrorKind::NotFound {
684 tracing::debug!(path = %path.display(), %error, "unreadable delegation record");
685 }
686 return None;
687 }
688 };
689 let record: DelegationRecord = match serde_json::from_slice(&bytes) {
690 Ok(record) => record,
691 Err(error) => {
692 tracing::debug!(path = %path.display(), %error, "malformed delegation record");
693 return None;
694 }
695 };
696 if path.file_stem().and_then(|stem| stem.to_str()) == Some(record.handle.as_str()) {
698 Some(record)
699 } else {
700 tracing::debug!(path = %path.display(), "delegation record named for another handle");
701 None
702 }
703}
704
705fn remove_record(dir: &Path, handle: &str) {
706 let path = dir.join(format!("{handle}.json"));
707 if let Err(error) = std::fs::remove_file(&path)
708 && error.kind() != std::io::ErrorKind::NotFound
709 {
710 tracing::debug!(path = %path.display(), %error, "could not remove a delegation record");
711 }
712}
713
714pub fn become_child_subreaper() -> bool {
717 #[cfg(target_os = "linux")]
718 {
719 unsafe { libc::prctl(libc::PR_SET_CHILD_SUBREAPER, 1, 0, 0, 0) == 0 }
722 }
723 #[cfg(not(target_os = "linux"))]
724 {
725 false
726 }
727}
728
729static SPAWNED: Mutex<Option<HashSet<u32>>> = Mutex::new(None);
730
731pub(crate) fn track_spawned(pid: u32) {
733 lock(&SPAWNED).get_or_insert_with(HashSet::new).insert(pid);
734}
735
736pub(crate) fn untrack_spawned(pid: u32) {
737 if let Some(spawned) = lock(&SPAWNED).as_mut() {
738 spawned.remove(&pid);
739 }
740}
741
742pub fn reap_orphaned_zombies() -> usize {
745 #[cfg(target_os = "linux")]
746 {
747 let own = std::process::id();
748 let spawned = lock(&SPAWNED).clone().unwrap_or_default();
749 let uptime = linux::uptime_ticks();
750 let mut reaped = 0;
751 for info in linux::all_stats() {
752 if info.ppid != own || info.state != 'Z' || spawned.contains(&info.pid) {
753 continue;
754 }
755 let old_enough = uptime.is_some_and(|now| {
756 now.saturating_sub(info.start_time)
757 >= ZOMBIE_MIN_AGE.as_secs() * linux::clock_ticks()
758 });
759 if !old_enough {
760 continue;
761 }
762 let mut status = 0;
763 if unsafe { libc::waitpid(info.pid as i32, &raw mut status, libc::WNOHANG) }
766 == info.pid as i32
767 {
768 reaped += 1;
769 }
770 }
771 reaped
772 }
773 #[cfg(not(target_os = "linux"))]
774 {
775 0
776 }
777}
778
779struct ProcessTable {
781 groups: Vec<(ProcessIdentity, u32)>,
782 tagged: Vec<(ProcessIdentity, String)>,
783}
784
785impl ProcessTable {
786 fn members(&self, record: &DelegationRecord) -> HashSet<u32> {
787 let mut members: HashSet<u32> = self
788 .groups
789 .iter()
790 .filter(|(_, pgid)| *pgid == record.pgid)
791 .map(|(identity, _)| identity.pid)
792 .collect();
793 members.extend(
794 self.tagged
795 .iter()
796 .filter(|(_, chain)| chain_names(chain, &record.handle))
797 .map(|(identity, _)| identity.pid),
798 );
799 members
800 }
801
802 #[cfg(target_os = "linux")]
803 fn snapshot() -> Self {
804 let mut groups = Vec::new();
805 let mut tagged = Vec::new();
806 for info in linux::all_stats() {
807 if info.state == 'Z' {
808 continue;
809 }
810 let identity = ProcessIdentity {
811 pid: info.pid,
812 start_time: info.start_time,
813 };
814 groups.push((identity, info.pgid));
815 if let Some(chain) = linux::parent_chain(info.pid) {
816 tagged.push((identity, chain));
817 }
818 }
819 Self { groups, tagged }
820 }
821
822 #[cfg(not(target_os = "linux"))]
823 fn snapshot() -> Self {
824 let mut groups = Vec::new();
825 let mut tagged = Vec::new();
826 let Ok(output) = std::process::Command::new("ps")
828 .args(["-E", "-ww", "-axo", "pid=,pgid=,command="])
829 .output()
830 else {
831 return Self { groups, tagged };
832 };
833 for line in String::from_utf8_lossy(&output.stdout).lines() {
834 let mut fields = line.split_whitespace();
835 let (Some(pid), Some(pgid)) = (
836 fields.next().and_then(|value| value.parse::<u32>().ok()),
837 fields.next().and_then(|value| value.parse::<u32>().ok()),
838 ) else {
839 continue;
840 };
841 let Some(identity) = ProcessIdentity::of(pid) else {
842 continue;
843 };
844 groups.push((identity, pgid));
845 if let Some(chain) = fields.find_map(|field| {
846 field
847 .strip_prefix(PARENT_VARIABLE)
848 .and_then(|rest| rest.strip_prefix('='))
849 }) {
850 tagged.push((identity, chain.to_owned()));
851 }
852 }
853 Self { groups, tagged }
854 }
855}
856
857#[cfg(target_os = "linux")]
858fn process_start_time(pid: u32) -> Option<u64> {
859 linux::stat(pid).map(|info| info.start_time)
860}
861
862#[cfg(target_os = "macos")]
863fn process_start_time(pid: u32) -> Option<u64> {
864 let mut info: libc::proc_bsdinfo = unsafe { std::mem::zeroed() };
867 let size = std::mem::size_of::<libc::proc_bsdinfo>() as i32;
868 let written = unsafe {
871 libc::proc_pidinfo(
872 pid as i32,
873 libc::PROC_PIDTBSDINFO,
874 0,
875 (&mut info as *mut libc::proc_bsdinfo).cast(),
876 size,
877 )
878 };
879 (written == size).then(|| info.pbi_start_tvsec * 1_000_000 + info.pbi_start_tvusec)
880}
881
882#[cfg(not(target_os = "linux"))]
883fn process_is_zombie(pid: u32) -> bool {
884 std::process::Command::new("ps")
885 .args(["-o", "stat=", "-p", &pid.to_string()])
886 .output()
887 .ok()
888 .filter(|output| output.status.success())
889 .is_some_and(|output| {
890 String::from_utf8_lossy(&output.stdout)
891 .trim_start()
892 .starts_with('Z')
893 })
894}
895
896#[cfg(not(any(target_os = "linux", target_os = "macos")))]
897fn process_start_time(pid: u32) -> Option<u64> {
898 let alive = unsafe { libc::kill(pid as i32, 0) } == 0;
901 alive.then_some(0)
902}
903
904#[cfg(target_os = "linux")]
905mod linux {
906 pub(super) struct Stat {
907 pub(crate) pid: u32,
908 pub(crate) ppid: u32,
909 pub(crate) pgid: u32,
910 pub(crate) state: char,
911 pub(crate) start_time: u64,
912 }
913
914 pub(super) fn stat(pid: u32) -> Option<Stat> {
915 let text = std::fs::read_to_string(format!("/proc/{pid}/stat")).ok()?;
916 let rest = &text[text.rfind(')')? + 2..];
918 let fields: Vec<&str> = rest.split_whitespace().collect();
919 Some(Stat {
921 pid,
922 state: fields.first()?.chars().next()?,
923 ppid: fields.get(1)?.parse().ok()?,
924 pgid: fields.get(2)?.parse().ok()?,
925 start_time: fields.get(19)?.parse().ok()?,
926 })
927 }
928
929 pub(super) fn all_stats() -> Vec<Stat> {
930 let Ok(entries) = std::fs::read_dir("/proc") else {
931 return Vec::new();
932 };
933 entries
934 .filter_map(Result::ok)
935 .filter_map(|entry| entry.file_name().to_str()?.parse::<u32>().ok())
936 .filter_map(stat)
937 .collect()
938 }
939
940 pub(super) fn parent_chain(pid: u32) -> Option<String> {
942 let environ = std::fs::read(format!("/proc/{pid}/environ")).ok()?;
943 let prefix = format!("{}=", super::PARENT_VARIABLE);
944 environ.split(|byte| *byte == 0).find_map(|entry| {
945 entry
946 .strip_prefix(prefix.as_bytes())
947 .map(|value| String::from_utf8_lossy(value).into_owned())
948 })
949 }
950
951 pub(super) fn clock_ticks() -> u64 {
952 let ticks = unsafe { libc::sysconf(libc::_SC_CLK_TCK) };
954 u64::try_from(ticks)
955 .ok()
956 .filter(|ticks| *ticks > 0)
957 .unwrap_or(100)
958 }
959
960 pub(super) fn uptime_ticks() -> Option<u64> {
961 let text = std::fs::read_to_string("/proc/uptime").ok()?;
962 let seconds: f64 = text.split_whitespace().next()?.parse().ok()?;
963 Some((seconds * clock_ticks() as f64) as u64)
964 }
965}
966
967#[cfg(test)]
968mod tests;