|
| 1 | +// |
| 2 | +// proc_stat.rs |
| 3 | +// |
| 4 | +// Copyright (C) 2026 Posit Software, PBC. All rights reserved. |
| 5 | +// Licensed under the Elastic License 2.0. See LICENSE.txt for license information. |
| 6 | +// |
| 7 | + |
| 8 | +//! Linux-specific utilities for parsing /proc filesystem. |
| 9 | +//! |
| 10 | +//! This module provides shared utilities for parsing /proc/[pid]/stat files, |
| 11 | +//! used by both the process tree enumeration and CPU tracking code. |
| 12 | +
|
| 13 | +#![cfg(target_os = "linux")] |
| 14 | + |
| 15 | +use std::fs; |
| 16 | + |
| 17 | +/// Parsed fields from /proc/[pid]/stat |
| 18 | +#[derive(Debug, Clone)] |
| 19 | +pub struct ProcStat { |
| 20 | + /// Parent process ID (field 4, index 1 after comm) |
| 21 | + pub ppid: u32, |
| 22 | + /// Process group ID (field 5, index 2 after comm) |
| 23 | + pub pgid: u32, |
| 24 | + /// User mode CPU time in jiffies (field 14, index 11 after comm) |
| 25 | + pub utime: u64, |
| 26 | + /// Kernel mode CPU time in jiffies (field 15, index 12 after comm) |
| 27 | + pub stime: u64, |
| 28 | +} |
| 29 | + |
| 30 | +impl ProcStat { |
| 31 | + /// Total CPU time (utime + stime) |
| 32 | + pub fn cpu_time(&self) -> u64 { |
| 33 | + self.utime + self.stime |
| 34 | + } |
| 35 | +} |
| 36 | + |
| 37 | +/// Parse /proc/[pid]/stat to extract process information. |
| 38 | +/// |
| 39 | +/// The stat file format is: `pid (comm) state ppid pgrp session tty_nr tpgid flags |
| 40 | +/// minflt cminflt majflt cmajflt utime stime cutime cstime ...` |
| 41 | +/// |
| 42 | +/// Note: `comm` can contain spaces and parentheses, so we find the last ')' to |
| 43 | +/// reliably parse the remaining fields. |
| 44 | +pub fn parse_proc_stat(pid: u32) -> Option<ProcStat> { |
| 45 | + let stat_path = format!("/proc/{}/stat", pid); |
| 46 | + let stat_content = fs::read_to_string(stat_path).ok()?; |
| 47 | + |
| 48 | + // comm can contain spaces and parens, so find the last ')' |
| 49 | + let last_paren = stat_content.rfind(')')?; |
| 50 | + let fields_after_comm = stat_content.get(last_paren + 2..)?; // Skip ") " |
| 51 | + let fields: Vec<&str> = fields_after_comm.split_whitespace().collect(); |
| 52 | + |
| 53 | + // fields[0] = state, fields[1] = ppid, fields[2] = pgrp, ... |
| 54 | + // fields[11] = utime, fields[12] = stime |
| 55 | + if fields.len() < 13 { |
| 56 | + return None; |
| 57 | + } |
| 58 | + |
| 59 | + Some(ProcStat { |
| 60 | + ppid: fields[1].parse().ok()?, |
| 61 | + pgid: fields[2].parse().ok()?, |
| 62 | + utime: fields[11].parse().ok()?, |
| 63 | + stime: fields[12].parse().ok()?, |
| 64 | + }) |
| 65 | +} |
| 66 | + |
| 67 | +/// Read total CPU time from /proc/stat (sum of all jiffies across all CPUs). |
| 68 | +/// |
| 69 | +/// The first line of /proc/stat is: |
| 70 | +/// `cpu user nice system idle iowait irq softirq steal guest guest_nice` |
| 71 | +/// |
| 72 | +/// We sum all these values to get total CPU time. |
| 73 | +pub fn read_total_cpu_time() -> u64 { |
| 74 | + let Ok(content) = fs::read_to_string("/proc/stat") else { |
| 75 | + return 0; |
| 76 | + }; |
| 77 | + |
| 78 | + let Some(cpu_line) = content.lines().next() else { |
| 79 | + return 0; |
| 80 | + }; |
| 81 | + |
| 82 | + if !cpu_line.starts_with("cpu ") { |
| 83 | + return 0; |
| 84 | + } |
| 85 | + |
| 86 | + // Sum all the values (skip "cpu" label) |
| 87 | + cpu_line |
| 88 | + .split_whitespace() |
| 89 | + .skip(1) |
| 90 | + .filter_map(|s| s.parse::<u64>().ok()) |
| 91 | + .sum() |
| 92 | +} |
| 93 | + |
| 94 | +/// Count the number of CPUs by counting cpu[N] lines in /proc/stat. |
| 95 | +pub fn count_cpus() -> usize { |
| 96 | + let Ok(content) = fs::read_to_string("/proc/stat") else { |
| 97 | + return 1; |
| 98 | + }; |
| 99 | + |
| 100 | + // Count lines starting with "cpu" followed by a digit (cpu0, cpu1, etc.) |
| 101 | + content |
| 102 | + .lines() |
| 103 | + .filter(|line| { |
| 104 | + line.starts_with("cpu") |
| 105 | + && line |
| 106 | + .chars() |
| 107 | + .nth(3) |
| 108 | + .map(|c| c.is_ascii_digit()) |
| 109 | + .unwrap_or(false) |
| 110 | + }) |
| 111 | + .count() |
| 112 | + .max(1) |
| 113 | +} |
| 114 | + |
| 115 | +#[cfg(test)] |
| 116 | +mod tests { |
| 117 | + use super::*; |
| 118 | + |
| 119 | + #[test] |
| 120 | + fn test_read_total_cpu_time() { |
| 121 | + // Should return a non-zero value on a real Linux system |
| 122 | + let cpu_time = read_total_cpu_time(); |
| 123 | + // Just verify it doesn't panic and returns something reasonable |
| 124 | + assert!(cpu_time > 0 || cfg!(not(target_os = "linux"))); |
| 125 | + } |
| 126 | + |
| 127 | + #[test] |
| 128 | + fn test_count_cpus() { |
| 129 | + let cpus = count_cpus(); |
| 130 | + assert!(cpus >= 1); |
| 131 | + } |
| 132 | + |
| 133 | + #[test] |
| 134 | + fn test_parse_proc_stat_current_process() { |
| 135 | + // Parse our own process's stat |
| 136 | + let pid = std::process::id(); |
| 137 | + if let Some(stat) = parse_proc_stat(pid) { |
| 138 | + // Our parent PID should be non-zero |
| 139 | + assert!(stat.ppid > 0); |
| 140 | + // PGID should be set |
| 141 | + assert!(stat.pgid > 0); |
| 142 | + } |
| 143 | + // It's OK if this fails on non-Linux or in restricted environments |
| 144 | + } |
| 145 | +} |
0 commit comments