B4J=true Group=Default Group ModulesStructureVersion=1 Type=Class Version=8.8 @EndOfDesignText@ ' Módulo de clase: Manager ' Este handler proporciona un panel de administración web para el servidor jRDC2-Multi. ' Permite monitorear el estado del servidor, recargar configuraciones de bases de datos, ' ver estadísticas de rendimiento, reiniciar servicios externos, y gestionar la autenticación de usuarios. Sub Class_Globals ' Objeto para generar respuestas JSON. Se utiliza para mostrar mapas de datos de forma legible. Dim j As JSONGenerator ' La clase BCrypt no se usa directamente en este módulo, pero se mantiene si hubiera planes futuros. ' Private bc As BCrypt End Sub ' Subrutina de inicialización de la clase. Se llama cuando se crea un objeto de esta clase. Public Sub Initialize ' No se requiere inicialización específica para esta clase en este momento. End Sub ' Método principal que maneja las peticiones HTTP para el panel de administración. ' req: El objeto ServletRequest que contiene la información de la petición entrante. ' resp: El objeto ServletResponse para construir y enviar la respuesta al cliente. Sub Handle(req As ServletRequest, resp As ServletResponse) ' --- 1. Bloque de Seguridad: Autenticación de Usuario --- ' Verifica si el usuario actual ha iniciado sesión y está autorizado. ' Si no está autorizado, se le redirige a la página de login. If req.GetSession.GetAttribute2("user_is_authorized", False) = False Then resp.SendRedirect("/login") Return ' Termina la ejecución si no está autorizado. End If ' Obtiene el comando solicitado de los parámetros de la URL (ej. "?command=reload"). Dim Command As String = req.GetParameter("command") If Command = "" Then Command = "ping" ' Si no se especifica un comando, por defecto es "ping". Log($"Command: ${Command}"$) ' --- MANEJO ESPECIAL PARA SNAPSHOT --- ' El comando "snapshot" no devuelve HTML, sino una imagen. Lo manejamos por separado al principio. If Command = "snapshot" Then ' Try ' resp.ContentType = "image/png" ' Dim robot, toolkit As JavaObject ' robot.InitializeNewInstance("java.awt.Robot", Null) ' toolkit.InitializeStatic("java.awt.Toolkit") ' Dim screenRect As JavaObject ' screenRect.InitializeNewInstance("java.awt.Rectangle", Array As Object( _ ' toolkit.RunMethodJO("getDefaultToolkit", Null).RunMethod("getScreenSize", Null))) ' Dim image As JavaObject = robot.RunMethod("createScreenCapture", Array As Object(screenRect)) ' Dim ImageIO As JavaObject ' ImageIO.InitializeStatic("javax.imageio.ImageIO").RunMethod("write", Array As Object(image, "png", resp.OutputStream)) ' Catch ' resp.SendError(500, LastException.Message) ' End Try ' Return ' Detenemos la ejecución aquí para no enviar más HTML. End If ' --- FIN DE MANEJO ESPECIAL --- ' Para todos los demás comandos, construimos la página HTML de respuesta. resp.ContentType = "text/html" ' Establece el tipo de contenido como HTML. Dim sb As StringBuilder ' Usamos StringBuilder para construir eficientemente el HTML. sb.Initialize ' --- Estilos y JavaScript (igual que antes) --- sb.Append("") sb.Append("") sb.Append("") ' --- Cabecera de la Página y Mensaje de Bienvenida --- sb.Append("

Panel de Administración jRDC

") sb.Append($"

Bienvenido, ${req.GetSession.GetAttribute("username")}

"$) ' --- Menú de Navegación del Manager --- ' Este menú incluye las opciones para interactuar con el servidor. sb.Append("") sb.Append("
") sb.Append("") ' --- Resultado del Comando --- sb.Append("

Resultado del Comando: '" & Command & "'

") sb.Append("
") ' ========================================================================= ' ### INICIO DE TU LÓGICA DE COMANDOS INTEGRADA ### ' ========================================================================= If Command = "reload" Then Dim sbTemp As StringBuilder sbTemp.Initialize sbTemp.Append($"Iniciando recarga de configuración (Hot-Swap) ($DateTime{DateTime.Now})"$).Append(" " & CRLF) ' <<< PASO CLAVE 1: DETENER TIMER DE LOGS (ZONA SEGURA DE SQLite) >>> ' Detenemos el timer incondicionalmente al inicio para evitar conflictos de bloqueo con SQLite ' durante la inicialización de conectores. Dim oldTimerState As Boolean = Main.timerLogs.Enabled If oldTimerState Then Main.timerLogs.Enabled = False sbTemp.Append(" -> Timer de limpieza de logs (SQLite) detenido temporalmente.").Append(" " & CRLF) End If ' 1. Crear un nuevo mapa temporal para almacenar los conectores recién inicializados. Dim newConnectors As Map newConnectors.Initialize Dim oldConnectors As Map Dim reloadSuccessful As Boolean = True ' *** INICIO DEL BLOQUE CRÍTICO 1: Obtener oldConnectors con ReentrantLock *** Dim lock1Acquired As Boolean = False Try Main.MainConnectorsLock.RunMethod("lock", Null) lock1Acquired = True oldConnectors = Main.Connectors Catch sbTemp.Append($" -> ERROR CRÍTICO: No se pudo adquirir el bloqueo para leer conectores antiguos: ${LastException.Message}"$).Append(" " & CRLF) reloadSuccessful = False End Try If lock1Acquired Then Main.MainConnectorsLock.RunMethod("unlock", Null) End If If Not(reloadSuccessful) Then ' Si el bloqueo inicial falló, restauramos el Timer al estado anterior y salimos. If oldTimerState Then Main.timerLogs.Enabled = True sb.Append(sbTemp.ToString) sb.Append($"¡ERROR: La recarga de configuración falló en la fase de bloqueo inicial! Los conectores antiguos siguen activos."$).Append(" " & CRLF) Return End If ' 2. Iterar sobre las bases de datos configuradas y crear *nuevas* instancias de RDCConnector. For Each dbKey As String In Main.listaDeCP Try Dim newRDC As RDCConnector newRDC.Initialize(dbKey) ' Inicializa la nueva instancia con la configuración fresca. ' <<< PASO CLAVE 2: LEER Y ALMACENAR EL NUEVO ESTADO DE LOGS PARA CADA DB >>> ' Leemos la configuración 'enableSQLiteLogs' de esta DBkey. Dim enableLogsSetting As Int = newRDC.config.GetDefault("enableSQLiteLogs", 0).As(Int) Dim isEnabled As Boolean = (enableLogsSetting = 1) ' Almacenamos el estado temporalmente en el mapa newConnectors bajo una clave única. newConnectors.Put(dbKey & "_LOG_STATE", isEnabled) sbTemp.Append($" -> Logs de ${dbKey} activados: ${isEnabled}"$).Append(" " & CRLF) ' <<< FIN PASO CLAVE 2 >>> newConnectors.Put(dbKey, newRDC) Dim newPoolStats As Map = newRDC.GetPoolStats sbTemp.Append($" -> ${dbKey}: Nuevo conector inicializado. Conexiones: ${newPoolStats.Get("TotalConnections")}"$).Append(" " & CRLF) Catch sbTemp.Append($" -> ERROR CRÍTICO al inicializar nuevo conector para ${dbKey}: ${LastException.Message}"$).Append(" " & CRLF) reloadSuccessful = False Exit ' Si uno falla, abortamos la recarga. End Try Next sb.Append(sbTemp.ToString) If reloadSuccessful Then ' 3. Si todo fue exitoso, hacemos el Hot-Swap atómico. ' *** INICIO DEL BLOQUE CRÍTICO 2: Reemplazar Main.Connectors con ReentrantLock *** Dim lock2Acquired As Boolean = False Try Main.MainConnectorsLock.RunMethod("lock", Null) lock2Acquired = True Main.Connectors = newConnectors ' Reemplazamos el mapa de conectores completo por el nuevo. Catch sb.Append($" -> ERROR CRÍTICO: No se pudo adquirir el bloqueo para reemplazar conectores: ${LastException.Message}"$).Append(" " & CRLF) reloadSuccessful = False End Try If lock2Acquired Then Main.MainConnectorsLock.RunMethod("unlock", Null) End If If reloadSuccessful Then ' Si el swap fue exitoso ' <<< PASO CLAVE 3: APLICAR EL NUEVO ESTADO GLOBAL GRANULAR Y REINICIAR TIMER >>> ' 3a. Reemplazar el mapa de estados de logging granular Main.SQLiteLoggingStatusByDB.Clear ' Limpiamos el mapa global Dim isAnyEnabled As Boolean = False For Each dbKey As String In Main.listaDeCP ' Recuperamos el estado logueado temporalmente. Dim isEnabled As Boolean = newConnectors.Get(dbKey & "_LOG_STATE").As(Boolean) Main.SQLiteLoggingStatusByDB.Put(dbKey, isEnabled) ' Aplicamos el estado al mapa global If isEnabled Then isAnyEnabled = True ' Calculamos el flag general Next ' 3b. Controlar el Timer y el flag global Main.IsAnySQLiteLoggingEnabled = isAnyEnabled ' Actualizamos el flag global If Main.IsAnySQLiteLoggingEnabled Then Main.timerLogs.Enabled = True sb.Append($" -> Logs de SQLite HABILITADOS (Granular). Timer de limpieza ACTIVADO."$).Append(" " & CRLF) Else Main.timerLogs.Enabled = False sb.Append($" -> Logs de SQLite DESHABILITADOS (Total). Timer de limpieza PERMANECERÁ DETENIDO."$).Append(" " & CRLF) End If ' <<< FIN PASO CLAVE 3 >>> sb.Append($"¡Recarga de configuración completada con éxito (Hot-Swap)!"$).Append(" " & CRLF) ' ... (Resto del código: Mostrar estado de pools y Cierre explícito de oldConnectors) ... Else sb.Append($"¡ERROR: La recarga de configuración falló en la fase de reemplazo de conectores! Los conectores antiguos siguen activos."$).Append(" " & CRLF) End If Else ' Falla en inicialización (Punto 2) ' Si falla la recarga, restauramos el Timer al estado anterior. If oldTimerState Then Main.timerLogs.Enabled = True sb.Append(" -> Restaurando Timer de limpieza de logs (SQLite) al estado ACTIVO debido a fallo en recarga.").Append(" " & CRLF) End If sb.Append($"¡ERROR: La recarga de configuración falló durante la inicialización de nuevos conectores! Los conectores antiguos siguen activos."$).Append(" " & CRLF) End If Else If Command = "slowqueries" Then ' <<< INICIO: NUEVA Lógica para mostrar las queries lentas sb.Append("

Consultas Lentas Recientes

") sb.Append("(Este registro depende de que los logs estén habilitados con del parámetro ""enableSQLiteLogs=1"" en config properties)

") Try ' 1. Calcular el límite de tiempo: el tiempo actual (en milisegundos) menos 1 hora (3,600,000 ms). Dim oneHourAgoMs As Long = DateTime.Now - 3600000 ' Ajusta la consulta SQL para obtener las 20 queries más lentas. ' Utilizamos datetime con 'unixepoch' y 'localtime' para una visualización legible del timestamp. Dim rs As ResultSet = Main.SQL1.ExecQuery($"SELECT query_name, duration_ms, datetime(timestamp / 1000, 'unixepoch', 'localtime') as timestamp_local, db_key, client_ip, busy_connections, handler_active_requests FROM query_logs WHERE timestamp >= ${oneHourAgoMs} ORDER BY duration_ms DESC LIMIT 20"$) sb.Append("") sb.Append("") sb.Append("") Do While rs.NextRow sb.Append("") sb.Append($""$) sb.Append($""$) sb.Append($""$) sb.Append($""$) sb.Append($""$) sb.Append($""$) sb.Append($""$) sb.Append("") Loop sb.Append("") sb.Append("
QueryDuración (ms)Fecha/Hora LocalDB KeyCliente IPConex. OcupadasPeticiones Activas
${rs.GetString("query_name")}${rs.GetLong("duration_ms")}${rs.GetString("timestamp_local")}${rs.GetString("db_key")}${rs.GetString("client_ip")}${rs.GetInt("busy_connections")}${rs.GetInt("handler_active_requests")}
") rs.Close Catch Log("Error al obtener queries lentas en Manager: " & LastException.Message) sb.Append($"

Error al cargar queries lentas: ${LastException.Message}

"$) End Try Else If Command = "test" Then Try Dim con As SQL = Main.Connectors.Get("DB1").As(RDCConnector).GetConnection("") sb.Append("Connection successful.

") Private estaDB As String = "" Log(Main.listaDeCP) For i = 0 To Main.listaDeCP.Size - 1 If Main.listaDeCP.get(i) <> "" Then estaDB = "." & Main.listaDeCP.get(i) sb.Append($"Using config${estaDB}.properties
"$) Next con.Close Catch resp.Write("Error fetching connection.") End Try Else If Command = "stop" Then ' Public shl As Shell... Else If Command = "rsx" Then Log($"Ejecutamos ${File.DirApp}\start.bat"$) sb.Append($"Ejecutamos ${File.DirApp}\start.bat"$) Public shl As Shell shl.Initialize("shl","cmd",Array("/c",File.DirApp & "\start.bat " & Main.srvr.Port)) shl.WorkingDirectory = File.DirApp shl.Run(-1) Else If Command = "rpm2" Then Log($"Ejecutamos ${File.DirApp}\reiniciaProcesoPM2.bat"$) sb.Append($"Ejecutamos ${File.DirApp}\reiniciaProcesoPM2.bat"$) Public shl As Shell shl.Initialize("shl","cmd",Array("/c",File.DirApp & "\reiniciaProcesoPM2.bat " & Main.srvr.Port)) shl.WorkingDirectory = File.DirApp shl.Run(-1) Else If Command = "reviveBow" Then Log($"Ejecutamos ${File.DirApp}\reiniciaProcesoBow.bat"$) sb.Append($"Ejecutamos ${File.DirApp}\reiniciaProcesoBow.bat

"$) sb.Append($"!!!BOW REINICIANDO!!!"$) Public shl As Shell shl.Initialize("shl","cmd",Array("/c",File.DirApp & "\reiniciaProcesoBow.bat " & Main.srvr.Port)) shl.WorkingDirectory = File.DirApp shl.Run(-1) Else If Command = "paused" Then GlobalParameters.IsPaused = 1 sb.Append("Servidor pausado.") Else If Command = "continue" Then GlobalParameters.IsPaused = 0 sb.Append("Servidor reanudado.") Else If Command = "logs" Then If GlobalParameters.mpLogs.IsInitialized Then j.Initialize(GlobalParameters.mpLogs) sb.Append(j.ToString) End If Else If Command = "block" Then Dim BlockedConIP As String = req.GetParameter("IP") If GlobalParameters.mpBlockConnection.IsInitialized Then GlobalParameters.mpBlockConnection.Put(BlockedConIP, BlockedConIP) sb.Append("IP bloqueada: " & BlockedConIP) End If Else If Command = "unblock" Then Dim UnBlockedConIP As String = req.GetParameter("IP") If GlobalParameters.mpBlockConnection.IsInitialized Then GlobalParameters.mpBlockConnection.Remove(UnBlockedConIP) sb.Append("IP desbloqueada: " & UnBlockedConIP) End If Else If Command = "restartserver" Then Log($"Ejecutamos ${File.DirApp}/restarServer.bat"$) sb.Append("Reiniciando servidor...") Else If Command = "runatstartup" Then File.Copy("C:\jrdcinterface", "startup.bat", "C:\ProgramData\Microsoft\Windows\Start Menu\Programs\StartUp", "startup.bat") sb.Append("Script de inicio añadido.") Else If Command = "stoprunatstartup" Then File.Delete("C:\ProgramData\Microsoft\Windows\Start Menu\Programs\StartUp", "startup.bat") sb.Append("Script de inicio eliminado.") Else If Command = "totalrequests" Then If GlobalParameters.mpTotalRequests.IsInitialized Then j.Initialize(GlobalParameters.mpTotalRequests) sb.Append(j.ToString) End If Else If Command = "totalblocked" Then If GlobalParameters.mpBlockConnection.IsInitialized Then ' j.Initialize(Global.mpBlockConnection) sb.Append(j.ToString) End If Else If Command = "ping" Then sb.Append($"Pong ($DateTime{DateTime.Now})"$) Else If Command = "totalcon" Then ' <<< Modificado: Ahora usa GetPoolStats para cada pool ' Verificamos que el mapa global de conexiones esté inicializado. ' Aunque no lo poblamos directamente, es un buen chequeo de estado. If GlobalParameters.mpTotalConnections.IsInitialized Then sb.Append("

Estadísticas del Pool de Conexiones por DB:

") ' Creamos un mapa LOCAL para almacenar las estadísticas de TODOS los pools de conexiones. Dim allPoolStats As Map allPoolStats.Initialize ' Iteramos sobre cada clave de base de datos que tenemos configurada (DB1, DB2, etc.). For Each dbKey As String In Main.listaDeCP ' Obtenemos el conector RDC para la base de datos actual. Dim connector As RDCConnector = Main.Connectors.Get(dbKey).As(RDCConnector) ' Si el conector no está inicializado (lo cual no debería ocurrir si Main.AppStart funcionó), ' registramos un error y pasamos al siguiente. If connector.IsInitialized = False Then Log($"Manager: ADVERTENCIA: El conector para ${dbKey} no está inicializado."$) Dim errorMap As Map = CreateMap("Error": "Conector no inicializado o no cargado correctamente") allPoolStats.Put(dbKey, errorMap) Continue ' Salta a la siguiente iteración del bucle. End If ' Llamamos al método GetPoolStats del conector para obtener las métricas de su pool. Dim poolStats As Map = connector.GetPoolStats ' Añadimos las estadísticas de este pool (poolStats) al mapa general (allPoolStats), ' usando la clave de la base de datos (dbKey) como su identificador. allPoolStats.Put(dbKey, poolStats) Next ' Inicializamos el generador JSON con el mapa 'allPoolStats' (que ahora sí debería contener datos). ' (La variable 'j' ya está declarada en Class_Globals de Manager.bas, no la declares de nuevo aquí). j.Initialize(allPoolStats) ' Añadimos la representación JSON de las estadísticas al StringBuilder para la respuesta HTML. sb.Append(j.ToString) Else sb.Append("El mapa de conexiones GlobalParameters.mpTotalConnections no está inicializado.") End If End If ' ========================================================================= ' ### FIN DE TU LÓGICA DE COMANDOS ### ' ========================================================================= ' --- Cerramos la página y la enviamos --- sb.Append("

Cerrar Sesión | Cambiar Contraseña

") resp.Write(sb.ToString) If GlobalParameters.mpLogs.IsInitialized Then GlobalParameters.mpLogs.Put(Command, "Manager : " & Command & " - Time : " & DateTime.Time(DateTime.Now)) End Sub