From 8b876e5095748d9516a69c0df6143381d5536196 Mon Sep 17 00:00:00 2001 From: Jose Alberto Guerra Ugalde Date: Sat, 27 Sep 2025 20:33:44 -0600 Subject: [PATCH] =?UTF-8?q?-=20VERSION=205.09.19=20-=20feat(sqlite):=20Imp?= =?UTF-8?q?lementa=20optimizaci=C3=B3n=20de=20SQLite=20(WAL=20e=20=C3=8Dnd?= =?UTF-8?q?ices)=20-=20fix(manager):=20Extiende=20el=20comando=20'test'=20?= =?UTF-8?q?para=20verificar=20todos=20los=20pools=20de=20conexi=C3=B3n=20c?= =?UTF-8?q?onfigurados.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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. --- Cambios.bas | 16 +++++ DBHandlerB4X.bas | 1 - Files/config.properties | 2 +- Files/login.html | 21 ------ Files/www/manager.html | 1 + Manager.bas | 150 ++++++++++++++++++++++++++++++++++------ RDCConnector.bas | 2 +- SSEHandler.bas | 131 +++++++++++++++++++++++++++++++++++ jRDC_Multi.b4j | 134 ++++++++++++++++++++++++++++------- jRDC_Multi.b4j.meta | 4 +- 10 files changed, 391 insertions(+), 71 deletions(-) delete mode 100644 Files/login.html create mode 100644 SSEHandler.bas diff --git a/Cambios.bas b/Cambios.bas index 5adff58..37ec458 100644 --- a/Cambios.bas +++ b/Cambios.bas @@ -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 ' 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 ' - feat(manager): Implementa recarga granular (Hot-Swap). ' - Actualiza manager.html para solicitar la DB Key a recargar (ej: DB2). diff --git a/DBHandlerB4X.bas b/DBHandlerB4X.bas index 3df853a..f03fbf6 100644 --- a/DBHandlerB4X.bas +++ b/DBHandlerB4X.bas @@ -522,7 +522,6 @@ Private Sub ExecuteBatch(DB As String, con As SQL, in As InputStream, resp As Se WriteObject(Main.VERSION, out) WriteObject("batch", out) WriteInt(res.Length, out) -' Log(affectedCounts.Size) For Each r As Int In affectedCounts WriteInt(r, out) Next diff --git a/Files/config.properties b/Files/config.properties index aace91a..3d468ee 100644 --- a/Files/config.properties +++ b/Files/config.properties @@ -25,7 +25,7 @@ AcquireIncrement=1 MaxConnectionAge=60 # 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). # Por defecto, si no se especifica o el valor es diferente de 1, la tolerancia estará DESHABILITADA (modo estricto). parameterTolerance=1 diff --git a/Files/login.html b/Files/login.html deleted file mode 100644 index 60a6738..0000000 --- a/Files/login.html +++ /dev/null @@ -1,21 +0,0 @@ - - - - - Login jRDC Server - - - -
-

Acceso al Manager

- - - -
- - \ No newline at end of file diff --git a/Files/www/manager.html b/Files/www/manager.html index ab8a90f..979a3e4 100644 --- a/Files/www/manager.html +++ b/Files/www/manager.html @@ -135,6 +135,7 @@ Estadísticas Pool Reiniciar (pm2) Revive Bow + Info
Estado de Estadísticas en Tiempo Real: diff --git a/Manager.bas b/Manager.bas index 4a497ab..49d4cf1 100644 --- a/Manager.bas +++ b/Manager.bas @@ -259,7 +259,6 @@ Sub Handle(req As ServletRequest, resp As ServletResponse) Exit End If Next - If Main.IsAnySQLiteLoggingEnabled Then Main.timerLogs.Enabled = True 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 sbTemp.Append($" -> Timer de limpieza de logs DESHABILITADO (estado global: DESHABILITADO)."$).Append(" " & CRLF) End If - sbTemp.Append($"¡Recarga de configuración completada con éxito!"$).Append(" " & CRLF) - Else - ' Si falló, restauramos el estado del timer anterior. If oldTimerState Then Main.timerLogs.Enabled = True @@ -279,31 +275,65 @@ Sub Handle(req As ServletRequest, resp As ServletResponse) End If sbTemp.Append($"¡ERROR: La recarga de configuración falló! Los conectores antiguos siguen activos."$).Append(" " & CRLF) End If - resp.Write(sbTemp.ToString) Return - Case "test" resp.ContentType = "text/plain; charset=utf-8" Dim sb As StringBuilder sb.Initialize + sb.Append("--- INICIANDO PRUEBA DE CONECTIVIDAD A TODOS LOS POOLS CONFIGURADOS ---").Append(CRLF).Append(CRLF) + + ' Iteramos sobre la lista de DB Keys cargadas al inicio (DB1, DB2, etc.) + For Each dbKey As String In Main.listaDeCP + Dim success As Boolean = False + Dim errorMsg As String = "" + Dim con As SQL ' Conexión para la prueba + + Try + ' 1. Obtener el RDCConnector para esta DBKey + Dim connector As RDCConnector = Main.Connectors.Get(dbKey) - Try - Dim con As SQL = Main.Connectors.Get("DB1").As(RDCConnector).GetConnection("") - sb.Append("Connection successful." & CRLF & CRLF) - Dim 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"$ & CRLF) - Next - con.Close - resp.Write(sb.ToString) - Catch - resp.Write("Error fetching connection: " & LastException.Message) - End Try + If connector.IsInitialized = False Then + errorMsg = "Conector no inicializado (revisa logs de AppStart)" + Else + ' 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 - Case "rsx", "rpm2", "revivebow", "restartserver" resp.ContentType = "text/plain; charset=utf-8" 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.") End If 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 resp.ContentType = "text/plain; charset=utf-8" resp.SendError(404, $"Comando desconocido: '{Command}'"$) diff --git a/RDCConnector.bas b/RDCConnector.bas index 9cde619..73aae56 100644 --- a/RDCConnector.bas +++ b/RDCConnector.bas @@ -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. Main.LatestPoolStats.Put(dbKeyToStore, initialPoolStats) - Log(Main.LatestPoolStats) +' Log(Main.LatestPoolStats) ' com.mchange.v2.c3p0.ComboPooledDataSource [ ' acquireIncrement -> 3, diff --git a/SSEHandler.bas b/SSEHandler.bas new file mode 100644 index 0000000..564cd1f --- /dev/null +++ b/SSEHandler.bas @@ -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 + diff --git a/jRDC_Multi.b4j b/jRDC_Multi.b4j index dff9663..17d0162 100644 --- a/jRDC_Multi.b4j +++ b/jRDC_Multi.b4j @@ -1,19 +1,15 @@ AppType=StandardJava Build1=Default,b4j.JRDCMulti File1=config.DB2.properties -File10=start2.bat -File11=stop.bat File2=config.DB3.properties File3=config.DB4.properties File4=config.properties -File5=login.html -File6=manager.html -File7=reiniciaProcesoBow.bat -File8=reiniciaProcesoPM2.bat -File9=start.bat +File5=reiniciaProcesoBow.bat +File6=reiniciaProcesoPM2.bat +File7=start.bat +File8=start2.bat +File9=stop.bat FileGroup1=Default Group -FileGroup10=Default Group -FileGroup11=Default Group FileGroup2=Default Group FileGroup3=Default Group FileGroup4=Default Group @@ -23,15 +19,15 @@ FileGroup7=Default Group FileGroup8=Default Group FileGroup9=Default Group Group=Default Group -Library1=byteconverter -Library2=javaobject -Library3=jcore -Library4=jrandomaccessfile -Library5=jserver -Library6=jshell -Library7=json -Library8=jsql -Library9=bcrypt +Library1=bcrypt +Library2=byteconverter +Library3=javaobject +Library4=jcore +Library5=jrandomaccessfile +Library6=jserver +Library7=jshell +Library8=json +Library9=jsql Module1=Cambios Module10=Manager Module11=Manager0 @@ -49,7 +45,7 @@ Module6=faviconHandler Module7=GlobalParameters Module8=LoginHandler Module9=LogoutHandler -NumberOfFiles=11 +NumberOfFiles=9 NumberOfLibraries=9 NumberOfModules=17 Version=10.3 @@ -60,7 +56,7 @@ Version=10.3 #CommandLineArgs: #MergeLibraries: True -' VERSION 5.09.18 +' VERSION 5.09.19 '########################################################################################################### '###################### 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 ' --- 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") QueryLogCache.Initialize @@ -343,7 +354,6 @@ Sub InitializeSQLiteDatabase If File.Exists(File.DirApp, dbFileName) = False Then Log("Creando nueva base de datos de usuarios: " & dbFileName) SQL1.InitializeSQLite(File.DirApp, dbFileName, True) - ' Crear tabla 'users' Dim createUserTable As String = "CREATE TABLE users (username TEXT PRIMARY KEY, password_hash TEXT NOT NULL)" 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)" SQL1.ExecNonQuery(createQueryLogsTable) + SQL1.ExecNonQuery("PRAGMA journal_mode=WAL;") + SQL1.ExecNonQuery("PRAGMA synchronous=NORMAL;") + ' Insertar usuario por defecto Dim defaultUser As String = "admin" Dim defaultPass As String = "12345" @@ -364,10 +377,23 @@ Sub InitializeSQLiteDatabase Log("Creando tabla 'errores' para registrar eventos.") 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) + + 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 SQL1.InitializeSQLite(File.DirApp, dbFileName, True) 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 <<< If logger Then Log("Verificando y migrando tabla 'query_logs' si es necesario.") @@ -671,7 +697,6 @@ Public Sub WriteErrorLogsBatch End Sub ' --- Borra los registros más antiguos de la tabla 'query_logs' y hace VACUUM. --- -' ¡MODIFICADA PARA USAR FILTRADO GLOBAL! Sub borraArribaDe15000Logs 'ignore If IsAnySQLiteLoggingEnabled Then ' Solo ejecutar si al menos una DB requiere logs. @@ -690,4 +715,65 @@ Sub borraArribaDe15000Logs 'ignore ' Si IsAnySQLiteLoggingEnabled es False, el Timer no debería estar activo. If logger Then Log("AVISO: Tarea de limpieza de logs omitida. El logging global de SQLite está deshabilitado.") End If -End Sub \ No newline at end of file +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 diff --git a/jRDC_Multi.b4j.meta b/jRDC_Multi.b4j.meta index f2cf27d..3d9fb04 100644 --- a/jRDC_Multi.b4j.meta +++ b/jRDC_Multi.b4j.meta @@ -34,7 +34,7 @@ ModuleBreakpoints6= ModuleBreakpoints7= ModuleBreakpoints8= ModuleBreakpoints9= -ModuleClosedNodes0= +ModuleClosedNodes0=5,6,7,8,9,10,12,13 ModuleClosedNodes1= ModuleClosedNodes10= ModuleClosedNodes11= @@ -52,6 +52,6 @@ ModuleClosedNodes6= ModuleClosedNodes7= ModuleClosedNodes8= 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 VisibleModules=3,4,14,1,10,15,16,17,13