use crate::error::NodeInfoError;
use crate::model::{NodeSysCap, NodeSysInfo, SysCap};
use crate::sysinfo_collector::SysInfoCollector;
use std::sync::Arc;
const BYTES_PER_GB: f64 = 1024.0 * 1024.0 * 1024.0;
#[allow(clippy::cast_precision_loss)]
fn bytes_to_gb(bytes: u64) -> f64 {
bytes as f64 / BYTES_PER_GB
}
struct SysCapBuilder {
key: String,
category: String,
name: String,
display_name: String,
present: bool,
version: Option<String>,
amount: Option<f64>,
amount_dimension: Option<String>,
details: Option<String>,
cache_ttl_secs: u64,
}
impl SysCapBuilder {
fn new(key: String, category: String, name: String, display_name: String) -> Self {
Self {
key,
category,
name,
display_name,
present: true,
version: None,
amount: None,
amount_dimension: None,
details: None,
cache_ttl_secs: 300, }
}
fn version(mut self, version: Option<String>) -> Self {
self.version = version;
self
}
fn amount(mut self, amount: Option<f64>) -> Self {
self.amount = amount;
self
}
fn amount_dimension(mut self, amount_dimension: Option<String>) -> Self {
self.amount_dimension = amount_dimension;
self
}
fn details(mut self, details: Option<String>) -> Self {
self.details = details;
self
}
fn cache_ttl_secs(mut self, cache_ttl_secs: u64) -> Self {
self.cache_ttl_secs = cache_ttl_secs;
self
}
fn build(self) -> SysCap {
SysCap {
key: self.key,
category: self.category,
name: self.name,
display_name: self.display_name,
present: self.present,
version: self.version,
amount: self.amount,
amount_dimension: self.amount_dimension,
details: self.details,
cache_ttl_secs: self.cache_ttl_secs,
fetched_at_secs: chrono::Utc::now().timestamp(),
}
}
}
pub struct SysCapCollector {
sysinfo_collector: Arc<SysInfoCollector>,
}
impl SysCapCollector {
pub fn new(sysinfo_collector: Arc<SysInfoCollector>) -> Self {
Self { sysinfo_collector }
}
pub fn collect(&self, node_id: uuid::Uuid) -> Result<NodeSysCap, NodeInfoError> {
let sysinfo = self.sysinfo_collector.collect(node_id)?;
let mut capabilities = Vec::new();
capabilities.extend(Self::collect_hardware_caps(&sysinfo));
capabilities.extend(Self::collect_os_caps(&sysinfo));
capabilities.extend(Self::collect_gpu_caps(&sysinfo));
capabilities.extend(Self::collect_battery_caps(&sysinfo));
capabilities.extend(Self::collect_software_caps());
Ok(NodeSysCap {
node_id,
capabilities,
collected_at: chrono::Utc::now(),
})
}
fn collect_hardware_caps(sysinfo: &NodeSysInfo) -> Vec<SysCap> {
let mut caps = Vec::new();
let arch = &sysinfo.os.arch;
caps.push(
SysCapBuilder::new(
format!("hardware:{arch}"),
"hardware".to_owned(),
arch.clone(),
arch.to_uppercase(),
)
.details(Some(format!("{arch} architecture detected")))
.cache_ttl_secs(3600) .build(),
);
let total_gb = bytes_to_gb(sysinfo.memory.total_bytes);
caps.push(
SysCapBuilder::new(
"hardware:ram".to_owned(),
"hardware".to_owned(),
"ram".to_owned(),
"RAM".to_owned(),
)
.amount(Some(total_gb))
.amount_dimension(Some("GB".to_owned()))
.details(Some(format!(
"Total: {:.2} GB, Used: {}%",
total_gb, sysinfo.memory.used_percent
)))
.cache_ttl_secs(5) .build(),
);
caps.push(
SysCapBuilder::new(
"hardware:cpu".to_owned(),
"hardware".to_owned(),
"cpu".to_owned(),
"CPU".to_owned(),
)
.version(Some(sysinfo.cpu.model.clone()))
.amount(Some(f64::from(sysinfo.cpu.cores)))
.amount_dimension(Some("cores".to_owned()))
.details(Some(format!(
"{} with {} cores @ {:.0} MHz",
sysinfo.cpu.model, sysinfo.cpu.cores, sysinfo.cpu.frequency_mhz
)))
.cache_ttl_secs(600) .build(),
);
caps
}
fn collect_os_caps(sysinfo: &NodeSysInfo) -> Vec<SysCap> {
let mut caps = Vec::new();
let os = std::env::consts::OS;
caps.push(
SysCapBuilder::new(
format!("os:{os}"),
"os".to_owned(),
os.to_owned(),
match os {
"macos" => "macOS",
"linux" => "Linux",
"windows" => "Windows",
_ => os,
}
.to_owned(),
)
.version(Some(sysinfo.os.version.clone()))
.details(Some(format!(
"Platform: {}, Version: {}, Arch: {}",
sysinfo.os.name, sysinfo.os.version, sysinfo.os.arch
)))
.cache_ttl_secs(120) .build(),
);
caps
}
fn collect_gpu_caps(sysinfo: &NodeSysInfo) -> Vec<SysCap> {
let mut caps = Vec::new();
for (i, gpu) in sysinfo.gpus.iter().enumerate() {
let gpu_key = if i == 0 {
"hardware:gpu".to_owned()
} else {
format!("hardware:gpu{i}")
};
let mut details = format!("Model: {}", gpu.model);
if let Some(vram) = gpu.total_memory_mb {
use std::fmt::Write;
_ = write!(details, ", VRAM: {vram:.0} MB");
}
if let Some(cores) = gpu.cores {
use std::fmt::Write;
_ = write!(details, ", Cores: {cores}");
}
caps.push(
SysCapBuilder::new(
gpu_key,
"hardware".to_owned(),
format!("gpu{}", if i == 0 { String::new() } else { i.to_string() }),
"GPU".to_owned(),
)
.version(Some(gpu.model.clone()))
.amount(gpu.total_memory_mb)
.amount_dimension(if gpu.total_memory_mb.is_some() {
Some("MB".to_owned())
} else {
None
})
.details(Some(details))
.cache_ttl_secs(10) .build(),
);
}
caps
}
fn collect_battery_caps(sysinfo: &NodeSysInfo) -> Vec<SysCap> {
let mut caps = Vec::new();
if let Some(battery) = &sysinfo.battery {
let status = if battery.on_battery {
"discharging (on battery power)"
} else {
"charging"
};
caps.push(
SysCapBuilder::new(
"hardware:battery".to_owned(),
"hardware".to_owned(),
"battery".to_owned(),
"Battery".to_owned(),
)
.amount(Some(f64::from(battery.percentage)))
.amount_dimension(Some("percent".to_owned()))
.details(Some(format!(
"Status: {}, Level: {}%",
status, battery.percentage
)))
.cache_ttl_secs(3) .build(),
);
}
caps
}
fn collect_software_caps() -> Vec<SysCap> {
Vec::new()
}
}
impl Default for SysCapCollector {
fn default() -> Self {
Self::new(Arc::new(SysInfoCollector::new()))
}
}