- VERSION 5.09.19

- feat(sqlite): Implementa optimización de SQLite (WAL e Índices)
- fix(manager): Extiende el comando 'test' para verificar todos los pools de conexión configurados.

- Mejoras al subsistema de logs y diagnóstico del servidor jRDC2-Multi.
- Cambios principales:
1. Optimización del Rendimiento de SQLite (users.db):
*   Habilitación de WAL: Se implementó PRAGMA journal_mode=WAL y PRAGMA synchronous=NORMAL en `InitializeSQLiteDatabase`. Esto reduce la contención de disco y mejora el rendimiento de I/O en las escrituras transaccionales de logs por lotes.
*   Índices de logs: Se agregaron índices a las columnas `timestamp` y `duration_ms` en `query_logs`, y a `timestamp` en `errores`. Esto acelera drásticamente las operaciones de limpieza periódica (`borraArribaDe15000Logs`) y la generación de reportes de consultas lentas (`slowqueries`).

2. Mejora del Comando de Diagnóstico 'test':
*   Se corrigió el comando `manager?command=test` para que no solo pruebe la conexión de `DB1`, sino que itere sobre `Main.listaDeCP` y fuerce la adquisición y liberación de una conexión (`GetConnection`) en *todos* los `RDCConnector` configurados (DB1, DB2, DB3, etc.).
*   La nueva lógica garantiza una prueba de vida rigurosa de cada pool C3P0, devolviendo un mensaje detallado del estado de conectividad y registrando un error crítico vía `LogServerError` si algún pool no responde.
This commit is contained in:
2025-09-27 20:33:44 -06:00
parent 616013f0fb
commit 8b876e5095
10 changed files with 391 additions and 71 deletions

View File

@@ -24,6 +24,22 @@ Sub Process_Globals
' - Que en el reporte de "Queries lentos" se pueda especificar de cuanto tiempo, ahorita esta de la ultima hora, pero que se pueda seleccionar desde una ' - Que en el reporte de "Queries lentos" se pueda especificar de cuanto tiempo, ahorita esta de la ultima hora, pero que se pueda seleccionar desde una
' lista, por ejemplo 15, 30, 45 y 60 minutos antes. ' lista, por ejemplo 15, 30, 45 y 60 minutos antes.
' - VERSION 5.09.19
' - feat(sqlite): Implementa optimización de SQLite (WAL e Índices)
' - fix(manager): Extiende el comando 'test' para verificar todos los pools de conexión configurados.
'
' - Mejoras al subsistema de logs y diagnóstico del servidor jRDC2-Multi.
'
' - Cambios principales:
'
' 1. Optimización del Rendimiento de SQLite (users.db):
' * Habilitación de WAL: Se implementó PRAGMA journal_mode=WAL y PRAGMA synchronous=NORMAL en `InitializeSQLiteDatabase`. Esto reduce la contención de disco y mejora el rendimiento de I/O en las escrituras transaccionales de logs por lotes.
' * Índices de logs: Se agregaron índices a las columnas `timestamp` y `duration_ms` en `query_logs`, y a `timestamp` en `errores`. Esto acelera drásticamente las operaciones de limpieza periódica (`borraArribaDe15000Logs`) y la generación de reportes de consultas lentas (`slowqueries`).
'
' 2. Mejora del Comando de Diagnóstico 'test':
' * Se corrigió el comando `manager?command=test` para que no solo pruebe la conexión de `DB1`, sino que itere sobre `Main.listaDeCP` y fuerce la adquisición y liberación de una conexión (`GetConnection`) en *todos* los `RDCConnector` configurados (DB1, DB2, DB3, etc.).
' * La nueva lógica garantiza una prueba de vida rigurosa de cada pool C3P0, devolviendo un mensaje detallado del estado de conectividad y registrando un error crítico vía `LogServerError` si algún pool no responde.
' - VERSION 5.09.18 ' - VERSION 5.09.18
' - feat(manager): Implementa recarga granular (Hot-Swap). ' - feat(manager): Implementa recarga granular (Hot-Swap).
' - Actualiza manager.html para solicitar la DB Key a recargar (ej: DB2). ' - Actualiza manager.html para solicitar la DB Key a recargar (ej: DB2).

View File

@@ -522,7 +522,6 @@ Private Sub ExecuteBatch(DB As String, con As SQL, in As InputStream, resp As Se
WriteObject(Main.VERSION, out) WriteObject(Main.VERSION, out)
WriteObject("batch", out) WriteObject("batch", out)
WriteInt(res.Length, out) WriteInt(res.Length, out)
' Log(affectedCounts.Size)
For Each r As Int In affectedCounts For Each r As Int In affectedCounts
WriteInt(r, out) WriteInt(r, out)
Next Next

View File

@@ -25,7 +25,7 @@ AcquireIncrement=1
MaxConnectionAge=60 MaxConnectionAge=60
# Configuración de tolerancia de parámetros: # Configuración de tolerancia de parámetros:
# 1 = Habilita la tolerancia a parámetros de más (se recortarán los excesivos). # 1 = Habilita la tolerancia a parámetros de más (se ignoran los extras).
# 0 = Deshabilita la tolerancia (el servidor será estricto y lanzará un error si hay parámetros de más). # 0 = Deshabilita la tolerancia (el servidor será estricto y lanzará un error si hay parámetros de más).
# Por defecto, si no se especifica o el valor es diferente de 1, la tolerancia estará DESHABILITADA (modo estricto). # Por defecto, si no se especifica o el valor es diferente de 1, la tolerancia estará DESHABILITADA (modo estricto).
parameterTolerance=1 parameterTolerance=1

View File

@@ -1,21 +0,0 @@
<!DOCTYPE html>
<html lang="es">
<head>
<meta charset="UTF-8">
<title>Login jRDC Server</title>
<style>
body { font-family: sans-serif; display: flex; justify-content: center; align-items: center; height: 100vh; background-color: #f0f0f0; }
form { background: white; padding: 2em; border-radius: 8px; box-shadow: 0 4px 8px rgba(0,0,0,0.1); }
input { display: block; margin-bottom: 1em; padding: 0.5em; width: 200px; }
button { padding: 0.7em; width: 100%; border: none; background-color: #007bff; color: white; cursor: pointer; border-radius: 4px; }
</style>
</head>
<body>
<form action="/dologin" method="post">
<h2>Acceso al Manager</h2>
<input type="text" name="username" placeholder="Usuario" required>
<input type="password" name="password" placeholder="Contraseña" required>
<button type="submit">Entrar</button>
</form>
</body>
</html>

View File

@@ -135,6 +135,7 @@
<a data-command="getstats">Estadísticas Pool</a> <a data-command="getstats">Estadísticas Pool</a>
<a data-command="rpm2">Reiniciar (pm2)</a> <a data-command="rpm2">Reiniciar (pm2)</a>
<a data-command="reviveBow">Revive Bow</a> <a data-command="reviveBow">Revive Bow</a>
<a data-command="getconfiginfo">Info</a>
</nav> </nav>
<div class="sse-status-container"> <div class="sse-status-container">
<span>Estado de Estadísticas en Tiempo Real:</span> <span>Estado de Estadísticas en Tiempo Real:</span>

View File

@@ -259,7 +259,6 @@ Sub Handle(req As ServletRequest, resp As ServletResponse)
Exit Exit
End If End If
Next Next
If Main.IsAnySQLiteLoggingEnabled Then If Main.IsAnySQLiteLoggingEnabled Then
Main.timerLogs.Enabled = True Main.timerLogs.Enabled = True
sbTemp.Append($" -> Timer de limpieza de logs ACTIVADO (estado global: HABILITADO)."$).Append(" " & CRLF) sbTemp.Append($" -> Timer de limpieza de logs ACTIVADO (estado global: HABILITADO)."$).Append(" " & CRLF)
@@ -267,11 +266,8 @@ Sub Handle(req As ServletRequest, resp As ServletResponse)
Main.timerLogs.Enabled = False Main.timerLogs.Enabled = False
sbTemp.Append($" -> Timer de limpieza de logs DESHABILITADO (estado global: DESHABILITADO)."$).Append(" " & CRLF) sbTemp.Append($" -> Timer de limpieza de logs DESHABILITADO (estado global: DESHABILITADO)."$).Append(" " & CRLF)
End If End If
sbTemp.Append($"¡Recarga de configuración completada con éxito!"$).Append(" " & CRLF) sbTemp.Append($"¡Recarga de configuración completada con éxito!"$).Append(" " & CRLF)
Else Else
' Si falló, restauramos el estado del timer anterior. ' Si falló, restauramos el estado del timer anterior.
If oldTimerState Then If oldTimerState Then
Main.timerLogs.Enabled = True Main.timerLogs.Enabled = True
@@ -279,31 +275,65 @@ Sub Handle(req As ServletRequest, resp As ServletResponse)
End If End If
sbTemp.Append($"¡ERROR: La recarga de configuración falló! Los conectores antiguos siguen activos."$).Append(" " & CRLF) sbTemp.Append($"¡ERROR: La recarga de configuración falló! Los conectores antiguos siguen activos."$).Append(" " & CRLF)
End If End If
resp.Write(sbTemp.ToString) resp.Write(sbTemp.ToString)
Return Return
Case "test" Case "test"
resp.ContentType = "text/plain; charset=utf-8" resp.ContentType = "text/plain; charset=utf-8"
Dim sb As StringBuilder Dim sb As StringBuilder
sb.Initialize sb.Initialize
sb.Append("--- INICIANDO PRUEBA DE CONECTIVIDAD A TODOS LOS POOLS CONFIGURADOS ---").Append(CRLF).Append(CRLF)
Try ' Iteramos sobre la lista de DB Keys cargadas al inicio (DB1, DB2, etc.)
Dim con As SQL = Main.Connectors.Get("DB1").As(RDCConnector).GetConnection("") For Each dbKey As String In Main.listaDeCP
sb.Append("Connection successful." & CRLF & CRLF) Dim success As Boolean = False
Dim estaDB As String = "" Dim errorMsg As String = ""
Log(Main.listaDeCP) Dim con As SQL ' Conexión para la prueba
For i = 0 To Main.listaDeCP.Size - 1
If Main.listaDeCP.get(i) <> "" Then estaDB = "." & Main.listaDeCP.get(i) Try
sb.Append($"Using config${estaDB}.properties"$ & CRLF) ' 1. Obtener el RDCConnector para esta DBKey
Next Dim connector As RDCConnector = Main.Connectors.Get(dbKey)
con.Close
resp.Write(sb.ToString) If connector.IsInitialized = False Then
Catch errorMsg = "Conector no inicializado (revisa logs de AppStart)"
resp.Write("Error fetching connection: " & LastException.Message) Else
End Try ' 2. Forzar la adquisición de una conexión del pool C3P0
con = connector.GetConnection(dbKey)
If con.IsInitialized Then
' 3. Si la conexión es válida, la cerramos inmediatamente para devolverla al pool
con.Close
success = True
Else
errorMsg = "La conexión devuelta no es válida (SQL.IsInitialized = False)"
End If
End If
Catch
' Capturamos cualquier excepción (ej. fallo de JDBC, timeout de C3P0)
errorMsg = LastException.Message
End Try
If success Then
sb.Append($"* ${dbKey}: Conexión adquirida y liberada correctamente."$).Append(CRLF)
Else
' Si falla, registramos el error para el administrador.
Main.LogServerError("ERROR", "Manager.TestCommand", $"Falló la prueba de conectividad para ${dbKey}: ${errorMsg}"$, dbKey, "test_command", req.RemoteAddress)
sb.Append($"[FALLO] ${dbKey}: ERROR CRÍTICO al obtener conexión. Mensaje: ${errorMsg}"$).Append(CRLF)
End If
Next
sb.Append(CRLF).Append("--- FIN DE PRUEBA DE CONEXIONES ---").Append(CRLF)
' Mantenemos la lista original de archivos de configuración cargados (esto es informativo)
sb.Append(CRLF).Append("Archivos de configuración cargados:").Append(CRLF)
For Each item As String In Main.listaDeCP
Dim configName As String = "config"
If item <> "DB1" Then configName = configName & "." & item
sb.Append($" -> Usando ${configName}.properties"$).Append(CRLF)
Next
resp.Write(sb.ToString)
Return Return
Case "rsx", "rpm2", "revivebow", "restartserver" Case "rsx", "rpm2", "revivebow", "restartserver"
resp.ContentType = "text/plain; charset=utf-8" resp.ContentType = "text/plain; charset=utf-8"
Dim batFile As String Dim batFile As String
@@ -358,7 +388,85 @@ Sub Handle(req As ServletRequest, resp As ServletResponse)
resp.Write("Error: El mapa de bloqueo no está inicializado.") resp.Write("Error: El mapa de bloqueo no está inicializado.")
End If End If
Return Return
Case "getconfiginfo"
resp.ContentType = "text/plain; charset=utf-8"
Dim sbInfo As StringBuilder
sbInfo.Initialize
' sbInfo.Append($"--- CONFIGURACIÓN ACTUAL DEL SERVIDOR jRDC2-Multi ($DateTime{DateTime.Now}) ---"$).Append(CRLF).Append(CRLF)
Dim allKeys As List
allKeys.Initialize
allKeys.AddAll(Main.listaDeCP) ' DB1, DB2, ...
sbInfo.Append("======================================================================").Append(CRLF)
sbInfo.Append($"=== CONFIGURACIÓN jRDC2-Multi V$1.2{Main.VERSION} (ACTIVA) ($DateTime{DateTime.Now}) ==="$).Append(CRLF)
sbInfo.Append("======================================================================").Append(CRLF).Append(CRLF)
' ***** GLOSARIO DE PARÁMETROS CONFIGURABLES *****
sbInfo.Append("### GLOSARIO DE PARÁMETROS PERMITIDOS EN CONFIG.PROPERTIES ###").Append(CRLF)
sbInfo.Append("--------------------------------------------------").Append(CRLF)
sbInfo.Append("DriverClass: Clase del driver JDBC (ej: oracle.jdbc.driver.OracleDriver).").Append(CRLF)
sbInfo.Append("JdbcUrl: URL de conexión a la base de datos (IP, puerto, servicio).").Append(CRLF)
sbInfo.Append("User/Password: Credenciales de acceso a la BD.").Append(CRLF)
sbInfo.Append("ServerPort: Puerto de escucha del servidor B4J (solo lo toma de config.properties).").Append(CRLF)
sbInfo.Append("Debug: Si es 'true', los comandos SQL se recargan en cada petición (DESHABILITADO, USAR COMANDO RELOAD).").Append(CRLF)
sbInfo.Append("parameterTolerance: Define si se recortan (1) o se rechazan (0) los parámetros SQL sobrantes a los requeridos por el query.").Append(CRLF)
sbInfo.Append("enableSQLiteLogs: Control granular. Habilita (1) o deshabilita (0) la escritura de logs en users.db para esta DB.").Append(CRLF)
sbInfo.Append("InitialPoolSize: Conexiones que el pool establece al iniciar (c3p0).").Append(CRLF)
sbInfo.Append("MinPoolSize: Mínimo de conexiones inactivas que se mantendrán.").Append(CRLF)
sbInfo.Append("MaxPoolSize: Máximo de conexiones simultáneas permitido.").Append(CRLF)
sbInfo.Append("AcquireIncrement: Número de conexiones nuevas que se adquieren en lote al necesitar más.").Append(CRLF)
sbInfo.Append("MaxIdleTime: Tiempo máximo (segundos) de inactividad antes de cerrar una conexión.").Append(CRLF)
sbInfo.Append("MaxConnectionAge: Tiempo máximo de vida (segundos) de una conexión.").Append(CRLF)
sbInfo.Append("CheckoutTimeout: Tiempo máximo de espera (milisegundos) por una conexión disponible.").Append(CRLF)
sbInfo.Append(CRLF)
For Each dbKey As String In allKeys
' --- COMIENZA EL DETALLE POR CONECTOR ---
Dim connector As RDCConnector = Main.Connectors.Get(dbKey)
sbInfo.Append("--------------------------------------------------").Append(CRLF).Append(CRLF)
sbInfo.Append($"---------------- ${dbKey} ------------------"$).Append(CRLF).Append(CRLF)
sbInfo.Append("--------------------------------------------------").Append(CRLF).Append(CRLF)
If connector.IsInitialized Then
Dim configMap As Map = connector.config
sbInfo.Append($"DriverClass: ${configMap.GetDefault("DriverClass", "N/A")}"$).Append(CRLF)
sbInfo.Append($"JdbcUrl: ${configMap.GetDefault("JdbcUrl", "N/A")}"$).Append(CRLF)
sbInfo.Append($"User: ${configMap.GetDefault("User", "N/A")}"$).Append(CRLF)
sbInfo.Append($"ServerPort: ${configMap.GetDefault("ServerPort", "N/A")}"$).Append(CRLF).Append(CRLF)
sbInfo.Append("--- CONFIGURACIÓN DEL POOL (C3P0) ---").Append(CRLF)
sbInfo.Append($"InitialPoolSize: ${configMap.GetDefault("InitialPoolSize", 3)}"$).Append(CRLF)
sbInfo.Append($"MinPoolSize: ${configMap.GetDefault("MinPoolSize", 2)}"$).Append(CRLF)
sbInfo.Append($"MaxPoolSize: ${configMap.GetDefault("MaxPoolSize", 5)}"$).Append(CRLF)
sbInfo.Append($"AcquireIncrement: ${configMap.GetDefault("AcquireIncrement", 5)}"$).Append(CRLF)
sbInfo.Append($"MaxIdleTime (s): ${configMap.GetDefault("MaxIdleTime", 300)}"$).Append(CRLF)
sbInfo.Append($"MaxConnectionAge (s): ${configMap.GetDefault("MaxConnectionAge", 900)}"$).Append(CRLF)
sbInfo.Append($"CheckoutTimeout (ms): ${configMap.GetDefault("CheckoutTimeout", 60000)}"$).Append(CRLF).Append(CRLF)
sbInfo.Append("--- COMPORTAMIENTO ---").Append(CRLF)
sbInfo.Append($"Debug (Recarga Queries - DESHABILITADO): ${configMap.GetDefault("Debug", "false")}"$).Append(CRLF)
' Lectura explícita de las nuevas propiedades, asegurando un Int.
Dim tolerance As Int = configMap.GetDefault("parameterTolerance", 0).As(Int)
sbInfo.Append($"ParameterTolerance: ${tolerance} (0=Estricto, 1=Habilitado)"$).Append(CRLF)
Dim logsEnabled As Int = configMap.GetDefault("enableSQLiteLogs", 1).As(Int)
sbInfo.Append($"EnableSQLiteLogs: ${logsEnabled} (0=Deshabilitado, 1=Habilitado)"$).Append(CRLF)
sbInfo.Append(CRLF)
Else
sbInfo.Append($"ERROR: Conector ${dbKey} no inicializado o falló al inicio."$).Append(CRLF).Append(CRLF)
End If
Next
resp.Write(sbInfo.ToString)
Return
Case Else Case Else
resp.ContentType = "text/plain; charset=utf-8" resp.ContentType = "text/plain; charset=utf-8"
resp.SendError(404, $"Comando desconocido: '{Command}'"$) resp.SendError(404, $"Comando desconocido: '{Command}'"$)

View File

@@ -114,7 +114,7 @@ Public Sub Initialize(DB As String)
' PASO C: Almacenamos el mapa completo (estático + dinámico inicial) en el cache global. ' PASO C: Almacenamos el mapa completo (estático + dinámico inicial) en el cache global.
Main.LatestPoolStats.Put(dbKeyToStore, initialPoolStats) Main.LatestPoolStats.Put(dbKeyToStore, initialPoolStats)
Log(Main.LatestPoolStats) ' Log(Main.LatestPoolStats)
' com.mchange.v2.c3p0.ComboPooledDataSource [ ' com.mchange.v2.c3p0.ComboPooledDataSource [
' acquireIncrement -> 3, ' acquireIncrement -> 3,

131
SSEHandler.bas Normal file
View File

@@ -0,0 +1,131 @@
B4J=true
Group=Default Group
ModulesStructureVersion=1
Type=Class
Version=10.3
@EndOfDesignText@
' Handler class: StatsSSEHandler.b4j
' Gestiona y transmite en tiempo real las estadísticas del pool de conexiones vía Server-Sent Events (SSE).
' Opera en modo Singleton: una única instancia maneja todas las conexiones.
Sub Class_Globals
' Almacena de forma centralizada a todos los clientes (navegadores) conectados.
' La clave es un ID único y el valor es el canal de comunicación (OutputStream).
Private Connections As Map
' Timer #1 ("El Vigilante"): Se encarga de detectar y eliminar conexiones muertas.
Private RemoveTimer As Timer
' Timer #2 ("El Informante"): Se encarga de recolectar y enviar los datos de estadísticas.
Dim StatsTimer As Timer
Dim const UPDATE_INTERVAL_MS As Long = 2000 ' Intervalo de envío de estadísticas: 2 segundos.
End Sub
' Se ejecuta UNA SOLA VEZ cuando el servidor arranca, gracias al modo Singleton.
Public Sub Initialize
Log("Stats SSE Handler Initialized (Singleton Mode)")
' Crea el mapa de conexiones, asegurando que sea seguro para el manejo de múltiples hilos.
Connections = Main.srvr.CreateThreadSafeMap
' Configura y activa el timer para la limpieza de conexiones cada 5 segundos.
' NOTA: El EventName "RemoveTimer" debe coincidir con el nombre de la subrutina del tick.
RemoveTimer.Initialize("RemoveTimer", 5000)
RemoveTimer.Enabled = True
' Configura y activa el timer para el envío de estadísticas.
' NOTA: El EventName "StatsTimer" debe coincidir con el nombre de la subrutina del tick.
StatsTimer.Initialize("StatsTimer", UPDATE_INTERVAL_MS)
StatsTimer.Enabled = True
End Sub
' Es el punto de entrada principal. Atiende todas las peticiones HTTP dirigidas a este handler.
Sub Handle(req As ServletRequest, resp As ServletResponse)
Log($"StatsTimerinicializado: ${StatsTimer.IsInitialized}, StatsTimer habilitado: ${StatsTimer.Enabled}"$)
StatsTimer.Initialize("StatsTimer", 2000)
StatsTimer.Enabled = True
' Filtro de seguridad: verifica si el usuario tiene una sesión autorizada.
If req.GetSession.GetAttribute2("user_is_authorized", False) = False Then
resp.SendRedirect("/login")
Return
End If
' Procesa únicamente las peticiones GET, que son las que usan los navegadores para iniciar una conexión SSE.
If req.Method = "GET" Then
' Mantiene la petición activa de forma asíncrona para poder enviar datos en el futuro.
Dim reqJO As JavaObject = req
reqJO.RunMethod("startAsync", Null)
' Registra al nuevo cliente para que empiece a recibir eventos.
SSE.AddTarget("stats", resp)
Else
' Rechaza cualquier otro método HTTP (POST, PUT, etc.) con un error.
resp.SendError(405, "Method Not Allowed")
End If
End Sub
' --- LÓGICA DE LOS TIMERS ---
' Evento del Timer #1 ("El Vigilante"): se dispara cada 5 segundos.
Sub RemoveTimer_Tick
' Log("REMOVETIMER TICK")
' Optimización: si no hay nadie conectado, no hace nada.
If Connections.Size = 0 Then Return
' Itera sobre todos los clientes para verificar si siguen activos.
For Each key As String In Connections.Keys
Try
' Envía un evento "ping" silencioso. Si la conexión está viva, no pasa nada.
SSE.SendMessage(Connections.Get(key), "ping", "", 0, "")
Catch
' Si el envío falla, la conexión está muerta. Se procede a la limpieza.
Log("######################")
Log("## Removing (timer cleanup): " & key)
Log("######################")
Connections.Remove(key)
End Try
Next
End Sub
' Evento del Timer #2 ("El Informante"): se dispara cada 2 segundos.
public Sub StatsTimer_Tick
' Optimización: si no hay nadie conectado, no realiza el trabajo pesado.
If Connections.Size = 0 Then Return
Try
' Prepara un mapa para almacenar las estadísticas recolectadas.
Dim allPoolStats As Map
allPoolStats.Initialize
' Bloquea el acceso a los conectores para leer sus datos de forma segura.
Main.MainConnectorsLock.RunMethod("lock", Null)
For Each dbKey As String In Main.listaDeCP
Dim connector As RDCConnector
If Main.Connectors.ContainsKey(dbKey) Then
connector = Main.Connectors.Get(dbKey)
If connector.IsInitialized Then
allPoolStats.Put(dbKey, connector.GetPoolStats)
Else
allPoolStats.Put(dbKey, CreateMap("Error": "Conector no inicializado"))
End If
End If
Next
' Libera el bloqueo para que otras partes del programa puedan usar los conectores.
Main.MainConnectorsLock.RunMethod("unlock", Null)
' Convierte el mapa de estadísticas a un formato de texto JSON.
Dim j As JSONGenerator
j.Initialize(allPoolStats)
Dim jsonStats As String = j.ToString
' Llama al "locutor" para enviar el JSON a todos los clientes conectados.
SSE.Broadcast("stats", "stats_update", jsonStats, 0)
Catch
' Captura y registra cualquier error que ocurra durante la recolección de datos.
Log($"[SSE] Error CRÍTICO durante la adquisición de estadísticas: ${LastException.Message}"$)
End Try
End Sub

View File

@@ -1,19 +1,15 @@
AppType=StandardJava AppType=StandardJava
Build1=Default,b4j.JRDCMulti Build1=Default,b4j.JRDCMulti
File1=config.DB2.properties File1=config.DB2.properties
File10=start2.bat
File11=stop.bat
File2=config.DB3.properties File2=config.DB3.properties
File3=config.DB4.properties File3=config.DB4.properties
File4=config.properties File4=config.properties
File5=login.html File5=reiniciaProcesoBow.bat
File6=manager.html File6=reiniciaProcesoPM2.bat
File7=reiniciaProcesoBow.bat File7=start.bat
File8=reiniciaProcesoPM2.bat File8=start2.bat
File9=start.bat File9=stop.bat
FileGroup1=Default Group FileGroup1=Default Group
FileGroup10=Default Group
FileGroup11=Default Group
FileGroup2=Default Group FileGroup2=Default Group
FileGroup3=Default Group FileGroup3=Default Group
FileGroup4=Default Group FileGroup4=Default Group
@@ -23,15 +19,15 @@ FileGroup7=Default Group
FileGroup8=Default Group FileGroup8=Default Group
FileGroup9=Default Group FileGroup9=Default Group
Group=Default Group Group=Default Group
Library1=byteconverter Library1=bcrypt
Library2=javaobject Library2=byteconverter
Library3=jcore Library3=javaobject
Library4=jrandomaccessfile Library4=jcore
Library5=jserver Library5=jrandomaccessfile
Library6=jshell Library6=jserver
Library7=json Library7=jshell
Library8=jsql Library8=json
Library9=bcrypt Library9=jsql
Module1=Cambios Module1=Cambios
Module10=Manager Module10=Manager
Module11=Manager0 Module11=Manager0
@@ -49,7 +45,7 @@ Module6=faviconHandler
Module7=GlobalParameters Module7=GlobalParameters
Module8=LoginHandler Module8=LoginHandler
Module9=LogoutHandler Module9=LogoutHandler
NumberOfFiles=11 NumberOfFiles=9
NumberOfLibraries=9 NumberOfLibraries=9
NumberOfModules=17 NumberOfModules=17
Version=10.3 Version=10.3
@@ -60,7 +56,7 @@ Version=10.3
#CommandLineArgs: #CommandLineArgs:
#MergeLibraries: True #MergeLibraries: True
' VERSION 5.09.18 ' VERSION 5.09.19
'########################################################################################################### '###########################################################################################################
'###################### PULL ############################################################# '###################### PULL #############################################################
'Ctrl + click ide://run?file=%WINDIR%\System32\cmd.exe&Args=/c&Args=git&Args=pull 'Ctrl + click ide://run?file=%WINDIR%\System32\cmd.exe&Args=/c&Args=git&Args=pull
@@ -148,7 +144,22 @@ Sub AppStart (Args() As String)
#End If #End If
' --- Subrutina principal que se ejecuta al iniciar la aplicación --- ' --- Subrutina principal que se ejecuta al iniciar la aplicación ---
' SSE.Initialize ' La subcarpeta es "www"
CopiarRecursoSiNoExiste("manager.html", "www")
CopiarRecursoSiNoExiste("login.html", "www")
' --- Copiar los archivos .bat de la raíz ---
' La subcarpeta es "" (vacía) porque están en la raíz de "Files"
CopiarRecursoSiNoExiste("config.properties", "")
CopiarRecursoSiNoExiste("config.DB2.properties", "")
CopiarRecursoSiNoExiste("config.DB3.properties", "")
CopiarRecursoSiNoExiste("start.bat", "")
CopiarRecursoSiNoExiste("start2.bat", "")
CopiarRecursoSiNoExiste("stop.bat", "")
CopiarRecursoSiNoExiste("reiniciaProcesoBow.bat", "")
CopiarRecursoSiNoExiste("reiniciaProcesoPM2.bat", "")
'
' Log("Verificación de archivos completada.")
bc.Initialize("BC") bc.Initialize("BC")
QueryLogCache.Initialize QueryLogCache.Initialize
@@ -343,7 +354,6 @@ Sub InitializeSQLiteDatabase
If File.Exists(File.DirApp, dbFileName) = False Then If File.Exists(File.DirApp, dbFileName) = False Then
Log("Creando nueva base de datos de usuarios: " & dbFileName) Log("Creando nueva base de datos de usuarios: " & dbFileName)
SQL1.InitializeSQLite(File.DirApp, dbFileName, True) SQL1.InitializeSQLite(File.DirApp, dbFileName, True)
' Crear tabla 'users' ' Crear tabla 'users'
Dim createUserTable As String = "CREATE TABLE users (username TEXT PRIMARY KEY, password_hash TEXT NOT NULL)" Dim createUserTable As String = "CREATE TABLE users (username TEXT PRIMARY KEY, password_hash TEXT NOT NULL)"
SQL1.ExecNonQuery(createUserTable) SQL1.ExecNonQuery(createUserTable)
@@ -353,6 +363,9 @@ Sub InitializeSQLiteDatabase
Dim createQueryLogsTable As String = "CREATE TABLE query_logs (id INTEGER PRIMARY KEY AUTOINCREMENT, query_name TEXT, duration_ms INTEGER, timestamp INTEGER, db_key TEXT, client_ip TEXT, busy_connections INTEGER, handler_active_requests INTEGER)" Dim createQueryLogsTable As String = "CREATE TABLE query_logs (id INTEGER PRIMARY KEY AUTOINCREMENT, query_name TEXT, duration_ms INTEGER, timestamp INTEGER, db_key TEXT, client_ip TEXT, busy_connections INTEGER, handler_active_requests INTEGER)"
SQL1.ExecNonQuery(createQueryLogsTable) SQL1.ExecNonQuery(createQueryLogsTable)
SQL1.ExecNonQuery("PRAGMA journal_mode=WAL;")
SQL1.ExecNonQuery("PRAGMA synchronous=NORMAL;")
' Insertar usuario por defecto ' Insertar usuario por defecto
Dim defaultUser As String = "admin" Dim defaultUser As String = "admin"
Dim defaultPass As String = "12345" Dim defaultPass As String = "12345"
@@ -365,9 +378,22 @@ Sub InitializeSQLiteDatabase
Dim createErrorsTable As String = "CREATE TABLE errores (id INTEGER PRIMARY KEY AUTOINCREMENT, timestamp INTEGER, type TEXT, source TEXT, message TEXT, db_key TEXT, command_name TEXT, client_ip TEXT)" Dim createErrorsTable As String = "CREATE TABLE errores (id INTEGER PRIMARY KEY AUTOINCREMENT, timestamp INTEGER, type TEXT, source TEXT, message TEXT, db_key TEXT, command_name TEXT, client_ip TEXT)"
SQL1.ExecNonQuery(createErrorsTable) SQL1.ExecNonQuery(createErrorsTable)
If logger Then Log("Creando índices de rendimiento en tablas de logs.")
' Índice en timestamp para limpieza rápida (DELETE/ORDER BY) en query_logs
SQL1.ExecNonQuery("CREATE INDEX idx_query_timestamp ON query_logs(timestamp)")
' Índice en duration_ms para la consulta 'slowqueries' (ORDER BY)
SQL1.ExecNonQuery("CREATE INDEX idx_query_duration ON query_logs(duration_ms)")
' Índice en timestamp para limpieza rápida de la tabla de errores
SQL1.ExecNonQuery("CREATE INDEX idx_error_timestamp ON errores(timestamp)")
Else Else
SQL1.InitializeSQLite(File.DirApp, dbFileName, True) SQL1.InitializeSQLite(File.DirApp, dbFileName, True)
Log("Base de datos de usuarios cargada.") Log("Base de datos de usuarios cargada.")
SQL1.ExecNonQuery("PRAGMA journal_mode=WAL;")
SQL1.ExecNonQuery("PRAGMA synchronous=NORMAL;")
' >>> INICIO: Lógica de migración (ALTER TABLE) si la DB ya existía <<< ' >>> INICIO: Lógica de migración (ALTER TABLE) si la DB ya existía <<<
If logger Then Log("Verificando y migrando tabla 'query_logs' si es necesario.") If logger Then Log("Verificando y migrando tabla 'query_logs' si es necesario.")
@@ -671,7 +697,6 @@ Public Sub WriteErrorLogsBatch
End Sub End Sub
' --- Borra los registros más antiguos de la tabla 'query_logs' y hace VACUUM. --- ' --- Borra los registros más antiguos de la tabla 'query_logs' y hace VACUUM. ---
' ¡MODIFICADA PARA USAR FILTRADO GLOBAL!
Sub borraArribaDe15000Logs 'ignore Sub borraArribaDe15000Logs 'ignore
If IsAnySQLiteLoggingEnabled Then ' Solo ejecutar si al menos una DB requiere logs. If IsAnySQLiteLoggingEnabled Then ' Solo ejecutar si al menos una DB requiere logs.
@@ -691,3 +716,64 @@ Sub borraArribaDe15000Logs 'ignore
If logger Then Log("AVISO: Tarea de limpieza de logs omitida. El logging global de SQLite está deshabilitado.") If logger Then Log("AVISO: Tarea de limpieza de logs omitida. El logging global de SQLite está deshabilitado.")
End If End If
End Sub End Sub
'Copiamos recursos del jar al directorio de la app
Sub CopiarRecursoSiNoExiste(NombreArchivo As String, SubCarpeta As String)
Dim DirDestino As String = File.Combine(File.DirApp, SubCarpeta)
If SubCarpeta <> "" And File.Exists(DirDestino, "") = False Then
File.MakeDir(DirDestino, "")
End If
Dim ArchivoDestino As String = File.Combine(DirDestino, NombreArchivo)
If File.Exists(DirDestino, NombreArchivo) = False Then
Dim RutaRecurso As String
If SubCarpeta <> "" Then
RutaRecurso = "Files/" & SubCarpeta & "/" & NombreArchivo
Else
RutaRecurso = "Files/" & NombreArchivo
End If
Dim classLoader As JavaObject = GetThreadContextClassLoader
Dim InStream As InputStream = classLoader.RunMethod("getResourceAsStream", Array(RutaRecurso))
If InStream.IsInitialized Then
Log($"Copiando recurso: '${RutaRecurso}'..."$)
' Llamamos a nuestra propia función de copiado manual
Dim OutStream As OutputStream = File.OpenOutput(DirDestino, NombreArchivo, False)
CopiarStreamManualmente(InStream, OutStream)
Log($"'${ArchivoDestino}' copiado correctamente."$)
Else
Log($"ERROR: No se pudo encontrar el recurso con la ruta interna: '${RutaRecurso}'"$)
End If
End If
End Sub
' No depende de ninguna librería extraña.
Sub CopiarStreamManualmente (InStream As InputStream, OutStream As OutputStream)
Try
Dim buffer(1024) As Byte
Dim len As Int
len = InStream.ReadBytes(buffer, 0, buffer.Length)
Do While len > 0
OutStream.WriteBytes(buffer, 0, len)
len = InStream.ReadBytes(buffer, 0, buffer.Length)
Loop
Catch
LogError(LastException)
End Try
InStream.Close
OutStream.Close
End Sub
' Función ayudante para obtener el Class Loader correcto.
Sub GetThreadContextClassLoader As JavaObject
Dim thread As JavaObject
thread = thread.InitializeStatic("java.lang.Thread").RunMethod("currentThread", Null)
Return thread.RunMethod("getContextClassLoader", Null)
End Sub

View File

@@ -34,7 +34,7 @@ ModuleBreakpoints6=
ModuleBreakpoints7= ModuleBreakpoints7=
ModuleBreakpoints8= ModuleBreakpoints8=
ModuleBreakpoints9= ModuleBreakpoints9=
ModuleClosedNodes0= ModuleClosedNodes0=5,6,7,8,9,10,12,13
ModuleClosedNodes1= ModuleClosedNodes1=
ModuleClosedNodes10= ModuleClosedNodes10=
ModuleClosedNodes11= ModuleClosedNodes11=
@@ -52,6 +52,6 @@ ModuleClosedNodes6=
ModuleClosedNodes7= ModuleClosedNodes7=
ModuleClosedNodes8= ModuleClosedNodes8=
ModuleClosedNodes9= ModuleClosedNodes9=
NavigationStack=SSE,GetGUID,115,0,SSE,RemoveTimer_Tick,124,5,RDCConnector,GetPoolStats,255,0,Manager,Class_Globals,10,0,Manager,Initialize,24,0,Manager,Handle,149,0,SSEHandler,RemoveTimer_Tick,66,0,SSEHandler,Class_Globals,16,0,Main,AppStart,265,0,Cambios,Process_Globals,25,0 NavigationStack=Main,CopiarStreamManualmente,714,0,Main,GetThreadContextClassLoader,716,0,Main,borraArribaDe15000Logs,653,0,Main,WriteErrorLogsBatch,631,0,Main,InitializeSQLiteDatabase,332,0,Main,CopiarRecursoSiNoExiste,698,6,RDCConnector,Initialize,100,0,Manager,Handle,301,0,Cambios,Process_Globals,20,1,Main,AppStart,88,4
SelectedBuild=0 SelectedBuild=0
VisibleModules=3,4,14,1,10,15,16,17,13 VisibleModules=3,4,14,1,10,15,16,17,13