diff --git a/src/discovery.rs b/src/discovery.rs new file mode 100644 index 0000000..691e0ff --- /dev/null +++ b/src/discovery.rs @@ -0,0 +1,226 @@ +/// Módulo para descubrir servicios systemd existentes +use std::fs; +use std::path::Path; +use crate::logger::get_logger; +use crate::config::{get_config_manager, MonitoredApp}; + +const SYSTEMD_DIR: &str = "/etc/systemd/system"; +const SERVICE_PREFIX: &str = "siax-app-"; + +/// Descubre servicios systemd existentes con prefijo siax-app-* +pub fn discover_services() -> Vec { + let logger = get_logger(); + logger.info("Discovery", "Escaneando servicios systemd existentes..."); + + let mut services = Vec::new(); + + // Leer directorio de systemd + let entries = match fs::read_dir(SYSTEMD_DIR) { + Ok(entries) => entries, + Err(e) => { + logger.error("Discovery", "No se pudo leer directorio systemd", Some(&e.to_string())); + return services; + } + }; + + // Buscar archivos siax-app-*.service + for entry in entries.flatten() { + let path = entry.path(); + if let Some(filename) = path.file_name() { + let filename_str = filename.to_string_lossy(); + + // Verificar que sea un archivo .service con nuestro prefijo + if filename_str.starts_with(SERVICE_PREFIX) && filename_str.ends_with(".service") { + logger.info("Discovery", &format!("Encontrado: {}", filename_str)); + + // Extraer nombre de la app + let app_name = extract_app_name(&filename_str); + + // Leer configuración del servicio + if let Some(service) = parse_service_file(&path, &app_name) { + services.push(service); + } + } + } + } + + logger.info("Discovery", &format!("✅ Descubiertos {} servicios", services.len())); + services +} + +/// Extrae el nombre de la app desde el nombre del archivo +/// Ejemplo: "siax-app-IDEAS.service" -> "app_IDEAS" +fn extract_app_name(filename: &str) -> String { + // Remover "siax-app-" del inicio y ".service" del final + filename + .trim_start_matches(SERVICE_PREFIX) + .trim_end_matches(".service") + .to_string() +} + +/// Servicio descubierto en systemd +#[derive(Debug, Clone)] +pub struct DiscoveredService { + pub app_name: String, + pub service_file: String, + pub working_directory: Option, + pub user: Option, + pub exec_start: Option, + pub port: Option, +} + +/// Parsea un archivo .service para extraer configuración básica +fn parse_service_file(path: &Path, app_name: &str) -> Option { + let logger = get_logger(); + + let content = match fs::read_to_string(path) { + Ok(content) => content, + Err(e) => { + logger.error("Discovery", &format!("Error leyendo {}", path.display()), Some(&e.to_string())); + return None; + } + }; + + let mut service = DiscoveredService { + app_name: app_name.to_string(), + service_file: path.to_string_lossy().to_string(), + working_directory: None, + user: None, + exec_start: None, + port: None, + }; + + // Parsear líneas del archivo + for line in content.lines() { + let line = line.trim(); + + // WorkingDirectory + if line.starts_with("WorkingDirectory=") { + service.working_directory = Some(line.trim_start_matches("WorkingDirectory=").to_string()); + } + + // User + if line.starts_with("User=") { + service.user = Some(line.trim_start_matches("User=").to_string()); + } + + // ExecStart + if line.starts_with("ExecStart=") { + service.exec_start = Some(line.trim_start_matches("ExecStart=").to_string()); + } + + // Environment con PORT + if line.starts_with("Environment=") && line.contains("PORT") { + if let Some(port) = extract_port_from_env(line) { + service.port = Some(port); + } + } + } + + logger.info("Discovery", &format!(" App: {}, User: {:?}, WorkDir: {:?}", + service.app_name, + service.user, + service.working_directory + )); + + Some(service) +} + +/// Extrae el puerto de una línea Environment +/// Ejemplo: Environment="PORT=3000" -> Some(3000) +fn extract_port_from_env(line: &str) -> Option { + // Buscar PORT=número + if let Some(start) = line.find("PORT=") { + let after_port = &line[start + 5..]; + // Extraer números + let port_str: String = after_port.chars() + .take_while(|c| c.is_numeric()) + .collect(); + + port_str.parse::().ok() + } else { + None + } +} + +/// Sincroniza los servicios descubiertos con monitored_apps.json +pub fn sync_discovered_services(services: Vec) { + let logger = get_logger(); + let config_manager = get_config_manager(); + + logger.info("Discovery", "Sincronizando servicios descubiertos con configuración..."); + + let mut added_count = 0; + let mut skipped_count = 0; + + for service in services { + // Intentar detectar el puerto si no se encontró en Environment + let port = service.port.unwrap_or_else(|| { + // Intentar extraer del nombre o usar puerto por defecto + detect_port_from_name(&service.app_name) + }); + + // Verificar si ya existe en la configuración + let existing_apps = config_manager.get_apps(); + let already_exists = existing_apps.iter().any(|app| app.name == service.app_name); + + if already_exists { + logger.info("Discovery", &format!("⏭️ {} ya existe en configuración", service.app_name)); + skipped_count += 1; + continue; + } + + // Agregar a monitored_apps.json + logger.info("Discovery", &format!("➕ Agregando {} (puerto: {})", service.app_name, port)); + + match config_manager.add_app(service.app_name.clone(), port) { + Ok(_) => { + logger.info("Discovery", &format!("✅ {} agregado exitosamente", service.app_name)); + added_count += 1; + } + Err(e) => { + logger.error("Discovery", &format!("Error agregando {}", service.app_name), Some(&e)); + } + } + } + + logger.info("Discovery", &format!("📊 Resumen: {} agregadas, {} ya existían", added_count, skipped_count)); +} + +/// Intenta detectar el puerto desde el nombre de la app +/// Esto es un fallback simple si no se encuentra en el .service +fn detect_port_from_name(app_name: &str) -> i32 { + // Algunos puertos conocidos por nombre + match app_name.to_lowercase().as_str() { + name if name.contains("tareas") => 3000, + name if name.contains("fidelizacion") => 3001, + name if name.contains("ideas") => 2000, + _ => 8080, // Puerto por defecto genérico + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_extract_app_name() { + assert_eq!(extract_app_name("siax-app-IDEAS.service"), "IDEAS"); + assert_eq!(extract_app_name("siax-app-TAREAS.service"), "TAREAS"); + assert_eq!(extract_app_name("siax-app-fidelizacion.service"), "fidelizacion"); + } + + #[test] + fn test_extract_port_from_env() { + assert_eq!(extract_port_from_env("Environment=PORT=3000"), Some(3000)); + assert_eq!(extract_port_from_env("Environment=\"PORT=8080\""), Some(8080)); + assert_eq!(extract_port_from_env("Environment=NODE_ENV=production"), None); + } + + #[test] + fn test_detect_port_from_name() { + assert_eq!(detect_port_from_name("app_tareas"), 3000); + assert_eq!(detect_port_from_name("IDEAS"), 2000); + assert_eq!(detect_port_from_name("unknown_app"), 8080); + } +} diff --git a/src/lib.rs b/src/lib.rs index 26d0f29..0e2a85e 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -6,8 +6,10 @@ pub mod logger; pub mod config; pub mod monitor; pub mod interface; +pub mod discovery; // Re-exportar solo lo necesario para evitar conflictos pub use models::{ServiceConfig, ManagedApp, AppStatus, ServiceStatus, AppType, RestartPolicy}; pub use logger::{Logger, LogEntry, LogLevel, get_logger}; pub use config::{ConfigManager, MonitoredApp, AppConfig, get_config_manager}; +pub use discovery::{discover_services, sync_discovered_services, DiscoveredService}; diff --git a/src/main.rs b/src/main.rs index 52d59c1..533072f 100644 --- a/src/main.rs +++ b/src/main.rs @@ -6,11 +6,13 @@ mod models; mod systemd; mod orchestrator; mod api; +mod discovery; use logger::get_logger; use config::get_config_manager; use orchestrator::{AppManager, LifecycleManager}; use api::{ApiState, WebSocketManager}; +use discovery::{discover_services, sync_discovered_services}; use std::sync::Arc; use axum::{ routing::{get, post, delete}, @@ -24,7 +26,14 @@ async fn main() { let logger = get_logger(); logger.info("Sistema", "Iniciando SIAX Agent"); - // Inicializar config manager + // 🔍 Descubrir servicios systemd existentes + logger.info("Sistema", "Escaneando servicios systemd existentes..."); + let discovered = discover_services(); + if !discovered.is_empty() { + sync_discovered_services(discovered); + } + + // Inicializar config manager (ahora con servicios descubiertos) let config_manager = get_config_manager(); let apps = config_manager.get_apps(); println!("📋 Apps a monitorear: {:?}", apps);