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