All checks were successful
Build and Release / build-and-release (push) Successful in 1m31s
- Remove all transitional icon infrastructure (CommandType, pending transitions) - Clean up ZMQ command system remnants after SSH migration - Add real-time log streaming for service start operations - Show final logs and status for service stop operations - Fix compilation warnings by removing unused methods - Simplify UI architecture with pure SSH-based service control
165 lines
5.5 KiB
Rust
165 lines
5.5 KiB
Rust
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<Arc<Mutex<UserStoppedServiceTracker>>> = 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<String>,
|
|
/// Path to persistent storage file
|
|
storage_path: String,
|
|
}
|
|
|
|
/// Serializable data structure for persistence
|
|
#[derive(Debug, Serialize, Deserialize)]
|
|
struct UserStoppedData {
|
|
services: Vec<String>,
|
|
}
|
|
|
|
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<Self> {
|
|
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<P: AsRef<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(())
|
|
}
|
|
}
|
|
|