ic_host_process/observation/procfs/
mod.rs1use super::{ChildProcesses, ProcessObservation};
3use std::{collections::BTreeSet, fs, path::Path, sync::OnceLock};
4
5fn ticks_per_second() -> u64 {
6 static VALUE: OnceLock<u64> = OnceLock::new();
7 *VALUE.get_or_init(|| {
8 #[cfg(unix)]
9 return positive_sysconf(libc::_SC_CLK_TCK, 100);
10 #[cfg(not(unix))]
11 100
12 })
13}
14
15fn page_size() -> u64 {
16 static VALUE: OnceLock<u64> = OnceLock::new();
17 *VALUE.get_or_init(|| {
18 #[cfg(unix)]
19 return positive_sysconf(libc::_SC_PAGESIZE, 4096);
20 #[cfg(not(unix))]
21 4096
22 })
23}
24
25#[cfg(unix)]
26#[expect(
27 unsafe_code,
28 reason = "read signed sysconf results for the existing fallback contract"
29)]
30fn positive_sysconf(name: libc::c_int, fallback: u64) -> u64 {
31 let value = unsafe { libc::sysconf(name) };
35 if value > 0 {
36 u64::try_from(value).ok().unwrap_or_default()
37 } else {
38 fallback
39 }
40}
41
42#[must_use]
44pub fn process_identity(proc_root: &Path, pid: u32) -> Option<u64> {
45 let stat = fs::read_to_string(proc_root.join(pid.to_string()).join("stat")).ok()?;
46 let (_, fields) = stat_parts(&stat)?;
47 let mut fields = fields.split_whitespace();
48 fields.next()?; fields.next()?.parse::<u64>().ok()?; fields.nth(17)?.parse().ok() }
52
53#[must_use]
55pub fn basic_process(proc_root: &Path, pid: u32) -> Option<ProcessObservation> {
56 let stat = fs::read_to_string(proc_root.join(pid.to_string()).join("stat")).ok()?;
57 let (name, fields) = parse_stat(&stat)?;
58 let number = |index: usize| fields.get(index)?.parse::<u64>().ok();
59 let ns = |index| number(index).unwrap_or(0).saturating_mul(1_000_000_000) / ticks_per_second();
60 Some(ProcessObservation {
61 pid,
62 ppid: u32::try_from(number(1)?).ok()?,
63 start_ticks: number(19)?,
64 name: name.to_owned(),
65 user_ns: ns(11),
66 system_ns: ns(12),
67 waited_user_ns: ns(13),
68 waited_system_ns: ns(14),
69 rss_bytes: number(21).unwrap_or(0).saturating_mul(page_size()),
70 pss_bytes: None,
71 read_bytes: None,
72 write_bytes: None,
73 minor_faults: number(7)
74 .unwrap_or(0)
75 .saturating_add(number(8).unwrap_or(0)),
76 major_faults: number(9)
77 .unwrap_or(0)
78 .saturating_add(number(10).unwrap_or(0)),
79 threads: number(17).unwrap_or(0),
80 })
81}
82
83fn stat_parts(stat: &str) -> Option<(&str, &str)> {
84 let begin = stat.find('(')?;
85 let end = stat.rfind(')')?;
86 Some((stat.get(begin + 1..end)?, stat.get(end + 1..)?))
87}
88
89fn parse_stat(stat: &str) -> Option<(&str, [&str; 22])> {
90 let (name, fields) = stat_parts(stat)?;
91 let mut fields = fields.split_whitespace();
92 Some((name, std::array::from_fn(|_| fields.next().unwrap_or(""))))
95}
96
97fn field_value(text: &str, name: &str) -> Option<u64> {
98 text.lines()
99 .filter_map(|line| {
100 let mut fields = line.split_whitespace();
101 if fields.next()?.trim_end_matches(':') != name {
102 return None;
103 }
104 fields.next()?.parse().ok()
105 })
106 .next_back()
107}
108
109pub 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")).ok();
113 result.read_bytes = io
114 .as_deref()
115 .and_then(|text| field_value(text, "read_bytes"));
116 result.write_bytes = io
117 .as_deref()
118 .and_then(|text| field_value(text, "write_bytes"));
119 if memory_detail {
120 result.pss_bytes =
121 fs::read_to_string(proc_root.join(result.pid.to_string()).join("smaps_rollup"))
122 .ok()
123 .and_then(|text| field_value(&text, "Pss"))
124 .map(|kb| kb.saturating_mul(1024));
125 }
126}
127#[must_use]
129pub fn children(proc_root: &Path, pid: u32) -> ChildProcesses {
130 let mut result = ChildProcesses::default();
131 let tasks = proc_root.join(pid.to_string()).join("task");
135 let Ok(entries) = fs::read_dir(&tasks) else {
136 result.unavailable = true;
137 return result;
138 };
139 let mut children = BTreeSet::new();
140 let mut saw_task = false;
141 for entry in entries {
142 let Ok(entry) = entry else {
143 result.incomplete = true;
144 continue;
145 };
146 if entry
147 .file_name()
148 .to_str()
149 .and_then(|s| s.parse::<u32>().ok())
150 .is_none()
151 {
152 continue;
153 }
154 saw_task = true;
155 match fs::read_to_string(entry.path().join("children")) {
156 Ok(value) => {
157 for child in value.split_whitespace() {
158 if let Ok(child) = child.parse::<u32>() {
159 children.insert(child);
160 } else {
161 result.incomplete = true;
162 }
163 }
164 }
165 Err(_) => {
166 result.incomplete = true;
169 }
170 }
171 }
172 if !saw_task {
173 result.unavailable = true;
174 }
175 result.pids = children.into_iter().collect();
176 result
177}
178
179#[cfg(all(test, unix))]
180mod tests;