Skip to main content

ic_host_process/observation/procfs/
mod.rs

1//! Linux procfs observations. Callers select a mount and own read budgets.
2#![allow(
3    unsafe_code,
4    reason = "reviewed sysconf observations imported from Profiler"
5)]
6use super::{ChildProcesses, ProcessObservation};
7use std::{
8    collections::{BTreeMap, BTreeSet},
9    fs,
10    path::Path,
11    sync::OnceLock,
12};
13
14fn ticks_per_second() -> u64 {
15    static VALUE: OnceLock<u64> = OnceLock::new();
16    *VALUE.get_or_init(|| {
17        #[cfg(unix)]
18        {
19            // SAFETY: sysconf is called with a defined name and no pointers.
20            let value = unsafe { libc::sysconf(libc::_SC_CLK_TCK) };
21            if value > 0 {
22                return u64::try_from(value).ok().unwrap_or_default();
23            }
24        }
25        100
26    })
27}
28
29fn page_size() -> u64 {
30    static VALUE: OnceLock<u64> = OnceLock::new();
31    *VALUE.get_or_init(|| {
32        #[cfg(unix)]
33        {
34            // SAFETY: sysconf is called with a defined name and no pointers.
35            let value = unsafe { libc::sysconf(libc::_SC_PAGESIZE) };
36            if value > 0 {
37                return u64::try_from(value).ok().unwrap_or_default();
38            }
39        }
40        4096
41    })
42}
43
44/// Read the Linux creation token from a caller-selected procfs mount.
45#[must_use]
46pub fn process_identity(proc_root: &Path, pid: u32) -> Option<u64> {
47    let stat = fs::read_to_string(proc_root.join(pid.to_string()).join("stat")).ok()?;
48    let (_, fields) = stat_parts(&stat)?;
49    let mut fields = fields.split_whitespace();
50    fields.next()?; // state
51    fields.next()?.parse::<u64>().ok()?; // preserve full-stat PPID validation
52    fields.nth(17)?.parse().ok() // starttime is field 19 after the command
53}
54
55/// Read base Linux process facts from a caller-selected procfs mount.
56#[must_use]
57pub fn basic_process(proc_root: &Path, pid: u32) -> Option<ProcessObservation> {
58    let stat = fs::read_to_string(proc_root.join(pid.to_string()).join("stat")).ok()?;
59    let (name, fields) = parse_stat(&stat)?;
60    let number = |index: usize| fields.get(index)?.parse::<u64>().ok();
61    let ns = |index| number(index).unwrap_or(0).saturating_mul(1_000_000_000) / ticks_per_second();
62    Some(ProcessObservation {
63        pid,
64        ppid: u32::try_from(number(1)?).ok()?,
65        start_ticks: number(19)?,
66        name: name.to_owned(),
67        user_ns: ns(11),
68        system_ns: ns(12),
69        waited_user_ns: ns(13),
70        waited_system_ns: ns(14),
71        rss_bytes: number(21).unwrap_or(0).saturating_mul(page_size()),
72        pss_bytes: None,
73        read_bytes: None,
74        write_bytes: None,
75        minor_faults: number(7)
76            .unwrap_or(0)
77            .saturating_add(number(8).unwrap_or(0)),
78        major_faults: number(9)
79            .unwrap_or(0)
80            .saturating_add(number(10).unwrap_or(0)),
81        threads: number(17).unwrap_or(0),
82    })
83}
84
85fn stat_parts(stat: &str) -> Option<(&str, &str)> {
86    let begin = stat.find('(')?;
87    let end = stat.rfind(')')?;
88    Some((stat.get(begin + 1..end)?, stat.get(end + 1..)?))
89}
90
91fn parse_stat(stat: &str) -> Option<(&str, [&str; 22])> {
92    let (name, fields) = stat_parts(stat)?;
93    let mut fields = fields.split_whitespace();
94    // Only the first 22 fields are used. Keep them borrowed and on the stack;
95    // samples and identity checks need neither the rest nor a temporary Vec.
96    Some((name, std::array::from_fn(|_| fields.next().unwrap_or(""))))
97}
98
99fn key_values(text: &str) -> BTreeMap<String, u64> {
100    text.lines()
101        .filter_map(|line| {
102            let mut fields = line.split_whitespace();
103            let key = fields.next()?.trim_end_matches(':');
104            Some((key.to_string(), fields.next()?.parse().ok()?))
105        })
106        .collect()
107}
108
109/// Read Linux I/O and optionally PSS for an already selected observation.
110/// This does not revalidate identity; consumers retain before/after checks.
111pub fn read_details(proc_root: &Path, result: &mut ProcessObservation, memory_detail: bool) {
112    let io = fs::read_to_string(proc_root.join(result.pid.to_string()).join("io"))
113        .ok()
114        .map(|s| key_values(&s));
115    result.read_bytes = io.as_ref().and_then(|m| m.get("read_bytes").copied());
116    result.write_bytes = io.as_ref().and_then(|m| m.get("write_bytes").copied());
117    if memory_detail {
118        result.pss_bytes =
119            fs::read_to_string(proc_root.join(result.pid.to_string()).join("smaps_rollup"))
120                .ok()
121                .and_then(|s| key_values(&s).get("Pss").copied())
122                .map(|kb| kb.saturating_mul(1024));
123    }
124}
125/// Read all thread child lists from an explicitly selected procfs mount.
126#[must_use]
127pub fn children(proc_root: &Path, pid: u32) -> ChildProcesses {
128    let mut result = ChildProcesses::default();
129    // A Linux task's children file only lists children forked by that task.
130    // Inspect every thread, including nonleaders that spawn sandboxes/adapters.
131    // Never enumerate the top-level /proc directory to discover unrelated PIDs.
132    let tasks = proc_root.join(pid.to_string()).join("task");
133    let Ok(entries) = fs::read_dir(&tasks) else {
134        result.unavailable = true;
135        return result;
136    };
137    let mut children = BTreeSet::new();
138    let mut saw_task = false;
139    for entry in entries {
140        let Ok(entry) = entry else {
141            result.incomplete = true;
142            continue;
143        };
144        if entry
145            .file_name()
146            .to_str()
147            .and_then(|s| s.parse::<u32>().ok())
148            .is_none()
149        {
150            continue;
151        }
152        saw_task = true;
153        match fs::read_to_string(entry.path().join("children")) {
154            Ok(value) => {
155                for child in value.split_whitespace() {
156                    if let Ok(child) = child.parse::<u32>() {
157                        children.insert(child);
158                    } else {
159                        result.incomplete = true;
160                    }
161                }
162            }
163            Err(_) => {
164                // A thread can disappear during the scan; even that race can
165                // hide children reparented to a previously inspected thread.
166                result.incomplete = true;
167            }
168        }
169    }
170    if !saw_task {
171        result.unavailable = true;
172    }
173    result.pids = children.into_iter().collect();
174    result
175}
176
177#[cfg(all(test, unix))]
178mod tests;