use crate::error::NodeInfoError;
use crate::model::{BatteryInfo, CpuInfo, GpuInfo, HostInfo, MemoryInfo, NodeSysInfo, OsInfo};
use sysinfo::System;
#[allow(clippy::integer_division)]
fn calculate_percent(used: u64, total: u64) -> u32 {
if total == 0 {
return 0;
}
let percent = (u128::from(used) * 100 / u128::from(total)).min(100);
u32::try_from(percent).unwrap_or(0)
}
pub struct SysInfoCollector {
system: std::sync::Mutex<System>,
}
impl SysInfoCollector {
pub fn new() -> Self {
let system = System::new_all();
Self {
system: std::sync::Mutex::new(system),
}
}
pub fn collect(&self, node_id: uuid::Uuid) -> Result<NodeSysInfo, NodeInfoError> {
let mut sys = self
.system
.lock()
.map_err(|e| NodeInfoError::SysInfoCollectionFailed(e.to_string()))?;
sys.refresh_cpu_all();
sys.refresh_memory();
let os_info = Self::collect_os_info();
let cpu_info = Self::collect_cpu_info(&sys);
let memory_info = Self::collect_memory_info(&sys);
let host_info = Self::collect_host_info();
let gpus = Self::collect_gpu_info();
let battery = Self::collect_battery_info();
Ok(NodeSysInfo {
node_id,
os: os_info,
cpu: cpu_info,
memory: memory_info,
host: host_info,
gpus,
battery,
collected_at: chrono::Utc::now(),
})
}
fn collect_os_info() -> OsInfo {
let name = System::name().unwrap_or_else(|| std::env::consts::OS.to_owned());
let version = System::os_version().unwrap_or_else(|| "unknown".to_owned());
let arch = std::env::consts::ARCH.to_owned();
OsInfo {
name,
version,
arch,
}
}
#[allow(clippy::cast_precision_loss)]
fn collect_cpu_info(sys: &System) -> CpuInfo {
let cpus = sys.cpus();
let num_cpus = u32::try_from(cpus.len()).unwrap_or(u32::MAX);
let model = if let Some(cpu) = cpus.first() {
cpu.brand().to_owned()
} else {
"Unknown".to_owned()
};
let cores =
u32::try_from(System::physical_core_count().unwrap_or(cpus.len())).unwrap_or(u32::MAX);
let frequency_mhz = if cpus.is_empty() {
0.0
} else {
cpus.iter().map(|cpu| cpu.frequency() as f64).sum::<f64>() / cpus.len() as f64
};
CpuInfo {
model,
num_cpus,
cores,
frequency_mhz,
}
}
fn collect_memory_info(sys: &System) -> MemoryInfo {
let total_bytes = sys.total_memory();
let available_bytes = sys.available_memory();
let used_bytes = sys.used_memory();
let used_percent = calculate_percent(used_bytes, total_bytes);
MemoryInfo {
total_bytes,
available_bytes,
used_bytes,
used_percent,
}
}
fn collect_host_info() -> HostInfo {
let hostname = hostname::get().map_or_else(
|_| "unknown".to_owned(),
|h| h.to_string_lossy().to_string(),
);
let uptime_seconds = System::uptime();
let mut ip_addresses = Vec::new();
if let Ok(primary_ip) = local_ip_address::local_ip() {
ip_addresses.push(primary_ip.to_string());
}
if let Ok(all_ips) = local_ip_address::list_afinet_netifas() {
for (_name, ip) in all_ips {
let ip_str = ip.to_string();
if !ip_addresses.contains(&ip_str) && !ip.is_loopback() {
ip_addresses.push(ip_str);
}
}
}
HostInfo {
hostname,
uptime_seconds,
ip_addresses,
}
}
fn collect_gpu_info() -> Vec<GpuInfo> {
#[cfg(target_os = "macos")]
{
super::gpu_collector_macos::collect_gpu_info()
}
#[cfg(target_os = "linux")]
{
super::gpu_collector_linux::collect_gpu_info()
}
#[cfg(target_os = "windows")]
{
super::gpu_collector_windows::collect_gpu_info()
}
#[cfg(not(any(target_os = "macos", target_os = "linux", target_os = "windows")))]
{
Vec::new()
}
}
#[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
fn collect_battery_info() -> Option<BatteryInfo> {
use starship_battery::Manager;
let manager = Manager::new().ok()?;
let mut batteries = manager.batteries().ok()?;
if let Some(Ok(battery)) = batteries.next() {
use starship_battery::State;
let on_battery = matches!(battery.state(), State::Discharging);
let charge = f64::from(battery.state_of_charge().value) * 100.0;
let percentage = charge.clamp(0.0, 100.0) as u32;
Some(BatteryInfo {
on_battery,
percentage,
})
} else {
None
}
}
}
impl Default for SysInfoCollector {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::calculate_percent;
#[test]
fn test_calculate_percent_zero_total() {
assert_eq!(calculate_percent(0, 0), 0);
assert_eq!(calculate_percent(100, 0), 0);
}
#[test]
fn test_calculate_percent_normal() {
assert_eq!(calculate_percent(50, 100), 50);
assert_eq!(calculate_percent(1, 4), 25);
assert_eq!(calculate_percent(3, 4), 75);
assert_eq!(calculate_percent(0, 100), 0);
}
#[test]
fn test_calculate_percent_full() {
assert_eq!(calculate_percent(100, 100), 100);
assert_eq!(calculate_percent(200, 100), 100);
}
#[test]
fn test_calculate_percent_overflow_repro() {
assert_eq!(calculate_percent(u64::MAX, 1), 100);
assert_eq!(calculate_percent(u64::MAX, u64::MAX), 100);
let petabyte: u64 = 1_000_000_000_000_000;
assert_eq!(calculate_percent(petabyte, 2 * petabyte), 50);
}
}