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
-
-
-
-
-
-
\ 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