1use std::cell::RefCell;
7use std::fs;
8use std::io;
9use std::os::unix::ffi::OsStrExt;
10use std::path::{Path, PathBuf};
11
12use crate::linux::parse::GpuUsage;
13use crate::row::Row;
14use crate::table::{Cache, Stable};
15use crate::Pid;
16
17thread_local! {
18 static BUF: RefCell<Vec<u8>> = RefCell::new(Vec::with_capacity(4096));
19 static PATH: RefCell<Vec<u8>> = RefCell::new(Vec::with_capacity(64));
20}
21
22fn read_into_buf(path: &[u8]) -> Option<usize> {
27 let mut file = fs::File::open(std::ffi::OsStr::from_bytes(path)).ok()?;
28 BUF.with(|b| {
29 let mut b = b.borrow_mut();
30 b.clear();
31 use std::io::Read as _;
32 file.read_to_end(&mut b).ok()
33 })
34}
35
36fn with_file<R>(path: &[u8], f: impl FnOnce(&[u8]) -> R) -> Option<R> {
38 let n = read_into_buf(path)?;
39 BUF.with(|b| {
40 let b = b.borrow();
41 Some(f(&b[..n]))
42 })
43}
44
45fn proc_path(root: &Path, pid: Pid, suffix: &str, f: impl FnOnce(&[u8])) {
48 PATH.with(|p| {
49 let mut p = p.borrow_mut();
50 p.clear();
51 p.extend_from_slice(root.as_os_str().as_bytes());
52 p.push(b'/');
53 write_pid(&mut p, pid);
54 if !suffix.is_empty() {
55 p.push(b'/');
56 p.extend_from_slice(suffix.as_bytes());
57 }
58 f(&p);
59 })
60}
61
62fn write_pid(out: &mut Vec<u8>, mut pid: Pid) {
63 let mut tmp = [0u8; 10];
64 let mut i = tmp.len();
65 if pid == 0 {
66 out.push(b'0');
67 return;
68 }
69 while pid > 0 {
70 i -= 1;
71 tmp[i] = b'0' + (pid % 10) as u8;
72 pid /= 10;
73 }
74 out.extend_from_slice(&tmp[i..]);
75}
76
77pub fn list_pids(root: &Path) -> io::Result<Vec<Pid>> {
79 let mut out = Vec::with_capacity(512);
80 for entry in fs::read_dir(root)? {
81 let Ok(entry) = entry else { continue };
82 let name = entry.file_name();
83 let bytes = name.as_bytes();
84 if bytes.is_empty() || !bytes.iter().all(u8::is_ascii_digit) {
85 continue;
86 }
87 if let Ok(pid) = name.to_string_lossy().parse::<Pid>() {
88 out.push(pid);
89 }
90 }
91 Ok(out)
92}
93
94pub fn list_tids(root: &Path, pid: Pid) -> io::Result<Vec<Pid>> {
99 let mut out = Vec::with_capacity(8);
100 let dir = root.join(pid.to_string()).join("task");
101 for entry in fs::read_dir(dir)? {
102 let Ok(entry) = entry else { continue };
103 let name = entry.file_name();
104 let bytes = name.as_bytes();
105 if bytes.is_empty() || !bytes.iter().all(u8::is_ascii_digit) {
106 continue;
107 }
108 if let Ok(tid) = name.to_string_lossy().parse::<Pid>() {
109 out.push(tid);
110 }
111 }
112 Ok(out)
113}
114
115pub fn read_thread_stat(
121 root: &Path,
122 pid: Pid,
123 tid: Pid,
124) -> Option<(crate::row::CommBuf, super::parse::StatFields)> {
125 let path = root
126 .join(pid.to_string())
127 .join("task")
128 .join(tid.to_string())
129 .join("stat");
130 let bytes = fs::read(path).ok()?;
131 let (_pid, comm, fields) = super::parse::parse_stat(&bytes)?;
132 Some((comm, fields))
133}
134
135pub fn read_stat(root: &Path, pid: Pid) -> Option<(crate::row::CommBuf, super::parse::StatFields)> {
137 let mut slot = None;
138 proc_path(root, pid, "stat", |p| {
139 slot = with_file(p, super::parse::parse_stat).flatten();
140 });
141 let (_pid, comm, fields) = slot?;
142 Some((comm, fields))
143}
144
145fn join_argv(b: &[u8]) -> String {
147 let mut s = String::with_capacity(b.len());
148 let mut first = true;
149 for part in b.split(|c| *c == 0) {
150 if part.is_empty() {
151 continue;
152 }
153 if !first {
154 s.push(' ');
155 }
156 s.push_str(&String::from_utf8_lossy(part));
157 first = false;
158 }
159 s
160}
161
162pub fn apply_stat(out: &mut Row, fields: &super::parse::StatFields, page_size: u64) {
164 super::parse::fill_row(out, fields, page_size);
165}
166
167pub fn read_identity(root: &Path, pid: Pid, starttime: u64, cache: &mut Cache) -> Stable {
169 if let Some(hit) = cache.get(pid, starttime) {
170 return hit.clone();
171 }
172
173 let mut stable = Stable::default();
174
175 proc_path(root, pid, "status", |p| {
176 if let Some(uid) = with_file(p, super::parse::parse_status_uid).flatten() {
177 stable.uid = uid;
178 }
179 });
180
181 let mut cmdline = None;
182 proc_path(root, pid, "cmdline", |p| {
183 cmdline = with_file(p, join_argv).filter(|s| !s.is_empty());
184 });
185 stable.cmdline = cmdline.map(String::into_boxed_str);
186
187 let mut exe = None;
188 let mut exe_deleted = false;
189 proc_path(root, pid, "exe", |p| {
190 if let Ok(target) = fs::read_link(std::ffi::OsStr::from_bytes(p)) {
191 let text = target.to_string_lossy();
192 exe_deleted = text.ends_with(" (deleted)");
197 let path = text.strip_suffix(" (deleted)").unwrap_or(&text);
198 exe = Some(path.to_string().into_boxed_str());
199 }
200 });
201 stable.exe = exe;
202 stable.exe_deleted = exe_deleted;
203
204 proc_path(root, pid, "cwd", |p| {
208 if let Ok(target) = fs::read_link(std::ffi::OsStr::from_bytes(p)) {
209 stable.cwd = Some(target.to_string_lossy().into_owned().into_boxed_str());
210 }
211 });
212
213 let mut cgroup = None;
218 #[cfg(target_os = "linux")]
219 let mut container = None;
220 #[cfg(target_os = "linux")]
221 let mut container_runtime = None;
222 proc_path(root, pid, "cgroup", |p| {
223 if let Some(entries) = with_file(p, super::fsops::parse_cgroup) {
224 cgroup = super::fsops::preferred_path(&entries)
227 .map(|s| s.to_string().into_boxed_str());
228 #[cfg(target_os = "linux")]
229 if let Some(c) = super::fsops::container_from_cgroup(&entries) {
230 container = Some(c.id.into_boxed_str());
231 container_runtime = Some(c.runtime.to_string().into_boxed_str());
232 }
233 }
234 });
235 stable.cgroup = cgroup;
236
237 #[cfg(target_os = "linux")]
250 if container.is_none()
251 && matches!(
252 super::fsops::in_different_pid_namespace(root, pid),
253 Some(true)
254 )
255 {
256 container = Some("?".to_string().into_boxed_str());
257 container_runtime = Some("container".to_string().into_boxed_str());
258 }
259
260 #[cfg(target_os = "linux")]
261 {
262 stable.container = container;
263 stable.container_runtime = container_runtime;
264 }
265
266 stable.is_kernel_thread = stable.cmdline.is_none() && stable.exe.is_none();
267
268 cache.insert(pid, starttime, stable.clone());
269 stable
270}
271
272pub fn read_meminfo(
274 root: &Path,
275 mem: &mut crate::system::MemInfo,
276 swap: &mut crate::system::SwapInfo,
277) {
278 let mut path = PathBuf::from(root);
279 path.push("meminfo");
280 with_file(path.as_os_str().as_bytes(), |b| {
281 super::parse::parse_meminfo(b, mem, swap)
282 });
283}
284
285pub fn read_proc_stat(root: &Path) -> Vec<crate::system::CpuTimes> {
287 let mut path = PathBuf::from(root);
288 path.push("stat");
289 with_file(path.as_os_str().as_bytes(), super::parse::parse_proc_stat).unwrap_or_default()
290}
291
292pub fn read_loadavg(root: &Path) -> Option<[f64; 3]> {
294 let mut path = PathBuf::from(root);
295 path.push("loadavg");
296 with_file(path.as_os_str().as_bytes(), super::parse::parse_loadavg)
297}
298
299pub fn read_uptime(root: &Path) -> Option<u64> {
301 let mut path = PathBuf::from(root);
302 path.push("uptime");
303 with_file(path.as_os_str().as_bytes(), |b| {
304 let s = String::from_utf8_lossy(b);
305 let secs = s.split_whitespace().next()?;
306 secs.split('.').next()?.parse::<u64>().ok()
307 })
308 .flatten()
309}
310
311pub fn is_same_process(root: &Path, pid: Pid, starttime: u64) -> bool {
314 read_stat(root, pid)
315 .map(|(_, fields)| fields.starttime == starttime)
316 .unwrap_or(false)
317}
318
319pub fn read_net_counters(root: &Path) -> crate::system::NetCounters {
324 let mut path = PathBuf::from(root);
325 path.push("net");
326 path.push("dev");
327 with_file(path.as_os_str().as_bytes(), super::parse::parse_net_dev).unwrap_or_default()
328}
329
330pub fn read_disk_counters(root: &Path) -> crate::system::DiskCounters {
337 let sys_block = root
340 .parent()
341 .map(|p| p.join("sys/block"))
342 .filter(|p| p.is_dir());
343
344 let mut path = PathBuf::from(root);
345 path.push("diskstats");
346 with_file(path.as_os_str().as_bytes(), |b| {
347 super::parse::parse_diskstats(b, |name| match &sys_block {
348 Some(dir) => dir
349 .join(std::ffi::OsStr::from_bytes(name))
350 .is_dir(),
351 None => super::parse::looks_like_whole_disk(name),
352 })
353 })
354 .unwrap_or_default()
355}
356
357pub fn read_psi(root: &Path) -> Option<crate::system::Psi> {
368 let mut psi = crate::system::Psi::default();
369 let read = |name: &str| -> Option<crate::system::PsiResource> {
370 let path = root.join("pressure").join(name);
371 with_file(path.as_os_str().as_bytes(), super::parse::parse_psi)
372 };
373
374 if let Some(r) = read("cpu") {
375 psi.cpu = r;
376 }
377 if let Some(r) = read("memory") {
378 psi.memory = r;
379 }
380 if let Some(r) = read("io") {
381 psi.io = r;
382 }
383 if let Some(r) = read("irq") {
384 psi.irq = r;
385 }
386
387 if psi.any_present() {
388 Some(psi)
389 } else {
390 None
391 }
392}
393
394pub fn read_zram(root: &Path) -> crate::system::ZramInfo {
399 let mut out = crate::system::ZramInfo::default();
400 let Some(sys) = root.parent().map(|p| p.join("sys/block")) else {
401 return out;
402 };
403 let Ok(entries) = fs::read_dir(&sys) else {
404 return out;
405 };
406
407 for entry in entries.flatten() {
408 let name = entry.file_name();
409 let name_bytes = name.as_bytes();
410 if !name_bytes.starts_with(b"zram")
413 || !name_bytes[b"zram".len()..].iter().all(u8::is_ascii_digit)
414 || name_bytes.len() == b"zram".len()
415 {
416 continue;
417 }
418 let Ok(name_str) = std::str::from_utf8(name_bytes) else {
419 continue;
420 };
421 let mm_stat = sys.join(&name).join("mm_stat");
422 if let Some(device) = with_file(mm_stat.as_os_str().as_bytes(), |b| {
423 super::parse::parse_zram_mm_stat(b, name_str)
424 })
425 .flatten()
426 {
427 out.devices.push(device);
428 }
429 }
430
431 out.devices.sort_by(|a, b| a.name.cmp(&b.name));
432 out
433}
434
435pub fn read_gpu_usage(proc_root: &Path, pid: crate::Pid) -> Option<GpuUsage> {
448 let fdinfo = proc_root.join(pid.to_string()).join("fdinfo");
449 let entries = fs::read_dir(fdinfo).ok()?;
450
451 let mut total: Option<GpuUsage> = None;
452 for entry in entries.flatten() {
453 let path = entry.path();
454 let Ok(bytes) = fs::read(&path) else {
455 continue;
456 };
457 let Some(usage) = super::parse::parse_fdinfo_drm(&bytes) else {
458 continue;
459 };
460 let acc = total.get_or_insert_with(GpuUsage::default);
461 acc.gpu_ns = acc.gpu_ns.saturating_add(usage.gpu_ns);
462 acc.busy_ns = acc.busy_ns.saturating_add(usage.busy_ns);
463 acc.mem_ns = acc.mem_ns.saturating_add(usage.mem_ns);
464 }
465 total
466}
467
468pub fn read_zswap_max_percent(root: &Path) -> Option<u64> {
474 let sys = root.parent()?.join("sys/module/zswap/parameters");
475 with_file(sys.join("max_pool_percent").as_os_str().as_bytes(), |b| {
476 let text = String::from_utf8_lossy(b);
477 text.trim().parse::<u64>().ok()
478 })
479 .flatten()
480}
481
482#[cfg(test)]
483mod tests {
484 use super::*;
485
486 #[test]
487 fn pid_writing() {
488 for pid in [0u32, 1, 9, 10, 42, 1234, 4_194_304] {
489 let mut v = Vec::new();
490 write_pid(&mut v, pid);
491 assert_eq!(String::from_utf8(v).unwrap(), pid.to_string());
492 }
493 }
494
495 #[test]
496 fn pid_path_shape() {
497 let mut seen = String::new();
498 proc_path(Path::new("/proc"), 42, "stat", |b| {
499 seen = String::from_utf8_lossy(b).into_owned();
500 });
501 assert_eq!(seen, "/proc/42/stat");
502
503 let mut root_only = String::new();
504 proc_path(Path::new("/proc"), 1, "", |b| {
505 root_only = String::from_utf8_lossy(b).into_owned();
506 });
507 assert_eq!(root_only, "/proc/1");
508 }
509
510 #[test]
511 fn argv_joining() {
512 assert_eq!(join_argv(b"/bin/sh\0-c\0echo hi\0"), "/bin/sh -c echo hi");
513 assert_eq!(join_argv(b""), "");
514 assert_eq!(join_argv(b"\0\0"), "");
515 }
516
517 #[test]
518 fn fixture_scan() {
519 let root = Path::new(concat!(env!("CARGO_MANIFEST_DIR"), "/tests/fixtures/proc"));
520 if !root.exists() {
521 return; }
523 let pids = list_pids(root).expect("readable fixture root");
524 assert!(!pids.is_empty());
525 }
526}