prepare for windows impl

This commit is contained in:
Robin Appelman 2023-05-17 22:55:01 +02:00
commit d12b70d11e
16 changed files with 550 additions and 279 deletions

210
src/linux/disk/mod.rs Normal file
View file

@ -0,0 +1,210 @@
use crate::{Error, MultiSensorSource, Result, SensorData};
use ahash::{AHashSet, AHasher};
use regex::Regex;
use std::ffi::CString;
use std::fmt::Write;
use std::fs::File;
use std::hash::{Hash, Hasher};
use std::io::{Read, Seek};
use std::mem::MaybeUninit;
pub mod zfs;
#[derive(Debug, Clone, Default)]
pub struct DiskStats {
pub interface: String,
pub bytes_sent: u64,
pub bytes_received: u64,
}
impl SensorData for DiskStats {
fn write<W: Write>(&self, mut w: W, hostname: &str) {
if self.bytes_received > 0 || self.bytes_sent > 0 {
writeln!(
&mut w,
"disk_sent{{host=\"{}\", disk=\"{}\"}} {}",
hostname, self.interface, self.bytes_sent
)
.ok();
writeln!(
&mut w,
"disk_received{{host=\"{}\", disk=\"{}\"}} {}",
hostname, self.interface, self.bytes_received
)
.ok();
}
}
}
pub struct DiskStatSource {
source: File,
buff: String,
regex: Regex,
}
impl DiskStatSource {
pub fn new() -> Result<DiskStatSource> {
Ok(DiskStatSource {
source: File::open("/proc/diskstats")?,
buff: String::new(),
regex: Regex::new(r" ([sv]d[a-z]+|nvme[0-9]n[0-9]|mmcblk[0-9]) ").unwrap(),
})
}
}
impl MultiSensorSource for DiskStatSource {
type Data = DiskStats;
type Iter<'a> = DiskStatParser<'a>;
fn read(&mut self) -> Result<Self::Iter<'_>> {
self.buff.clear();
self.source.rewind()?;
self.source.read_to_string(&mut self.buff)?;
Ok(DiskStatParser {
lines: self.buff.lines(),
regex: &self.regex,
})
}
}
pub struct DiskStatParser<'a> {
lines: std::str::Lines<'a>,
regex: &'a Regex,
}
impl Iterator for DiskStatParser<'_> {
type Item = Result<DiskStats>;
fn next(&mut self) -> Option<Self::Item> {
let line = loop {
let line = self.lines.next()?;
if self.regex.is_match(line) {
break line;
}
};
let mut parts = line.split_whitespace().skip(2);
let name: String = parts.next()?.into();
let _read_count = parts.next();
let _read_merged_count = parts.next();
let read_sectors = parts.next()?.parse::<u64>().ok()?;
let mut parts = parts.skip(1);
let _write_count = parts.next();
let _write_merged_count = parts.next();
let write_sectors = parts.next()?.parse::<u64>().ok()?;
Some(Ok(DiskStats {
interface: name,
bytes_sent: write_sectors * 512,
bytes_received: read_sectors * 512,
}))
}
}
#[derive(Clone, Debug)]
pub struct DiskUsage {
pub name: String,
pub size: u64,
pub free: u64,
}
impl SensorData for DiskUsage {
fn write<W: Write>(&self, mut w: W, hostname: &str) {
if self.size > 0 {
writeln!(
&mut w,
"disk_size{{host=\"{}\", disk=\"{}\"}} {}",
hostname, self.name, self.size
)
.ok();
writeln!(
&mut w,
"disk_free{{host=\"{}\", disk=\"{}\"}} {}",
hostname, self.name, self.free
)
.ok();
}
}
}
pub struct DiskUsageSource {
source: File,
buff: String,
}
impl DiskUsageSource {
pub fn new() -> Result<DiskUsageSource> {
Ok(DiskUsageSource {
source: File::open("/proc/mounts")?,
buff: String::new(),
})
}
}
impl MultiSensorSource for DiskUsageSource {
type Data = DiskUsage;
type Iter<'a> = DiskUsageParser<'a>;
fn read(&mut self) -> Result<Self::Iter<'_>> {
self.buff.clear();
self.source.rewind()?;
self.source.read_to_string(&mut self.buff)?;
Ok(DiskUsageParser {
lines: self.buff.lines(),
found_disks: AHashSet::with_capacity(16),
})
}
}
pub struct DiskUsageParser<'a> {
lines: std::str::Lines<'a>,
found_disks: AHashSet<u64>,
}
impl Iterator for DiskUsageParser<'_> {
type Item = Result<DiskUsage>;
fn next(&mut self) -> Option<Self::Item> {
let line = loop {
let line = self.lines.next()?;
if line.starts_with('/') && !line.contains("/dev/loop") && !line.contains("fuse") {
break line;
}
};
let mut parts = line.split_ascii_whitespace();
let disk = parts.next()?;
if !self.found_disks.insert(hash_str(disk)) {
return None;
}
let mount_point = parts.next()?;
let stat = match statvfs(&mount_point) {
Ok(stat) => stat,
Err(e) => return Some(Err(e)),
};
Some(Ok(DiskUsage {
name: mount_point.to_string(),
size: stat.f_blocks * stat.f_frsize as u64,
free: stat.f_bavail * stat.f_frsize as u64,
}))
}
}
fn statvfs(path: &str) -> Result<libc::statvfs> {
let path = CString::new(path)?;
let mut vfs = MaybeUninit::<libc::statvfs>::uninit();
let result = unsafe { libc::statvfs(path.as_ptr(), vfs.as_mut_ptr()) };
if result == 0 {
let vfs = unsafe { vfs.assume_init() };
Ok(vfs)
} else {
Err(Error::StatVfs)
}
}
fn hash_str(s: &str) -> u64 {
let mut hasher = AHasher::default();
s.hash(&mut hasher);
hasher.finish()
}

154
src/linux/disk/zfs.rs Normal file
View file

@ -0,0 +1,154 @@
use crate::linux::disk::DiskUsage;
use crate::Result;
use std::fmt::Write;
use std::fs::read_to_string;
use std::process::Command;
use std::str::FromStr;
use std::sync::atomic::{AtomicBool, Ordering};
use tracing::warn;
static CAN_READ: AtomicBool = AtomicBool::new(true);
pub fn pools() -> impl Iterator<Item = DiskUsage> {
if !CAN_READ.load(Ordering::Relaxed) {
return ZPoolOutputParser::default();
}
ZPoolOutputParser {
str: zpool_command().unwrap_or_default(),
pos: 0,
}
}
fn zpool_command() -> Result<String> {
let mut z = Command::new("zpool");
z.args(&["list", "-p", "-H", "-o", "name,size,free"]);
let out = z.output()?;
if out.status.success() {
Ok(String::from_utf8(out.stdout)?)
} else {
CAN_READ.store(false, Ordering::Relaxed);
warn!(
status = out.status.code().unwrap_or(-1),
stdout = String::from_utf8(out.stdout).unwrap_or_else(|_| String::from("non utf8")),
stderr = String::from_utf8(out.stderr).unwrap_or_else(|_| String::from("non utf8")),
"Failed to list zpool status"
);
Ok(String::new())
}
}
fn parse_line(line: &str) -> Option<DiskUsage> {
let mut parts = line.split_ascii_whitespace();
let name = parts.next()?.to_string();
let size = parts.next()?.parse().ok()?;
let free = parts.next()?.parse().ok()?;
Some(DiskUsage { name, size, free })
}
#[derive(Default)]
struct ZPoolOutputParser {
str: String,
pos: usize,
}
impl Iterator for ZPoolOutputParser {
type Item = DiskUsage;
fn next(&mut self) -> Option<Self::Item> {
let str = self.str.as_str();
let line = match str[self.pos..].find('\n') {
Some(next_pos) => {
let old_pos = self.pos;
self.pos += next_pos + 1;
Some(&str[old_pos..self.pos])
}
None if self.pos < str.len() => {
let old_pos = self.pos;
self.pos = str.len();
Some(&str[old_pos..])
}
None => None,
};
line.and_then(parse_line)
}
}
#[derive(Debug, Default)]
pub struct ArcStats {
hits: u64,
misses: u64,
prefetch: u64,
size: u64,
}
impl ArcStats {
pub fn write<W: Write>(&self, mut w: W, hostname: &str) {
writeln!(
&mut w,
"zfs_arc_hits{{host=\"{}\"}} {}",
hostname, self.hits
)
.ok();
writeln!(
&mut w,
"zfs_arc_misses{{host=\"{}\"}} {}",
hostname, self.misses
)
.ok();
writeln!(
&mut w,
"zfs_arc_size{{host=\"{}\"}} {}",
hostname, self.size
)
.ok();
writeln!(
&mut w,
"zfs_arc_prefetch{{host=\"{}\"}} {}",
hostname, self.prefetch
)
.ok();
}
}
pub fn arcstats() -> Result<Option<ArcStats>> {
let content = match read_to_string("/proc/spl/kstat/zfs/arcstats") {
Ok(c) => c,
Err(_) => return Ok(None),
};
let mut stats = ArcStats::default();
for line in content.lines().skip(2) {
let mut parts = line.split_ascii_whitespace();
if let (Some(name), _, Some(Ok(value))) =
(parts.next(), parts.next(), parts.next().map(u64::from_str))
{
match name {
"demand_data_hits" => stats.hits += value,
"demand_metadata_hits" => stats.hits += value,
"prefetch_data_hits" => {
stats.hits += value;
stats.prefetch += value;
}
"prefetch_metadata_hits" => {
stats.hits += value;
stats.prefetch += value;
}
"demand_data_misses" => stats.misses += value,
"demand_metadata_misses" => stats.misses += value,
"prefetch_data_misses" => {
stats.misses += value;
stats.prefetch += value;
}
"prefetch_metadata_misses" => {
stats.misses += value;
stats.prefetch += value;
}
"size" => stats.size = value,
_ => {}
}
}
}
Ok(Some(stats))
}

88
src/linux/gpu/mod.rs Normal file
View file

@ -0,0 +1,88 @@
use crate::data::{GpuMemory, GpuUsage};
use crate::linux::hwmon::FileSource;
use std::fs::read_to_string;
use std::str::FromStr;
use std::sync::atomic::{AtomicU64, Ordering};
use std::sync::Mutex;
use std::thread::sleep;
use std::time::{Duration, Instant};
pub mod nvidia;
fn read_num<T: FromStr>(path: &str) -> Option<T> {
read_to_string(path).ok()?.trim().parse().ok()
}
pub fn memory() -> Option<GpuMemory> {
if let Some(nv_mem) = nvidia::memory() {
return Some(nv_mem);
}
// 1 gpu should be enough for everyone
let used = read_num::<u64>("/sys/class/drm/card0/device/mem_info_vram_used")?;
let total = read_num("/sys/class/drm/card0/device/mem_info_vram_total")?;
Some(GpuMemory {
total,
free: total - used,
})
}
pub fn utilization() -> impl Iterator<Item = GpuUsage> {
let nv_usage = nvidia::utilization();
let sources = [
(
"memory",
read_num("/sys/class/drm/card0/device/mem_busy_percent"),
),
(
"compute",
read_num("/sys/class/drm/card0/device/gpu_busy_percent"),
),
];
let drm = sources.into_iter().flat_map(|(system, usage)| {
Some(GpuUsage {
system,
usage: usage?,
})
});
drm.chain(nv_usage)
}
static GPU_POWER_UJ: AtomicU64 = AtomicU64::new(0);
static GPU_POWER_LAST_READ: Mutex<Option<Instant>> = Mutex::new(None);
fn get_gpu_power_elapsed() -> Option<Duration> {
let mut last_read = GPU_POWER_LAST_READ.lock().unwrap();
let now = Instant::now();
let elapsed = last_read.as_ref().map(|last_read| now - *last_read);
*last_read = Some(now);
elapsed
}
pub fn update_gpu_power() {
if let Ok(mut file) =
FileSource::open("/sys/class/drm/card0/device/hwmon/hwmon0/power1_average")
{
loop {
if let Some(elapsed) = get_gpu_power_elapsed() {
let current_power: u64 = match file.read() {
Ok(current_power) => current_power,
Err(_) => {
return;
}
};
let elapsed_milli = elapsed.as_millis() as u64;
let power = current_power * elapsed_milli / 1000;
GPU_POWER_UJ.fetch_add(power, Ordering::SeqCst);
}
sleep(Duration::from_millis(500));
}
}
}
pub fn gpu_power() -> u64 {
GPU_POWER_UJ.load(Ordering::SeqCst)
}

56
src/linux/gpu/nvidia.rs Normal file
View file

@ -0,0 +1,56 @@
use crate::data::{GpuMemory, GpuUsage};
use nvml_wrapper::enum_wrappers::device::TemperatureSensor;
use nvml_wrapper::{Device, Nvml};
use once_cell::sync::Lazy;
static NVIDIA: Lazy<Option<Nvml>> = Lazy::new(|| Nvml::init().ok());
fn device() -> Option<Device<'static>> {
NVIDIA.as_ref()?.device_by_index(0).ok()
}
pub fn temperature() -> Option<f32> {
let temp = device()?.temperature(TemperatureSensor::Gpu).ok()?;
Some(temp as f32)
}
pub fn power() -> Option<u64> {
device()?
.total_energy_consumption()
.ok()
.map(|mj| mj * 1_000)
}
pub fn memory() -> Option<GpuMemory> {
let mem = device()?.memory_info().ok()?;
Some(GpuMemory {
total: mem.total,
free: mem.free,
})
}
pub fn utilization() -> impl Iterator<Item = GpuUsage> {
let sources = if let Some(device) = device() {
let utilization = device.utilization_rates().ok();
[
("compute", utilization.as_ref().map(|u| u.gpu)),
("memory", utilization.as_ref().map(|u| u.gpu)),
(
"encode",
device.encoder_utilization().ok().map(|u| u.utilization),
),
(
"decode",
device.decoder_utilization().ok().map(|u| u.utilization),
),
]
} else {
[("", None); 4]
};
sources.into_iter().flat_map(|(system, usage)| {
Some(GpuUsage {
system,
usage: usage?,
})
})
}

122
src/linux/hwmon.rs Normal file
View file

@ -0,0 +1,122 @@
use std::fs::{read_dir, read_to_string, File};
use std::io;
use std::io::{ErrorKind, Read, Seek};
use std::path::{Path, PathBuf};
use std::str::FromStr;
fn read_to_string_trimmed(path: &Path) -> io::Result<String> {
let mut s = read_to_string(path)?;
let len = s.trim().len();
s.truncate(len);
Ok(s)
}
pub struct FileSource {
buff: String,
file: File,
}
impl FileSource {
pub fn open<P: AsRef<Path>>(path: P) -> io::Result<FileSource> {
Ok(FileSource {
buff: String::with_capacity(32),
file: File::open(path)?,
})
}
pub fn read<T>(&mut self) -> io::Result<T>
where
T: FromStr,
<T as FromStr>::Err: std::error::Error + Send + Sync + 'static,
{
self.buff.clear();
self.file.rewind()?;
self.file.read_to_string(&mut self.buff)?;
self.buff
.trim()
.parse()
.map_err(|e| io::Error::new(ErrorKind::InvalidData, e))
}
}
pub struct Device {
base_path: PathBuf,
name: String,
}
impl Device {
pub fn new(path: PathBuf) -> io::Result<Device> {
let name = read_to_string_trimmed(&path.join("name"))?;
Ok(Device {
base_path: path,
name,
})
}
pub fn list() -> impl Iterator<Item = io::Result<Device>> {
let sensors = read_dir("/sys/class/hwmon").into_iter().flatten();
sensors.map(|device| device.and_then(|device| Device::new(device.path())))
}
pub fn name(&self) -> &str {
&self.name
}
pub fn sensors(&self) -> impl Iterator<Item = io::Result<Sensor>> {
// determine early to avoid borrowing &self in iterator
let is_cpu_thermal = self.name == "cpu_thermal";
let sensors = read_dir(&self.base_path).into_iter().flatten();
sensors
.filter_map(|sensor| {
let sensor = match sensor {
Ok(sensor) => sensor,
Err(e) => return Some(Err(e)),
};
if sensor.file_name().to_str()?.ends_with("_input") {
Some(Ok(sensor.path()))
} else {
None
}
})
.map(move |path| {
let path = path?;
let input_name = path.file_name().unwrap().to_str().unwrap();
// rpi cpu_thermal doesn't have labels, so we hardcode one
if is_cpu_thermal && input_name == "temp1_input" {
return Ok(Sensor {
input_path: path,
name: "Tdie".into(),
});
}
let base_name = input_name.trim_end_matches("_input");
let label_name = path.with_file_name(format!("{base_name}_label"));
let name = read_to_string_trimmed(&label_name)?;
Ok(Sensor {
input_path: path,
name,
})
})
}
}
pub struct Sensor {
input_path: PathBuf,
name: String,
}
impl Sensor {
pub fn name(&self) -> &str {
&self.name
}
pub fn reader(&self) -> io::Result<FileSource> {
FileSource::open(&self.input_path)
}
}

123
src/linux/mod.rs Normal file
View file

@ -0,0 +1,123 @@
pub mod disk;
pub mod gpu;
pub mod hwmon;
pub mod power;
pub mod sensors;
use self::disk::zfs::pools;
use self::disk::*;
use self::sensors::*;
use crate::linux::disk::zfs::arcstats;
use crate::linux::gpu::{update_gpu_power, utilization};
use crate::linux::power::power_usage;
use crate::{hostname, Error, MultiSensorSource, Result, SensorData, SensorSource};
use std::fmt::Write;
use std::sync::Mutex;
use sysconf::SysconfError;
impl From<SysconfError> for Error {
fn from(_: SysconfError) -> Self {
Error::Other("Unsupported sysconf".into())
}
}
pub struct Sensors {
pub hostname: String,
cpu: Mutex<CpuTimeSource>,
temp: Mutex<TemperatureSource>,
net: Mutex<NetworkSource>,
mem: Mutex<MemorySource>,
disk_stats: Mutex<DiskStatSource>,
disk_usage: Mutex<DiskUsageSource>,
}
impl Sensors {
pub fn new() -> Result<Sensors> {
std::thread::spawn(update_gpu_power);
Ok(Sensors {
hostname: hostname()?,
cpu: Mutex::new(CpuTimeSource::new()?),
temp: Mutex::new(TemperatureSource::new()?),
net: Mutex::new(NetworkSource::new()?),
mem: Mutex::new(MemorySource::new()?),
disk_stats: Mutex::new(DiskStatSource::new()?),
disk_usage: Mutex::new(DiskUsageSource::new()?),
})
}
}
pub fn get_metrics(sensors: &Sensors) -> Result<String> {
let hostname = &sensors.hostname;
let mut disk_source = sensors.disk_stats.lock().unwrap();
let mut disk_usage_source = sensors.disk_usage.lock().unwrap();
let disks = disk_source.read()?;
let disk_usage = disk_usage_source.read()?;
let cpu = sensors.cpu.lock().unwrap().read()?;
let memory = sensors.mem.lock().unwrap().read()?;
let temperatures = sensors.temp.lock().unwrap().read()?;
let mut net = sensors.net.lock().unwrap();
let networks = net.read()?;
let pools = pools();
let mut result = String::with_capacity(256);
cpu.write(&mut result, &hostname);
memory.write(&mut result, &hostname);
for pool in pools {
writeln!(
&mut result,
"zfs_pool_size{{host=\"{}\", pool=\"{}\"}} {}",
hostname, pool.name, pool.size
)
.ok();
writeln!(
&mut result,
"zfs_pool_free{{host=\"{}\", pool=\"{}\"}} {}",
hostname, pool.name, pool.free
)
.ok();
}
for network in networks {
if let Ok(network) = network {
network.write(&mut result, &hostname);
}
}
for disk in disks {
if let Ok(disk) = disk {
disk.write(&mut result, hostname);
}
}
for disk in disk_usage {
if let Ok(disk) = disk {
disk.write(&mut result, hostname);
}
}
for (label, temp) in temperatures {
if temp != 0.0 {
writeln!(
&mut result,
"temperature{{host=\"{}\", sensor=\"{}\"}} {:.1}",
hostname, label, temp
)
.ok();
}
}
if let Some(power) = power_usage()? {
power.write(&mut result, &sensors.hostname);
}
if let Some(arc) = arcstats()? {
arc.write(&mut result, &sensors.hostname);
}
if let Some(memory) = gpu::memory() {
memory.write(&mut result, &sensors.hostname)
}
for usage in utilization() {
usage.write(&mut result, &sensors.hostname);
}
Ok(result)
}

94
src/linux/power.rs Normal file
View file

@ -0,0 +1,94 @@
use crate::linux::gpu::gpu_power;
use crate::{Error, Result};
use std::fmt::Write;
use std::fs::{read_dir, read_to_string};
use std::sync::atomic::{AtomicBool, Ordering};
use tracing::warn;
static CAN_READ: AtomicBool = AtomicBool::new(true);
#[derive(Debug, Default)]
pub struct PowerUsage {
cpu_uj: u64,
cpu_packages_uj: Vec<u64>,
gpu_uj: u64,
}
impl PowerUsage {
pub fn write<W: Write>(&self, mut w: W, hostname: &str) {
writeln!(
&mut w,
r#"total_power{{host="{}", device="cpu"}} {:.3}"#,
hostname,
self.cpu_uj as f64 / 1_000_000.0
)
.ok();
for (i, package) in self.cpu_packages_uj.iter().enumerate() {
writeln!(
&mut w,
r#"package_power{{host="{}", package="{}", device="cpu"}} {:.3}"#,
hostname,
i,
*package as f64 / 1_000_000.0
)
.ok();
}
if self.gpu_uj > 0 {
writeln!(
&mut w,
r#"total_power{{host="{}", device="gpu"}} {:.3}"#,
hostname,
self.gpu_uj as f64 / 1_000_000.0
)
.ok();
}
}
}
pub fn power_usage() -> Result<Option<PowerUsage>> {
if !CAN_READ.load(Ordering::Relaxed) {
return Ok(None);
}
let dir = match read_dir("/sys/devices/virtual/powercap/intel-rapl") {
Ok(dir) => dir,
Err(_) => {
CAN_READ.store(false, Ordering::Relaxed);
return Ok(None);
}
};
let mut usage = PowerUsage::default();
for package in dir {
let package = package?;
if package
.file_name()
.to_str()
.ok_or_else(|| Error::Other("Invalid name".into()))?
.starts_with("intel-rapl")
{
let mut package_path = package.path();
package_path.push("energy_uj");
let package_usage = match read_to_string(&package_path) {
Err(e) if e.raw_os_error() == Some(13) => {
CAN_READ.store(false, Ordering::Relaxed);
warn!(
package_path = display(package_path.display()),
"can\'t read power usage"
);
return Ok(None);
}
result => result,
}?;
let package_usage = package_usage.trim().parse::<u64>()?;
usage.cpu_uj += package_usage;
usage.cpu_packages_uj.push(package_usage);
}
}
usage.gpu_uj = gpu_power();
if let Some(nvidia_power) = crate::linux::gpu::nvidia::power() {
usage.gpu_uj = nvidia_power;
}
Ok(Some(usage))
}

244
src/linux/sensors.rs Normal file
View file

@ -0,0 +1,244 @@
use crate::data::{CpuTime, Memory, NetStats, Temperatures};
use crate::linux::hwmon::{Device, FileSource};
use crate::{Error, MultiSensorSource, Result, SensorSource};
use std::fs::File;
use std::io;
use std::io::{BufRead, BufReader, ErrorKind, Read, Seek};
use sysconf::{sysconf, SysconfVariable};
pub struct TemperatureSource {
cpu_sensors: Vec<FileSource>,
gpu_sensors: Vec<FileSource>,
}
impl TemperatureSource {
pub fn new() -> Result<TemperatureSource> {
let mut cpu_sensors = Vec::new();
let mut gpu_sensors = Vec::new();
for device in Device::list().flatten() {
if device.name() == "k10temp" || device.name() == "coretemp" {
for sensor in device.sensors().flatten() {
if sensor.name() == "Tdie" || sensor.name().starts_with("Core ") {
cpu_sensors.push(sensor.reader()?);
}
}
}
if device.name() == "amdgpu" {
for sensor in device.sensors().flatten() {
if sensor.name() == "edge" {
gpu_sensors.push(sensor.reader()?);
}
}
}
}
Ok(TemperatureSource {
cpu_sensors,
gpu_sensors,
})
}
}
fn average_sensors(sensors: &mut [FileSource]) -> f32 {
if sensors.is_empty() {
return 0.0;
}
let mut total = 0.0;
let mut count = 0.0;
for sensor in sensors.iter_mut() {
if let Ok(value) = sensor.read::<f32>() {
total += value;
count += 1.0
}
}
total / count
}
impl SensorSource for TemperatureSource {
type Data = Temperatures;
fn read(&mut self) -> Result<Self::Data> {
let mut result = Temperatures {
cpu: average_sensors(&mut self.cpu_sensors) / 1000.0,
gpu: average_sensors(&mut self.gpu_sensors) / 1000.0,
};
if let Some(gpu) = super::gpu::nvidia::temperature() {
result.gpu = gpu;
}
Ok(result)
}
}
pub struct MemorySource {
source: File,
buff: String,
}
impl MemorySource {
pub fn new() -> Result<MemorySource> {
Ok(MemorySource {
source: File::open("/proc/meminfo")?,
buff: String::new(),
})
}
}
impl SensorSource for MemorySource {
type Data = Memory;
fn read(&mut self) -> Result<Self::Data> {
self.buff.clear();
self.source.rewind()?;
self.source.read_to_string(&mut self.buff)?;
let mut mem = Memory::default();
for line in self.buff.lines() {
if let Some(line) = line.strip_suffix(" kB") {
if let Some(line_total) = line.strip_prefix("MemTotal: ") {
mem.total = line_total.trim().parse::<u64>()? * 1000;
}
if let Some(line_free) = line.strip_prefix("MemFree: ") {
mem.free = line_free.trim().parse::<u64>()? * 1000;
}
if let Some(line_available) = line.strip_prefix("MemAvailable: ") {
mem.available = line_available.trim().parse::<u64>()? * 1000;
}
}
}
Ok(mem)
}
}
pub struct CpuTimeSource {
source: BufReader<File>,
buff: Vec<u8>,
cpu_count: f32,
}
impl CpuTimeSource {
pub fn new() -> Result<CpuTimeSource> {
Ok(CpuTimeSource {
source: BufReader::new(File::open("/proc/stat")?),
buff: Vec::new(),
cpu_count: sysconf(SysconfVariable::ScNprocessorsOnln)? as f32,
})
}
}
impl SensorSource for CpuTimeSource {
type Data = CpuTime;
fn read(&mut self) -> Result<Self::Data> {
self.buff.clear();
self.source.rewind()?;
self.source.read_until(b'\n', &mut self.buff)?;
let line = std::str::from_utf8(&self.buff)?;
let mut parts = line.split_ascii_whitespace();
if let (_cpu, Some(user), _nice, Some(system)) =
(parts.next(), parts.next(), parts.next(), parts.next())
{
let user: f32 = user.parse()?;
let system: f32 = system.parse()?;
let clock_ticks = sysconf(SysconfVariable::ScClkTck)?;
Ok(CpuTime(
(user + system) / (clock_ticks as f32) / self.cpu_count,
))
} else {
Err(io::Error::from(ErrorKind::InvalidData).into())
}
}
}
pub struct NetworkSource {
source: File,
buff: String,
}
impl NetworkSource {
pub fn new() -> Result<NetworkSource> {
Ok(NetworkSource {
source: File::open("/proc/net/dev")?,
buff: String::new(),
})
}
fn parse_line(line: &str) -> Result<NetStats> {
let mut parts = line.trim_start().split_ascii_whitespace();
if let (
Some(interface),
Some(bytes_received),
_packets,
_err,
_drop,
_fifo,
_frame,
_compressed,
_multicast,
Some(bytes_sent),
) = (
parts.next(),
parts.next(),
parts.next(),
parts.next(),
parts.next(),
parts.next(),
parts.next(),
parts.next(),
parts.next(),
parts.next(),
) {
Ok(NetStats {
interface: interface.trim_end_matches(':').into(),
bytes_sent: bytes_sent.parse()?,
bytes_received: bytes_received.parse()?,
})
} else {
Err(Error::Io(ErrorKind::InvalidData.into()))
}
}
}
impl MultiSensorSource for NetworkSource {
type Data = NetStats;
type Iter<'a> = NetworkStatParser<'a>;
fn read(&mut self) -> Result<Self::Iter<'_>> {
self.buff.clear();
self.source.rewind()?;
self.source.read_to_string(&mut self.buff)?;
Ok(NetworkStatParser {
lines: self.buff.lines(),
})
}
}
pub struct NetworkStatParser<'a> {
lines: std::str::Lines<'a>,
}
impl<'a> Iterator for NetworkStatParser<'a> {
type Item = Result<NetStats>;
fn next(&mut self) -> Option<Self::Item> {
let line = loop {
let line = self.lines.next()?;
let trimmed = line.trim_start();
if trimmed.starts_with("en") || trimmed.starts_with("eth") || trimmed.starts_with("wlp")
{
break trimmed;
}
};
Some(NetworkSource::parse_line(line))
}
}