use anyhow::Result; use serde::{Deserialize, Serialize}; use std::collections::HashSet; use std::fs; use std::path::Path; use std::sync::{Arc, Mutex, OnceLock}; use tracing::{debug, info, warn}; /// Shared instance for global access static GLOBAL_TRACKER: OnceLock>> = OnceLock::new(); /// Tracks services that have been stopped by user action /// These services should be treated as OK status instead of Warning #[derive(Debug)] pub struct UserStoppedServiceTracker { /// Set of services stopped by user action user_stopped_services: HashSet, /// Path to persistent storage file storage_path: String, } /// Serializable data structure for persistence #[derive(Debug, Serialize, Deserialize)] struct UserStoppedData { services: Vec, } impl UserStoppedServiceTracker { /// Create new tracker with default storage path pub fn new() -> Self { Self::with_storage_path("/var/lib/cm-dashboard/user-stopped-services.json") } /// Initialize global instance (called by agent) pub fn init_global() -> Result { let tracker = Self::new(); // Set global instance let global_instance = Arc::new(Mutex::new(tracker)); if GLOBAL_TRACKER.set(global_instance).is_err() { warn!("Global service tracker was already initialized"); } // Return a new instance for the agent to use Ok(Self::new()) } /// Check if a service is user-stopped (global access for collectors) pub fn is_service_user_stopped(service_name: &str) -> bool { if let Some(global) = GLOBAL_TRACKER.get() { if let Ok(tracker) = global.lock() { tracker.is_user_stopped(service_name) } else { debug!("Failed to lock global service tracker"); false } } else { debug!("Global service tracker not initialized"); false } } /// Update global tracker (called by agent when tracker state changes) pub fn update_global(updated_tracker: &UserStoppedServiceTracker) { if let Some(global) = GLOBAL_TRACKER.get() { if let Ok(mut tracker) = global.lock() { tracker.user_stopped_services = updated_tracker.user_stopped_services.clone(); } else { debug!("Failed to lock global service tracker for update"); } } else { debug!("Global service tracker not initialized for update"); } } /// Create new tracker with custom storage path pub fn with_storage_path>(storage_path: P) -> Self { let storage_path = storage_path.as_ref().to_string_lossy().to_string(); let mut tracker = Self { user_stopped_services: HashSet::new(), storage_path, }; // Load existing data from storage if let Err(e) = tracker.load_from_storage() { warn!("Failed to load user-stopped services from storage: {}", e); info!("Starting with empty user-stopped services list"); } tracker } /// Clear user-stopped flag for a service (when user starts it) pub fn clear_user_stopped(&mut self, service_name: &str) -> Result<()> { if self.user_stopped_services.remove(service_name) { info!("Cleared user-stopped flag for service '{}'", service_name); self.save_to_storage()?; debug!("Service '{}' user-stopped flag cleared and saved to storage", service_name); } else { debug!("Service '{}' was not marked as user-stopped", service_name); } Ok(()) } /// Check if a service is marked as user-stopped pub fn is_user_stopped(&self, service_name: &str) -> bool { let is_stopped = self.user_stopped_services.contains(service_name); debug!("Service '{}' user-stopped status: {}", service_name, is_stopped); is_stopped } /// Save current state to persistent storage fn save_to_storage(&self) -> Result<()> { // Create parent directory if it doesn't exist if let Some(parent_dir) = Path::new(&self.storage_path).parent() { if !parent_dir.exists() { fs::create_dir_all(parent_dir)?; debug!("Created parent directory: {}", parent_dir.display()); } } let data = UserStoppedData { services: self.user_stopped_services.iter().cloned().collect(), }; let json_data = serde_json::to_string_pretty(&data)?; fs::write(&self.storage_path, json_data)?; debug!( "Saved {} user-stopped services to {}", data.services.len(), self.storage_path ); Ok(()) } /// Load state from persistent storage fn load_from_storage(&mut self) -> Result<()> { if !Path::new(&self.storage_path).exists() { debug!("Storage file {} does not exist, starting fresh", self.storage_path); return Ok(()); } let json_data = fs::read_to_string(&self.storage_path)?; let data: UserStoppedData = serde_json::from_str(&json_data)?; self.user_stopped_services = data.services.into_iter().collect(); info!( "Loaded {} user-stopped services from {}", self.user_stopped_services.len(), self.storage_path ); if !self.user_stopped_services.is_empty() { debug!("User-stopped services: {:?}", self.user_stopped_services); } Ok(()) } }