import psutil import platform import socket import json import subprocess import time import requests from datetime import datetime, timedelta import os import re import threading import sys from ping3 import ping class SystemReporter: def __init__(self): self.sistema = platform.system() self.computer_name = socket.gethostname() self.config = self.cargar_configuracion() self.configuracion_remota = None self.ultima_actualizacion_config = None self.ejecutando = True # Obtener configuración inicial del servidor self.obtener_configuracion_remota() def cargar_configuracion(self): """Carga la configuración desde config.json - FALLA si no existe""" config_path = os.path.join(os.path.dirname(__file__), 'config.json') if not os.path.exists(config_path): print("ERROR CRITICO: Archivo config.json no encontrado") print(f"Ruta esperada: {config_path}") sys.exit(1) try: with open(config_path, 'r', encoding='utf-8') as f: config = json.load(f) except json.JSONDecodeError as e: print(f"ERROR CRITICO: config.json tiene formato JSON inválido: {e}") sys.exit(1) except Exception as e: print(f"ERROR CRITICO: No se puede leer config.json: {e}") sys.exit(1) # Validar campos requeridos básicos campos_requeridos = ['baseUrl', 'dispositivoId'] campos_faltantes = [campo for campo in campos_requeridos if campo not in config] if campos_faltantes: print(f"ERROR CRITICO: Campos faltantes en config.json: {campos_faltantes}") sys.exit(1) return config def obtener_configuracion_remota(self): """Obtiene la configuración del servidor remoto""" max_intentos = 3 for intento in range(max_intentos): try: url = f"{self.config['baseUrl']}/v1/configuracion/{self.config['dispositivoId']}" response = requests.get(url, timeout=10) response.raise_for_status() data = response.json() if data.get('response') == 'SUCCESS' and 'data' in data: self.configuracion_remota = data['data'] self.ultima_actualizacion_config = datetime.now() # Actualizar config.json con el nuevo intervalo self.config['intervalMinutos'] = self.configuracion_remota['conexionMinutos'] self.guardar_configuracion_local() print(f"Configuración remota obtenida exitosamente") print(f"Intervalo actualizado: {self.configuracion_remota['conexionMinutos']} minutos") print(f"Servicios configurados: {len(self.configuracion_remota.get('servicios', []))}") return True else: print(f"Respuesta inesperada del servidor: {data}") except requests.exceptions.Timeout: print(f"Timeout obteniendo configuración - intento {intento + 1}/{max_intentos}") except requests.exceptions.ConnectionError: print(f"Error de conexión obteniendo configuración - intento {intento + 1}/{max_intentos}") except requests.exceptions.RequestException as e: print(f"Error HTTP obteniendo configuración: {e}") except Exception as e: print(f"Error inesperado obteniendo configuración: {e}") if intento < max_intentos - 1: time.sleep(2 ** intento) # Backoff exponencial print("ERROR: No se pudo obtener la configuración remota") if self.configuracion_remota is None: # Si es la primera vez y no hay configuración, usar valores por defecto print("Usando configuración local por defecto") self.configuracion_remota = { 'conexionMinutos': self.config.get('intervalMinutos', 5), 'servicios': [] } return False def guardar_configuracion_local(self): """Guarda la configuración actualizada en config.json""" try: config_path = os.path.join(os.path.dirname(__file__), 'config.json') with open(config_path, 'w', encoding='utf-8') as f: json.dump(self.config, f, indent=4, ensure_ascii=False) except Exception as e: print(f"Error guardando configuración local: {e}") def verificar_actualizacion_configuracion(self): """Verifica si hay actualizaciones en la configuración""" config_anterior = self.configuracion_remota.copy() if self.configuracion_remota else None if self.obtener_configuracion_remota(): # Verificar si cambió el intervalo if (config_anterior and config_anterior.get('conexionMinutos') != self.configuracion_remota.get('conexionMinutos')): print(f"Intervalo actualizado de {config_anterior.get('conexionMinutos')} a {self.configuracion_remota.get('conexionMinutos')} minutos") return True # Verificar si cambiaron los servicios if (config_anterior and config_anterior.get('servicios') != self.configuracion_remota.get('servicios')): print("Servicios de monitoreo actualizados") return True return False def obtener_marca_computadora(self): """Obtiene la marca de la computadora según el sistema operativo""" try: if self.sistema == "Windows": # Opción 1: Usar PowerShell (disponible en todos los Windows modernos) try: resultado = subprocess.run( ['powershell', '-Command', 'Get-CimInstance -ClassName Win32_ComputerSystem | Select-Object -ExpandProperty Manufacturer'], capture_output=True, text=True, timeout=10 ) if resultado.returncode == 0: manufacturer = resultado.stdout.strip() if manufacturer and manufacturer != "To be filled by O.E.M.": return manufacturer except: pass # Opción 2: Usar registro de Windows con Python try: import winreg key = winreg.OpenKey(winreg.HKEY_LOCAL_MACHINE, r"HARDWARE\DESCRIPTION\System\BIOS") manufacturer, _ = winreg.QueryValueEx(key, "SystemManufacturer") winreg.CloseKey(key) if manufacturer and manufacturer != "To be filled by O.E.M.": return manufacturer except: pass elif self.sistema == "Linux": # Intentar con dmidecode (sin sudo primero) try: resultado = subprocess.run( ['dmidecode', '-s', 'system-manufacturer'], capture_output=True, text=True, timeout=10 ) if resultado.returncode == 0: manufacturer = resultado.stdout.strip() if manufacturer and manufacturer != "To be filled by O.E.M.": return manufacturer except: pass # Alternativa: leer desde /sys/class/dmi/id/ try: with open('/sys/class/dmi/id/sys_vendor', 'r') as f: manufacturer = f.read().strip() if manufacturer and manufacturer != "To be filled by O.E.M.": return manufacturer except: pass elif self.sistema == "Darwin": # macOS resultado = subprocess.run( ["sysctl", "-n", "hw.model"], capture_output=True, text=True, timeout=5 ) if resultado.returncode == 0: return resultado.stdout.strip() except Exception as e: print(f"Error obteniendo marca: {e}") return "Desconocido" def obtener_version_sistema_operativo(self): """Obtiene la versión detallada del sistema operativo. En Windows: Build.UBR (ej: 26200.7462) En Linux: Versión del kernel o distribución """ try: if self.sistema == "Windows": # Obtener Build Number y UBR desde el registro de Windows try: import winreg key = winreg.OpenKey( winreg.HKEY_LOCAL_MACHINE, r"SOFTWARE\Microsoft\Windows NT\CurrentVersion" ) # Obtener CurrentBuild (ej: 26200) current_build, _ = winreg.QueryValueEx(key, "CurrentBuild") # Obtener UBR - Update Build Revision (ej: 7462) try: ubr, _ = winreg.QueryValueEx(key, "UBR") version_completa = f"{current_build}.{ubr}" except WindowsError: version_completa = str(current_build) winreg.CloseKey(key) return version_completa except Exception as e: print(f"Error leyendo registro de Windows: {e}") # Fallback: usar PowerShell try: resultado = subprocess.run( ['powershell', '-Command', '(Get-ItemProperty "HKLM:\\SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion").CurrentBuild + "." + (Get-ItemProperty "HKLM:\\SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion").UBR'], capture_output=True, text=True, timeout=10 ) if resultado.returncode == 0 and resultado.stdout.strip(): return resultado.stdout.strip() except Exception: pass # Último fallback: platform.version() return platform.version() elif self.sistema == "Linux": # Intentar obtener versión desde /etc/os-release try: with open('/etc/os-release', 'r') as f: for line in f: if line.startswith('VERSION_ID='): version = line.split('=')[1].strip().strip('"') # Complementar con kernel version kernel = platform.release() return f"{version} (kernel {kernel})" except FileNotFoundError: pass # Fallback: usar uname -r (versión del kernel) try: resultado = subprocess.run( ['uname', '-r'], capture_output=True, text=True, timeout=5 ) if resultado.returncode == 0: return resultado.stdout.strip() except Exception: pass return platform.release() elif self.sistema == "Darwin": # macOS try: resultado = subprocess.run( ['sw_vers', '-productVersion'], capture_output=True, text=True, timeout=5 ) if resultado.returncode == 0: return resultado.stdout.strip() except Exception: pass return platform.mac_ver()[0] except Exception as e: print(f"Error obteniendo versión del SO: {e}") return platform.version() def actualizar_version_so(self): """Actualiza la versión del sistema operativo en el servidor""" max_intentos = 3 version_so = self.obtener_version_sistema_operativo() print(f"Actualizando versión del SO: {version_so}") for intento in range(max_intentos): try: url = f"{self.config['baseUrl']}/v1/dispositivo/version-so/{self.config['dispositivoId']}" headers = {'Content-Type': 'application/json'} body = {"versionSistemaOperativo": version_so} response = requests.put(url, json=body, headers=headers, timeout=10) response.raise_for_status() print(f"Versión del SO actualizada exitosamente - Status: {response.status_code}") return True except requests.exceptions.Timeout: print(f"Timeout actualizando versión SO - intento {intento + 1}/{max_intentos}") except requests.exceptions.ConnectionError: print(f"Error de conexión actualizando versión SO - intento {intento + 1}/{max_intentos}") except requests.exceptions.RequestException as e: print(f"Error HTTP actualizando versión SO: {e}") except Exception as e: print(f"Error inesperado actualizando versión SO: {e}") if intento < max_intentos - 1: time.sleep(2 ** intento) print("ADVERTENCIA: No se pudo actualizar la versión del SO") return False def obtener_numero_serie(self): """Obtiene el número de serie del sistema""" try: if self.sistema == "Windows": # Opción 1: Usar PowerShell (disponible en todos los Windows modernos) try: resultado = subprocess.run( ['powershell', '-Command', 'Get-CimInstance -ClassName Win32_BIOS | Select-Object -ExpandProperty SerialNumber'], capture_output=True, text=True, timeout=10 ) if resultado.returncode == 0: serial = resultado.stdout.strip() if serial and serial not in ["To be filled by O.E.M.", "Default string", "0"]: return serial except: pass # Opción 2: Usar registro de Windows con Python try: import winreg key = winreg.OpenKey(winreg.HKEY_LOCAL_MACHINE, r"HARDWARE\DESCRIPTION\System\BIOS") serial, _ = winreg.QueryValueEx(key, "SystemProductName") winreg.CloseKey(key) if serial and serial not in ["To be filled by O.E.M.", "Default string"]: return serial except: pass elif self.sistema == "Linux": # Intentar con dmidecode (sin sudo primero) try: resultado = subprocess.run( ['dmidecode', '-s', 'system-serial-number'], capture_output=True, text=True, timeout=10 ) if resultado.returncode == 0: serial = resultado.stdout.strip() if serial and serial not in ["To be filled by O.E.M.", "Default string"]: return serial except: pass # Alternativa: leer desde /sys/class/dmi/id/ try: with open('/sys/class/dmi/id/product_serial', 'r') as f: serial = f.read().strip() if serial and serial not in ["To be filled by O.E.M.", "Default string"]: return serial except: pass elif self.sistema == "Darwin": # macOS resultado = subprocess.run( ["system_profiler", "SPHardwareDataType"], capture_output=True, text=True, timeout=5 ) if resultado.returncode == 0: for line in resultado.stdout.split('\n'): if 'Serial Number' in line: return line.split(':')[1].strip() except Exception as e: print(f"Error obteniendo número de serie: {e}") return "Desconocido" def obtener_info_basica(self): """Obtiene información básica del sistema""" try: # Intentar usar cpuinfo para procesador try: import cpuinfo cpu_info = cpuinfo.get_cpu_info() procesador_modelo = cpu_info.get('brand_raw', 'Desconocido') except ImportError: procesador_modelo = platform.processor() or 'Desconocido' return { "soTecnico": f"{platform.system()} {platform.release()}", "hostnameTecnico": self.computer_name, "marcaTecnico": self.obtener_marca_computadora(), "serialTecnico": self.obtener_numero_serie(), "procesadorTecnico": procesador_modelo } except Exception as e: print(f"Error obteniendo información básica: {e}") return { "soTecnico": "Desconocido", "hostnameTecnico": self.computer_name, "marcaTecnico": "Desconocido", "serialTecnico": "Desconocido", "procesadorTecnico": "Desconocido" } def actualizar_info_tecnica(self): """Actualiza la información técnica del dispositivo en el servidor""" max_intentos = 3 print("Actualizando información técnica del dispositivo...") info_basica = self.obtener_info_basica() print(f" SO: {info_basica['soTecnico']}") print(f" Hostname: {info_basica['hostnameTecnico']}") print(f" Marca: {info_basica['marcaTecnico']}") print(f" Serial: {info_basica['serialTecnico']}") print(f" Procesador: {info_basica['procesadorTecnico']}") for intento in range(max_intentos): try: url = f"{self.config['baseUrl']}/v1/dispositivo/tecnico/{self.config['dispositivoId']}" headers = {'Content-Type': 'application/json'} response = requests.put(url, json=info_basica, headers=headers, timeout=10) response.raise_for_status() print(f"Información técnica actualizada exitosamente - Status: {response.status_code}") return True except requests.exceptions.Timeout: print(f"Timeout actualizando info técnica - intento {intento + 1}/{max_intentos}") except requests.exceptions.ConnectionError: print(f"Error de conexión actualizando info técnica - intento {intento + 1}/{max_intentos}") except requests.exceptions.RequestException as e: print(f"Error HTTP actualizando info técnica: {e}") except Exception as e: print(f"Error inesperado actualizando info técnica: {e}") if intento < max_intentos - 1: time.sleep(2 ** intento) # Backoff exponencial print("ADVERTENCIA: No se pudo actualizar la información técnica") return False def obtener_info_procesador(self): """Obtiene información del procesador - optimizado""" try: return { "procesadorNucleos": str(psutil.cpu_count(logical=False) or psutil.cpu_count()), "procesadorUsoPorcentaje": str(round(psutil.cpu_percent(interval=0.1), 1)) } except Exception: return {"procesadorNucleos": "0", "procesadorUsoPorcentaje": "0.0"} def obtener_info_ram(self): """Obtiene información de RAM - optimizado""" try: vm = psutil.virtual_memory() return { "ramCapacidadGb": str(round(vm.total / (1024**3), 4)), "ramUsoPorcentaje": str(round(vm.percent, 1)) } except Exception: return {"ramCapacidadGb": "0.0", "ramUsoPorcentaje": "0.0"} def obtener_info_bateria(self): """Obtiene información de batería - optimizado""" try: battery = psutil.sensors_battery() if battery: return { "bateriaInstalada": True, "bateriaCargaPorcentaje": str(round(battery.percent, 1)), "bateriaConectada": battery.power_plugged } except Exception: pass return { "bateriaInstalada": False, "bateriaCargaPorcentaje": "0", "bateriaConectada": False } def obtener_estres_disco_rapido(self): """Obtiene estrés de disco con muestra rápida""" try: disk_io_inicial = psutil.disk_io_counters() time.sleep(0.5) # Reducido para ser más rápido disk_io_final = psutil.disk_io_counters() if disk_io_inicial and disk_io_final: bytes_read = disk_io_final.read_bytes - disk_io_inicial.read_bytes bytes_write = disk_io_final.write_bytes - disk_io_inicial.write_bytes read_speed_mbs = (bytes_read / 0.5) / (1024 * 1024) write_speed_mbs = (bytes_write / 0.5) / (1024 * 1024) max_speed_typical = 100 read_stress = min(100, (read_speed_mbs / max_speed_typical) * 100) write_stress = min(100, (write_speed_mbs / max_speed_typical) * 100) return round(read_stress, 1), round(write_stress, 1) except Exception: pass return 0.0, 0.0 def obtener_info_discos(self): """Obtiene información de discos - optimizado""" discos = [] try: partitions = psutil.disk_partitions() avg_read, avg_write = self.obtener_estres_disco_rapido() for partition in partitions: try: # Solo incluir particiones principales if partition.fstype in ['', 'squashfs', 'tmpfs']: continue usage = psutil.disk_usage(partition.mountpoint) discos.append({ "sistemaArchivos": partition.fstype, "capacidadGb": str(round(usage.total / (1024**3), 4)), "usoPorcentaje": str(round((usage.used / usage.total) * 100, 4)), "lecturaPorcentaje": str(avg_read), "escrituraPorcentaje": str(avg_write) }) except (PermissionError, OSError): continue except Exception: pass return discos def obtener_velocidad_red_rapida(self, interfaz): """Obtiene velocidad de red con medición rápida""" try: stats_inicial = psutil.net_io_counters(pernic=True)[interfaz] time.sleep(0.5) # Reducido para ser más rápido stats_final = psutil.net_io_counters(pernic=True)[interfaz] bytes_up = stats_final.bytes_sent - stats_inicial.bytes_sent bytes_down = stats_final.bytes_recv - stats_inicial.bytes_recv mbps_up = round((bytes_up * 8) / (1024 * 1024 * 0.5), 4) mbps_down = round((bytes_down * 8) / (1024 * 1024 * 0.5), 4) return mbps_up, mbps_down except Exception: return 0.0, 0.0 def obtener_ip_interfaz(self, interfaz): """Obtiene IP de una interfaz - optimizado""" try: addrs = psutil.net_if_addrs().get(interfaz, []) for addr in addrs: if addr.family == socket.AF_INET: if addr.netmask: try: cidr = sum([bin(int(x)).count('1') for x in addr.netmask.split('.')]) return f"{addr.address}/{cidr}" except: return addr.address return addr.address except Exception: pass return "N/A" def obtener_mac_interfaz(self, interfaz): """Obtiene MAC de una interfaz - optimizado""" try: addrs = psutil.net_if_addrs().get(interfaz, []) for addr in addrs: if addr.family == psutil.AF_LINK: return addr.address except Exception: pass return "N/A" def obtener_info_redes(self): """Obtiene información de interfaces de red - optimizado""" interfaces = [] try: net_if_stats = psutil.net_if_stats() for interfaz, stats in net_if_stats.items(): # Filtrar interfaces irrelevantes if any(skip in interfaz.lower() for skip in [ 'loopback', 'vmware', 'virtualbox', 'docker', 'vethernet', 'teredo', 'isatap', '6to4' ]): continue # Solo incluir interfaces que estén UP o tengan IP válida ip = self.obtener_ip_interfaz(interfaz) if not stats.isup and ip == "N/A": continue velocidad_up, velocidad_down = self.obtener_velocidad_red_rapida(interfaz) interfaces.append({ "nombre": interfaz, "mac": self.obtener_mac_interfaz(interfaz), "ip": ip, "estadoRed": stats.isup, "velocidadSubidaMbps": str(velocidad_up), "velocidadBajadaMbps": str(velocidad_down) }) except Exception: pass return interfaces def hacer_ping_nmap(self, direccion): """Realiza ping usando ping3 (librería Python pura) y extrae la latencia real""" try: # Ejecutar ping con timeout de 10 segundos # ping() retorna latencia en segundos (float) o None si falla latencia = ping(direccion, timeout=10) if latencia is not None: # Convertir a string con 3 decimales (ej: "0.043") latencia_str = str(round(latencia, 3)) print(f"Latencia encontrada para {direccion}: {latencia_str}s") return latencia_str else: # Host no responde print(f"Host {direccion} no responde") return "0.0" except PermissionError: print(f"Error de permisos al hacer ping a {direccion}. Se requieren permisos de administrador.") return "0.0" except OSError as e: # Error de red o host no alcanzable print(f"Error de red al hacer ping a {direccion}: {e}") return "0.0" except Exception as e: print(f"Error en ping para {direccion}: {e}") return "0.0" def verificar_puerto(self, direccion, puerto): """Verifica si un puerto está abierto usando socket (TCP connect)""" try: print(f" Verificando puerto {puerto} en {direccion}") # Crear socket TCP sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) sock.settimeout(2) # Timeout de 2 segundos # Intentar conectar al puerto # connect_ex() retorna 0 si la conexión es exitosa, error code si falla resultado = sock.connect_ex((direccion, puerto)) sock.close() # Resultado 0 = conexión exitosa (puerto abierto) puerto_abierto = (resultado == 0) if puerto_abierto: print(f" Puerto {puerto} en {direccion}: open") else: print(f" Puerto {puerto} en {direccion}: closed/filtered") return puerto_abierto except socket.gaierror as e: # Error de DNS o resolución de nombre print(f" Error DNS al verificar {direccion}: {e}") return False except socket.timeout: print(f" Timeout verificando puerto {puerto} en {direccion}") return False except Exception as e: print(f" Error verificando puerto {puerto} en {direccion}: {e}") return False def obtener_latencia_ping(self): """Obtiene latencia de ping y verificación de puertos basado en configuración remota""" latencias = [] if not self.configuracion_remota or 'servicios' not in self.configuracion_remota: print("No hay servicios configurados para monitorear") return latencias servicios = self.configuracion_remota['servicios'] for servicio in servicios: try: direccion = servicio.get('direccion', '') puerto = servicio.get('puerto', 0) tipo_servicio = servicio.get('tipoServicio', True) # true = ping, false = port scan descripcion = servicio.get('descripcion', f'{direccion}:{puerto}') servicio_id = servicio.get('servicioId', 0) if not direccion: continue latencia_entry = { "nombreDestino": direccion, "respuestaMs": "0.0", "esRevision": True, "nombrePuerto": str(puerto), "respuestaPuerto": False, "servicioId": servicio_id } if tipo_servicio: # Ping primero, luego puerto opcional latencia_ms = self.hacer_ping_nmap(direccion) latencia_entry["respuestaMs"] = latencia_ms # También verificar el puerto si está especificado if puerto > 0: puerto_abierto = self.verificar_puerto(direccion, puerto) latencia_entry["respuestaPuerto"] = puerto_abierto else: # Puerto primero, pero SIEMPRE hacer ping # Primero verificar el puerto if puerto > 0: puerto_abierto = self.verificar_puerto(direccion, puerto) latencia_entry["respuestaPuerto"] = puerto_abierto # SIEMPRE hacer ping, sin importar el estado del puerto latencia_ms = self.hacer_ping_nmap(direccion) latencia_entry["respuestaMs"] = latencia_ms latencias.append(latencia_entry) except Exception as e: print(f"Error procesando servicio {servicio}: {e}") continue return latencias def enviar_datos_api(self, datos): """Envía los datos a la API con manejo robusto de errores""" max_intentos = 3 for intento in range(max_intentos): try: url = f"{self.config['baseUrl']}/v1/revision" headers = {'Content-Type': 'application/json'} response = requests.post(url, json=datos, headers=headers, timeout=15) response.raise_for_status() print(f"Datos enviados exitosamente - Status: {response.status_code}") return True except requests.exceptions.Timeout: print(f"Timeout en intento {intento + 1}/{max_intentos}") if intento < max_intentos - 1: time.sleep(2 ** intento) # Backoff exponencial except requests.exceptions.ConnectionError: print(f"Error de conexión en intento {intento + 1}/{max_intentos}") if intento < max_intentos - 1: time.sleep(2 ** intento) except requests.exceptions.RequestException as e: print(f"Error HTTP en intento {intento + 1}/{max_intentos}: {e}") if intento < max_intentos - 1: time.sleep(2 ** intento) except Exception as e: print(f"Error inesperado: {e}") break print("FALLO: No se pudieron enviar los datos después de todos los intentos") return False def recolectar_y_enviar(self): """Recolecta datos del sistema y los envía - optimizado""" try: print(f"Iniciando recolección - {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}") momento = datetime.now().isoformat() # Recolección optimizada en paralelo donde sea posible info_procesador = self.obtener_info_procesador() info_ram = self.obtener_info_ram() info_bateria = self.obtener_info_bateria() # Estas operaciones son más lentas, hacerlas secuencialmente info_discos = self.obtener_info_discos() info_redes = self.obtener_info_redes() latencia_ping = self.obtener_latencia_ping() datos_api = { "dispositivoId": self.config['dispositivoId'], "momento": momento, "procesadorNucleos": info_procesador["procesadorNucleos"], "procesadorUsoPorcentaje": info_procesador["procesadorUsoPorcentaje"], "ramCapacidadGb": info_ram["ramCapacidadGb"], "ramUsoPorcentaje": info_ram["ramUsoPorcentaje"], "bateriaInstalada": info_bateria["bateriaInstalada"], "bateriaCargaPorcentaje": info_bateria["bateriaCargaPorcentaje"], "bateriaConectada": info_bateria["bateriaConectada"], "hostname": self.computer_name, "discos": info_discos, "interfacesRedes": info_redes, "latenciasPing": latencia_ping } print(f"Recolección completada. CPU: {info_procesador['procesadorUsoPorcentaje']}%, RAM: {info_ram['ramUsoPorcentaje']}%") print(f"Servicios monitoreados: {len(latencia_ping)}") # Enviar datos al servidor resultado = self.enviar_datos_api(datos_api) return resultado except Exception as e: print(f"ERROR en recolección: {e}") return False def ejecutar_loop(self): """Loop principal del agente""" if not self.configuracion_remota: print("ERROR: No se pudo obtener configuración inicial. Deteniendo agente.") return intervalo_segundos = self.configuracion_remota['conexionMinutos'] * 60 print(f"AGENTE INICIADO") print(f"Dispositivo ID: {self.config['dispositivoId']}") print(f"Hostname: {self.computer_name}") print(f"Intervalo: {self.configuracion_remota['conexionMinutos']} minutos") print(f"API: {self.config['baseUrl']}") print(f"Servicios configurados: {len(self.configuracion_remota.get('servicios', []))}") print("-" * 60) # ACTUALIZAR INFORMACIÓN TÉCNICA Y VERSIÓN DEL SO ANTES DEL CICLO print("\n" + "=" * 60) self.actualizar_info_tecnica() self.actualizar_version_so() print("=" * 60 + "\n") # Primera ejecución inmediata self.recolectar_y_enviar() while self.ejecutando: try: print(f"Esperando {self.configuracion_remota['conexionMinutos']} minutos hasta próxima recolección...") # Dividir la espera en chunks para poder verificar self.ejecutando y configuración tiempo_espera = 0 intervalo_check = 30 # Verificar configuración cada 30 segundos while tiempo_espera < intervalo_segundos and self.ejecutando: tiempo_chunk = min(intervalo_check, intervalo_segundos - tiempo_espera) time.sleep(tiempo_chunk) tiempo_espera += tiempo_chunk # Verificar si hay cambios en la configuración cada cierto tiempo if tiempo_espera % (intervalo_check * 2) == 0: # Cada minuto if self.verificar_actualizacion_configuracion(): # Si cambió la configuración, recalcular intervalo nuevo_intervalo = self.configuracion_remota['conexionMinutos'] * 60 if nuevo_intervalo != intervalo_segundos: print(f"Intervalo actualizado. Reiniciando ciclo con nuevo tiempo.") intervalo_segundos = nuevo_intervalo break # Salir del loop de espera para empezar nuevo ciclo if self.ejecutando: self.recolectar_y_enviar() except KeyboardInterrupt: print("\nDeteniendo agente...") self.ejecutando = False break except Exception as e: print(f"ERROR en loop principal: {e}") print("Continuando ejecución...") time.sleep(10) # Espera corta antes de reintentar def detener(self): """Detiene el agente de forma segura""" self.ejecutando = False def main(): """Función principal - simplificada""" try: reporter = SystemReporter() reporter.ejecutar_loop() except KeyboardInterrupt: print("\nAgente detenido por el usuario") except SystemExit: pass # Exit codes ya manejados en cargar_configuracion except Exception as e: print(f"ERROR CRITICO: {e}") sys.exit(1) if __name__ == "__main__": main()