use async_trait::async_trait; use cm_dashboard_shared::{Metric, MetricValue, Status, StatusTracker}; use std::process::Command; use tracing::debug; use super::{Collector, CollectorError}; use crate::config::NixOSConfig; /// NixOS system information collector /// /// Collects NixOS-specific system information including: /// - NixOS version and build information /// - Currently active/logged in users pub struct NixOSCollector { config: NixOSConfig, } impl NixOSCollector { pub fn new(config: NixOSConfig) -> Self { Self { config } } /// Get NixOS build information (short hash and timestamp) fn get_nixos_build_info(&self) -> Result<(String, String), Box> { // Try nixos-version command first if let Ok(output) = Command::new("nixos-version").output() { if output.status.success() { let version_line = String::from_utf8_lossy(&output.stdout); let version = version_line.trim(); // Parse format: "24.05.20241023.abcdef (Vicuna)" if let Some(parts) = version.split('.').collect::>().get(2) { if parts.len() >= 14 { // 8 digits + 6 hex chars minimum let date_part = &parts[..8]; // YYYYMMDD let hash_part = &parts[8..]; // remaining is hash // Extract short hash (first 6 characters) let short_hash = if hash_part.len() >= 6 { &hash_part[..6] } else { hash_part }; // Format date from YYYYMMDD to dd/mm/yy if date_part.len() == 8 { let year = &date_part[2..4]; // YY let month = &date_part[4..6]; // MM let day = &date_part[6..8]; // DD let formatted_date = format!("{}/{}/{}", day, month, year); return Ok((short_hash.to_string(), formatted_date)); } } } } } Err("Could not parse NixOS build information".into()) } /// Get currently active users fn get_active_users(&self) -> Result, Box> { let output = Command::new("who").output()?; if !output.status.success() { return Err("who command failed".into()); } let who_output = String::from_utf8_lossy(&output.stdout); let mut users = std::collections::HashSet::new(); for line in who_output.lines() { if let Some(username) = line.split_whitespace().next() { if !username.is_empty() { users.insert(username.to_string()); } } } Ok(users.into_iter().collect()) } } #[async_trait] impl Collector for NixOSCollector { fn name(&self) -> &str { "nixos" } async fn collect(&self, _status_tracker: &mut StatusTracker) -> Result, CollectorError> { debug!("Collecting NixOS system information"); let mut metrics = Vec::new(); let timestamp = chrono::Utc::now().timestamp() as u64; // Collect NixOS build information match self.get_nixos_build_info() { Ok((short_hash, formatted_date)) => { // Create combined build string: "hash dd/mm/yy H:M:S" // For now, use current time for H:M:S (could be enhanced to get actual build time) let now = chrono::Local::now(); let build_string = format!("{} {} {}", short_hash, formatted_date, now.format("%H:%M:%S") ); metrics.push(Metric { name: "system_nixos_build".to_string(), value: MetricValue::String(build_string), unit: None, description: Some("NixOS build information".to_string()), status: Status::Ok, timestamp, }); } Err(e) => { debug!("Failed to get NixOS build info: {}", e); metrics.push(Metric { name: "system_nixos_build".to_string(), value: MetricValue::String("unknown".to_string()), unit: None, description: Some("NixOS build (failed to detect)".to_string()), status: Status::Unknown, timestamp, }); } } // Collect active users match self.get_active_users() { Ok(users) => { let users_str = users.join(", "); metrics.push(Metric { name: "system_active_users".to_string(), value: MetricValue::String(users_str), unit: None, description: Some("Currently active users".to_string()), status: Status::Ok, timestamp, }); } Err(e) => { debug!("Failed to get active users: {}", e); metrics.push(Metric { name: "system_active_users".to_string(), value: MetricValue::String("unknown".to_string()), unit: None, description: Some("Active users (failed to detect)".to_string()), status: Status::Unknown, timestamp, }); } } debug!("Collected {} NixOS metrics", metrics.len()); Ok(metrics) } }