1use anyhow::{Result, anyhow};
36use serde::{Deserialize, Serialize};
37use std::fs::read_to_string;
38
39use crate::traits::{ToJson, ToPlainText, print_opt_val};
40use crate::utils::Size;
41
42#[derive(Debug, Serialize, Clone)]
44pub struct Processors {
45 pub entries: Vec<CPU>,
47}
48
49impl Processors {
50 pub fn new() -> Result<Self> {
51 Ok(Self {
52 entries: read_info()?,
53 })
54 }
55}
56
57impl ToJson for Processors {}
58impl ToPlainText for Processors {
59 fn to_plain(&self) -> String {
60 let mut s = "Information about processors".to_string();
61 for proc in &self.entries {
62 s += &proc.to_plain();
63 }
64 s
65 }
66}
67
68#[derive(Debug, Serialize, Default, Clone)]
70pub struct CPU {
71 pub processor: Option<usize>,
73
74 pub vendor_id: Option<String>,
79
80 pub cpu_family: Option<u32>,
82
83 pub model: Option<u32>,
85
86 pub model_name: Option<String>,
88
89 pub stepping: Option<u32>,
91
92 pub microcode: Option<String>,
94
95 pub cpu_mhz: Option<f32>,
97
98 pub cache_size: Option<Size>,
100
101 pub physical_id: Option<u32>,
103
104 pub siblings: Option<u32>,
106
107 pub core_id: Option<u32>,
109
110 pub cpu_cores: Option<u32>,
112
113 pub apicid: Option<u32>,
115
116 pub initial_apicid: Option<u32>,
118
119 pub fpu: Option<bool>,
121
122 pub fpu_exception: Option<bool>,
123 pub cpuid_level: Option<u32>,
124 pub wp: Option<bool>,
125 pub flags: Option<Vec<String>>,
126 pub bugs: Option<Vec<String>>,
127 pub bogomips: Option<f64>,
128 pub clflush_size: Option<u32>,
129 pub cache_alignment: Option<u32>,
130 pub address_sizes: Option<String>,
131 pub power_management: Option<String>,
132
133 pub cpu_implementer: Option<String>,
137 pub cpu_architecture: Option<u8>,
138 pub cpu_variant: Option<String>,
139 pub cpu_part: Option<String>,
140 pub cpu_revision: Option<u32>,
141
142 pub cpu: Option<String>,
146 pub clock: Option<f32>,
147 pub revision: Option<String>,
148 pub timebase: Option<usize>,
149 pub platform: Option<String>,
150 pub machine: Option<String>,
151 pub model_ppc: Option<String>,
152}
153
154impl ToJson for CPU {}
155
156#[cfg(not(target_arch = "aarch64"))]
157impl ToPlainText for CPU {
158 fn to_plain(&self) -> String {
159 let mut s = match self.processor {
160 Some(proc) => format!("\nProcessor #{proc}\n"),
161 None => "\nProcessor #unknown\n".to_string(),
162 };
163 s += "\tArchitecture: x86_64\n";
164 s += &print_opt_val("Vendor ID", &self.vendor_id);
165 s += &print_opt_val("CPU Family", &self.cpu_family);
166 s += &print_opt_val("CPU Model ID", &self.model);
167 s += &print_opt_val("CPU Model Name", &self.model_name);
168 s += &print_opt_val("Stepping", &self.stepping);
169 s += &print_opt_val("Microcode", &self.microcode);
170 s += &print_opt_val("Current frequency", &self.cpu_mhz);
171 s += &print_opt_val("L3 Cache Size", &self.cache_size);
172 s += &print_opt_val("Physical ID of CPU Core", &self.physical_id);
173 s += &print_opt_val("Siblings", &self.siblings);
174 s += &print_opt_val("Core ID", &self.core_id);
175 s += &print_opt_val("CPU cores", &self.cpu_cores);
176 s += &print_opt_val("APIC ID", &self.apicid);
177 s += &print_opt_val("Initial APIC ID", &self.initial_apicid);
178 s += &print_opt_val("FPU", &self.fpu);
179 s += &print_opt_val("FPU Exception", &self.fpu_exception);
180 s += &print_opt_val("CPUID Level", &self.cpuid_level);
181 s += &print_opt_val("WP", &self.wp);
182 s += &print_opt_val("Bogo MIPS", &self.bogomips);
183 s += &print_opt_val("Clflush Size", &self.clflush_size);
184 s += &print_opt_val("Cache alignment", &self.cache_alignment);
185 s += &print_opt_val("Address sizes", &self.address_sizes);
186 s += &print_opt_val("Power management", &self.power_management);
187
188 s
189 }
190}
191
192#[cfg(target_arch = "aarch64")]
193impl ToPlainText for CPU {
194 fn to_plain(&self) -> String {
195 let mut s = match self.processor {
196 Some(proc) => format!("Processor #{proc}\n"),
197 None => format!("Processor #unknown\n"),
198 };
199 s += &print_opt_val("CPU Implementer", &self.cpu_implementer);
200 s += &print_opt_val("CPU Architecture", &self.cpu_architecture);
201 s += &print_opt_val("CPU Variant", &self.cpu_variant);
202 s += &print_opt_val("CPU Part", &self.cpu_part);
203 s += &print_opt_val("CPU Revision", &self.cpu_revision);
204
205 s
206 }
207}
208
209fn read_info() -> Result<Vec<CPU>> {
210 let blocks = read_to_string("/proc/cpuinfo")
211 .map_err(|err| anyhow!("read_info(): Failed to read `/proc/cpuinfo` file: {err}"))?;
212 let blocks = blocks
213 .split("\n\n") .collect::<Vec<_>>();
215 let mut processors = Vec::with_capacity(blocks.len());
216
217 for block in blocks {
218 if block.trim().is_empty() {
219 continue;
220 }
221 let mut cpu = CPU::default();
222 for line in block.lines() {
223 parse_cpuinfo(&mut cpu, line);
224 }
225 processors.push(cpu);
226 }
227 Ok(processors)
228}
229
230fn get_parts(s: &str) -> impl Iterator<Item = &str> {
231 s.splitn(2, ':').map(|item| item.trim())
232}
233
234#[cfg(not(target_arch = "aarch64"))]
235fn parse_cpuinfo(cpu: &mut CPU, parts: &str) {
236 let mut parts = get_parts(parts);
237 if let (Some(key), Some(val)) = (parts.next(), parts.next()) {
238 match key {
239 "processor" => cpu.processor = val.parse().ok(),
240 "vendor_id" => cpu.vendor_id = Some(val.to_string()),
241 "cpu family" => cpu.cpu_family = val.parse().ok(),
242 "model" => cpu.model = val.parse().ok(),
243 "model name" => cpu.model_name = Some(val.to_string()),
244 "stepping" => cpu.stepping = val.parse().ok(),
245 "microcode" => cpu.microcode = Some(val.to_string()),
246 "cpu MHz" => cpu.cpu_mhz = val.parse().ok(),
247 "cache size" => cpu.cache_size = Size::try_from(val).ok(),
248 "physical id" => cpu.physical_id = val.parse().ok(),
249 "siblings" => cpu.siblings = val.parse().ok(),
250 "core id" => cpu.core_id = val.parse().ok(),
251 "cpu cores" => cpu.cpu_cores = val.parse().ok(),
252 "apicid" => cpu.apicid = val.parse().ok(),
253 "initial apicid" => cpu.initial_apicid = val.parse().ok(),
254 "fpu" => cpu.fpu = Some(get_bool(val)),
255 "fpu_exception" => cpu.fpu_exception = Some(get_bool(val)),
256 "cpuid level" => cpu.cpuid_level = val.parse().ok(),
257 "wp" => cpu.wp = Some(get_bool(val)),
258 "flags" | "Features" => {
259 cpu.flags = Some(val.split_whitespace().map(String::from).collect())
260 }
261 "bugs" => cpu.bugs = Some(val.split_whitespace().map(String::from).collect()),
262 "bogomips" | "BogoMIPS" => cpu.bogomips = val.parse().ok(),
263 "clflush size" => cpu.clflush_size = val.parse().ok(),
264 "cache_alignment" => cpu.cache_alignment = val.parse().ok(),
265 "address sizes" => cpu.address_sizes = Some(val.to_string()),
266 "power management" => cpu.power_management = Some(val.to_string()),
267 _ => {} }
269 }
270}
271
272#[cfg(target_arch = "aarch64")]
273fn parse_cpuinfo(cpu: &mut CPU, parts: &str) {
274 let mut parts = get_parts(parts);
275 match (parts.next(), parts.next()) {
276 (Some(key), Some(val)) => match key {
277 "CPU implementer" => cpu.cpu_implementer = Some(val.to_string()),
279 "CPU architecture" => cpu.cpu_architecture = val.parse().ok(),
280 "CPU variant" => cpu.cpu_variant = Some(val.to_string()),
281 "CPU part" => cpu.cpu_part = Some(val.to_string()),
282 "CPU revision" => cpu.cpu_revision = val.parse().ok(),
283 _ => {} },
285 _ => {}
286 }
287}
288
289fn get_bool(s: &str) -> bool {
290 matches!(s, "yes" | "ok")
291}
292
293#[derive(Debug, Deserialize, Serialize, Clone, Default)]
295pub struct Stat {
296 pub cpu: Option<CpuUsage>,
297 pub cpus: Vec<CpuUsage>,
298 pub interrupts: Option<u64>,
299 pub context_switches: Option<u64>,
300 pub boot_time: Option<u64>,
301 pub processes_created: Option<u64>,
302 pub processes_running: Option<u64>,
303 pub processes_blocked: Option<u64>,
304 pub softirq: Option<SoftIrq>,
305}
306
307impl Stat {
308 pub fn new() -> Result<Self> {
309 parse_proc_stat()
310 }
311}
312
313#[derive(Debug, Deserialize, Serialize, Clone, Copy, Default)]
314pub struct CpuUsage {
315 pub user: Option<u64>,
316 pub nice: Option<u64>,
317 pub system: Option<u64>,
318 pub idle: Option<u64>,
319 pub iowait: Option<u64>,
320 pub irq: Option<u64>,
321 pub softirq: Option<u64>,
322 pub steal: Option<u64>,
323 pub guest: Option<u64>,
324 pub guest_nice: Option<u64>,
325}
326
327impl CpuUsage {
328 pub fn total_time(&self) -> u64 {
329 self.user.unwrap_or(0)
330 + self.nice.unwrap_or(0)
331 + self.system.unwrap_or(0)
332 + self.idle.unwrap_or(0)
333 + self.iowait.unwrap_or(0)
334 + self.irq.unwrap_or(0)
335 + self.softirq.unwrap_or(0)
336 + self.steal.unwrap_or(0)
337 }
338
339 pub fn active_time(&self) -> u64 {
340 self.total_time() - self.idle.unwrap_or(0) - self.iowait.unwrap_or(0)
341 }
342
343 pub fn usage_percentage(&self, prev: Option<Self>) -> f32 {
344 if prev.is_none() {
345 return 0.0;
346 }
347 let prev = prev.unwrap();
348
349 let total_diff = self.total_time().wrapping_sub(prev.total_time()); let active_diff = self.active_time().wrapping_sub(prev.active_time());
351
352 if total_diff > 0 {
353 (active_diff as f32 / total_diff as f32) * 100.0
354 } else {
355 0.0
356 }
357 }
358}
359
360impl From<&str> for CpuUsage {
361 fn from(value: &str) -> Self {
362 let parts = value.split_whitespace().collect::<Vec<&str>>();
363 if parts.is_empty() || parts.len() < 11 {
364 return Self::default();
365 }
366
367 Self {
368 user: parts[1].parse().ok(),
369 nice: parts[2].parse().ok(),
370 system: parts[3].parse().ok(),
371 idle: parts[4].parse().ok(),
372 iowait: parts[5].parse().ok(),
373 irq: parts[6].parse().ok(),
374 softirq: parts[7].parse().ok(),
375 steal: parts[8].parse().ok(),
376 guest: parts[9].parse().ok(),
377 guest_nice: parts[10].parse().ok(),
378 }
379 }
380}
381
382#[derive(Debug, Deserialize, Serialize, Clone, Copy, Default)]
383pub struct SoftIrq {
384 pub total: Option<u64>,
385 pub hi: Option<u64>,
386 pub timer: Option<u64>,
387 pub net_tx: Option<u64>,
388 pub net_rx: Option<u64>,
389 pub block: Option<u64>,
390 pub irq_poll: Option<u64>,
391 pub tasklet: Option<u64>,
392 pub shed: Option<u64>,
393 pub hrtimer: Option<u64>,
394 pub rcu: Option<u64>,
395}
396
397impl From<&str> for SoftIrq {
398 fn from(value: &str) -> Self {
399 let parts = value.split_whitespace().collect::<Vec<&str>>();
400 if parts.is_empty() || parts.len() < 12 {
401 return Self::default();
402 }
403
404 Self {
405 total: parts[1].parse().ok(),
406 hi: parts[2].parse().ok(),
407 timer: parts[3].parse().ok(),
408 net_tx: parts[4].parse().ok(),
409 net_rx: parts[5].parse().ok(),
410 block: parts[6].parse().ok(),
411 irq_poll: parts[7].parse().ok(),
412 tasklet: parts[8].parse().ok(),
413 shed: parts[9].parse().ok(),
414 hrtimer: parts[10].parse().ok(),
415 rcu: parts[11].parse().ok(),
416 }
417 }
418}
419
420fn parse_proc_stat() -> Result<Stat> {
421 let content = read_to_string("/proc/stat")
422 .map_err(|err| anyhow!("parse_proc_stat(): Failed to read `/proc/stat` file: {err}"))?;
423 let mut stat = Stat::default();
424
425 for line in content.lines() {
426 let parts = line.split_whitespace().collect::<Vec<&str>>();
427 if parts.is_empty() {
428 continue;
429 }
430 match parts[0] {
431 "cpu" => {
432 if parts.len() >= 11 {
433 stat.cpu = Some(CpuUsage::from(line));
434 }
435 }
436 key if key.starts_with("cpu")
437 && key[3..]
438 .chars()
439 .next()
440 .map(|c| c.is_ascii_digit())
441 .unwrap_or(false) =>
442 {
443 if parts.len() >= 11 {
444 stat.cpus.push(CpuUsage::from(line));
445 }
446 }
447 "intr" => {
448 if parts.len() >= 2 {
449 stat.interrupts = parts[1].parse().ok();
450 }
451 }
452 "ctxt" => {
453 if parts.len() >= 2 {
454 stat.context_switches = parts[1].parse().ok();
455 }
456 }
457 "btime" => {
458 if parts.len() >= 2 {
459 stat.boot_time = parts[1].parse().ok();
460 }
461 }
462 "processes" => {
463 if parts.len() >= 2 {
464 stat.processes_created = parts[1].parse().ok();
465 }
466 }
467 "procs_running" if parts.len() >= 2 => {
468 stat.processes_running = parts[1].parse().ok();
469 }
470 "procs_blocked" if parts.len() >= 2 => {
471 stat.processes_blocked = parts[1].parse().ok();
472 }
473 "softirq" if parts.len() >= 12 => {
474 stat.softirq = Some(SoftIrq::from(line));
475 }
476 _ => {}
477 }
478 }
479 Ok(stat)
480}