- VERSION 5.09.16

- feat: Implementa control de logs de SQLite granular por DBKey y corrige la concurrencia del Timer en Hot-Swap.
- Este commit introduce una mejora crucial en el rendimiento y la flexibilidad del servidor al permitir el control detallado del registro de logs en SQLite (users.db) por cada base de datos configurada (DB1, DB2, etc.).
- Cambios Principales y Beneficios:
1. Control Granular de Logs: Se reemplazó el flag de control global de logs por un mapa (SQLiteLoggingStatusByDB), permitiendo al administrador deshabilitar el costoso proceso de escritura de query_logs y errores para bases de datos específicas mediante la propiedad enableSQLiteLogs en sus archivos .properties correspondientes.
2. Estabilización del Timer y Hot-Swap:
    ◦ Se corrigió un problema de concurrencia y estado asegurando que timerLogs se inicialice incondicionalmente, resolviendo el error IllegalStateException: Interval must be larger than 0 que ocurría durante el reload.
    ◦ El Timer de limpieza (borraArribaDe15000Logs y VACUUM) ahora se activa solo si al menos una base de datos tiene el logging habilitado (IsAnySQLiteLoggingEnabled), minimizando el overhead de E/S de disco cuando los logs no se requieren.
3. Recarga Dinámica de Estado: El comando manager?command=reload ahora lee la configuración enableSQLiteLogs de todos los conectores nuevos y actualiza atómicamente el estado global de logs, aplicando los cambios sin requerir un reinicio del servidor.
This commit is contained in:
2025-09-19 12:31:03 -06:00
parent 3b352bb105
commit dbfafbf9ac
8 changed files with 347 additions and 242 deletions

View File

@@ -13,9 +13,28 @@ Sub Process_Globals
' - VERSION X.XX.XX (cambios a implementar) ' - VERSION X.XX.XX (cambios a implementar)
' - Agregar que se puedan usar cualquier cantidad de archivos config.properties ' - Agregar que se puedan usar cualquier cantidad de archivos config.properties
' - Agregar que se pueda recargar solo un archivo de configuracion o todos a la vez. ' - Agregar que se pueda recargar solo un archivo de configuracion o todos a la vez.
' - Agregar que el "Test" del manager revise (con el query de Jorge) cuantas conexiones hay actualmente activas, ' - Agregar una forma de probar con carga el servidor.
' - o si no en el test, un nuevo handler, talvez "Conexiones". ' - Agregar algun tipo de autenticación, posiblemente con tokens ... se podria poner
' - Agregar una forma de probar con carga el servidor ' en el config.properties un token de conexion y solo las peticiones que lo incluyan sean atendidas, o
' guardar los tokens en la BD sqlite.
' - Si se implementan los tokens, se podrian ligar los tokens con el "*" de CORS, y los tokens definirian
' los dominios permitidos.
' - Ej: token:1224abcd5678fghi, dominio:"keymon.net"
' - Ej: token:4321abcd8765fghi, dominio:"*"
' - Que los logs, en lugar de guardar de uno en uno en la BD Sqlte, se guarden en memoria, se junten ... por ejemplo 100 y ya que haya 100, se guarden
' en una solo query a la BD Sqlite.
' - 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.
' - Versión: 5.09.16
' - feat: Implementa control de logs de SQLite granular por DBKey y corrige la concurrencia del Timer en Hot-Swap.
' - Este commit introduce una mejora crucial en el rendimiento y la flexibilidad del servidor al permitir el control detallado del registro de logs en SQLite (users.db) por cada base de datos configurada (DB1, DB2, etc.).
' - Cambios Principales y Beneficios:
' 1. Control Granular de Logs: Se reemplazó el flag de control global de logs por un mapa (SQLiteLoggingStatusByDB), permitiendo al administrador deshabilitar el costoso proceso de escritura de query_logs y errores para bases de datos específicas mediante la propiedad enableSQLiteLogs en sus archivos .properties correspondientes.
' 2. Estabilización del Timer y Hot-Swap:
' ◦ Se corrigió un problema de concurrencia y estado asegurando que timerLogs se inicialice incondicionalmente, resolviendo el error IllegalStateException: Interval must be larger than 0 que ocurría durante el reload.
' ◦ El Timer de limpieza (borraArribaDe15000Logs y VACUUM) ahora se activa solo si al menos una base de datos tiene el logging habilitado (IsAnySQLiteLoggingEnabled), minimizando el overhead de E/S de disco cuando los logs no se requieren.
' 3. Recarga Dinámica de Estado: El comando manager?command=reload ahora lee la configuración enableSQLiteLogs de todos los conectores nuevos y actualiza atómicamente el estado global de logs, aplicando los cambios Sin requerir un reinicio del servidor.
' '
' - VERSION 5.09.16 ' - VERSION 5.09.16
' feat: Implementa tolerancia de parámetros configurable y mejora estabilidad general del servidor. ' feat: Implementa tolerancia de parámetros configurable y mejora estabilidad general del servidor.

View File

@@ -76,7 +76,7 @@ Sub Handle(req As ServletRequest, resp As ServletResponse)
End If End If
' === FIN DE LA LÓGICA DINÁMICA === ' === FIN DE LA LÓGICA DINÁMICA ===
Log("********************* " & dbKey & " ********************") ' Log de depuración para identificar la base de datos. ' Log("********************* " & dbKey & " ********************") ' Log de depuración para identificar la base de datos.
Dim start As Long = DateTime.Now ' Registra el tiempo de inicio de la petición para calcular la duración. Dim start As Long = DateTime.Now ' Registra el tiempo de inicio de la petición para calcular la duración.
@@ -117,7 +117,7 @@ Sub Handle(req As ServletRequest, resp As ServletResponse)
End If End If
' <<<< ¡FIN DE CAPTURA! >>>> ' <<<< ¡FIN DE CAPTURA! >>>>
Log("Metodo: " & method) ' Log de depuración para identificar el método de la petición. ' Log("Metodo: " & method) ' Log de depuración para identificar el método de la petición.
' --- Lógica para ejecutar diferentes tipos de comandos basados en el parámetro 'method' --- ' --- Lógica para ejecutar diferentes tipos de comandos basados en el parámetro 'method' ---
If method = "query2" Then If method = "query2" Then
@@ -177,7 +177,7 @@ Sub Handle(req As ServletRequest, resp As ServletResponse)
' Este bloque se asegura de que, independientemente de cómo termine la petición (éxito o error), ' Este bloque se asegura de que, independientemente de cómo termine la petición (éxito o error),
' la duración se calcule y se llamen las subrutinas de limpieza y logging. ' la duración se calcule y se llamen las subrutinas de limpieza y logging.
duration = DateTime.Now - start ' Calcula la duración total de la petición. duration = DateTime.Now - start ' Calcula la duración total de la petición.
Log($"Command: ${q}, took: ${duration}ms, client=${req.RemoteAddress}"$) ' Logea el comando y la duración. Log($"${dbKey} - Command: ${q}, took: ${duration}ms, client=${req.RemoteAddress}"$) ' Logea el comando y la duración.
' Llama a la subrutina centralizada para registrar el rendimiento y limpiar recursos. ' Llama a la subrutina centralizada para registrar el rendimiento y limpiar recursos.
CleanupAndLog(dbKey, q, duration, req.RemoteAddress, requestsBeforeDecrement, poolBusyConnectionsForLog, con) CleanupAndLog(dbKey, q, duration, req.RemoteAddress, requestsBeforeDecrement, poolBusyConnectionsForLog, con)
@@ -202,7 +202,7 @@ Private Sub CleanupAndLog(dbKey As String, qName As String, durMs As Long, clien
Else Else
' Si el contador ya está en 0 o negativo (lo cual no debería ocurrir con la lógica actual, ' Si el contador ya está en 0 o negativo (lo cual no debería ocurrir con la lógica actual,
' pero se maneja para robustez), registramos una advertencia y lo aseguramos en 0. ' pero se maneja para robustez), registramos una advertencia y lo aseguramos en 0.
Log($"ADVERTENCIA: Intento de decrementar ActiveRequestsCountByDB para ${dbKey} que ya estaba en ${currentCount}. Asegurando a 0."$) ' Log($"ADVERTENCIA: Intento de decrementar ActiveRequestsCountByDB para ${dbKey} que ya estaba en ${currentCount}. Asegurando a 0."$)
GlobalParameters.ActiveRequestsCountByDB.Put(dbKey, 0) GlobalParameters.ActiveRequestsCountByDB.Put(dbKey, 0)
End If End If
' Log($"[DEBUG] CleanupAndLog After Decrement (B4X): dbKey=${dbKey}, New count (as Int)=${GlobalParameters.ActiveRequestsCountByDB.GetDefault(dbKey,0).As(Int)}, Map state: ${GlobalParameters.ActiveRequestsCountByDB}"$) ' Log($"[DEBUG] CleanupAndLog After Decrement (B4X): dbKey=${dbKey}, New count (as Int)=${GlobalParameters.ActiveRequestsCountByDB.GetDefault(dbKey,0).As(Int)}, Map state: ${GlobalParameters.ActiveRequestsCountByDB}"$)
@@ -480,7 +480,7 @@ End Sub
' Ejecuta una consulta única usando el protocolo V1. ' Ejecuta una consulta única usando el protocolo V1.
Private Sub ExecuteQuery(DB As String, con As SQL, in As InputStream, resp As ServletResponse) As String Private Sub ExecuteQuery(DB As String, con As SQL, in As InputStream, resp As ServletResponse) As String
Log("====================== ExecuteQuery =====================") ' Log("====================== ExecuteQuery =====================")
' Deserializa los datos de la petición usando el protocolo V1. ' Deserializa los datos de la petición usando el protocolo V1.
Dim clientVersion As Float = ReadObject(in) 'ignore Dim clientVersion As Float = ReadObject(in) 'ignore
Dim queryName As String = ReadObject(in) Dim queryName As String = ReadObject(in)

View File

@@ -238,7 +238,7 @@ Private Sub CleanupAndLog(dbKey As String, qName As String, durMs As Long, clien
Else Else
' Si el contador ya está en 0 o negativo (lo cual no debería ocurrir con la lógica actual, ' Si el contador ya está en 0 o negativo (lo cual no debería ocurrir con la lógica actual,
' pero se maneja para robustez), registramos una advertencia y lo aseguramos en 0. ' pero se maneja para robustez), registramos una advertencia y lo aseguramos en 0.
Log($"ADVERTENCIA: Intento de decrementar ActiveRequestsCountByDB para ${dbKey} que ya estaba en ${currentCount}. Asegurando a 0."$) ' Log($"ADVERTENCIA: Intento de decrementar ActiveRequestsCountByDB para ${dbKey} que ya estaba en ${currentCount}. Asegurando a 0."$)
GlobalParameters.ActiveRequestsCountByDB.Put(dbKey, 0) GlobalParameters.ActiveRequestsCountByDB.Put(dbKey, 0)
End If End If
' Log($"[DEBUG] CleanupAndLog After Decrement (JSON): dbKey=${dbKey}, New count (as Int)=${GlobalParameters.ActiveRequestsCountByDB.GetDefault(dbKey,0).As(Int)}, Map state: ${GlobalParameters.ActiveRequestsCountByDB}"$) ' Log($"[DEBUG] CleanupAndLog After Decrement (JSON): dbKey=${dbKey}, New count (as Int)=${GlobalParameters.ActiveRequestsCountByDB.GetDefault(dbKey,0).As(Int)}, Map state: ${GlobalParameters.ActiveRequestsCountByDB}"$)

View File

@@ -49,7 +49,7 @@ Sub Handle(req As ServletRequest, resp As ServletResponse)
' toolkit.InitializeStatic("java.awt.Toolkit") ' toolkit.InitializeStatic("java.awt.Toolkit")
' Dim screenRect As JavaObject ' Dim screenRect As JavaObject
' screenRect.InitializeNewInstance("java.awt.Rectangle", Array As Object( _ ' screenRect.InitializeNewInstance("java.awt.Rectangle", Array As Object( _
' toolkit.RunMethodJO("getDefaultToolkit", Null).RunMethod("getScreenSize", Null))) ' toolkit.RunMethodJO("getDefaultToolkit", Null).RunMethod("getScreenSize", Null)))
' Dim image As JavaObject = robot.RunMethod("createScreenCapture", Array As Object(screenRect)) ' Dim image As JavaObject = robot.RunMethod("createScreenCapture", Array As Object(screenRect))
' Dim ImageIO As JavaObject ' Dim ImageIO As JavaObject
' ImageIO.InitializeStatic("javax.imageio.ImageIO").RunMethod("write", Array As Object(image, "png", resp.OutputStream)) ' ImageIO.InitializeStatic("javax.imageio.ImageIO").RunMethod("write", Array As Object(image, "png", resp.OutputStream))
@@ -110,119 +110,158 @@ Sub Handle(req As ServletRequest, resp As ServletResponse)
' ### INICIO DE TU LÓGICA DE COMANDOS INTEGRADA ### ' ### INICIO DE TU LÓGICA DE COMANDOS INTEGRADA ###
' ========================================================================= ' =========================================================================
If Command = "reload" Then If Command = "reload" Then
' Usamos un StringBuilder temporal para acumular los logs de la recarga antes de añadirlos al StringBuilder principal.
Dim sbTemp As StringBuilder Dim sbTemp As StringBuilder
sbTemp.Initialize sbTemp.Initialize
sbTemp.Append($"Iniciando recarga de configuración (Hot-Swap) ($DateTime{DateTime.Now})"$).Append("<br>" & CRLF) 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. ' 1. Crear un nuevo mapa temporal para almacenar los conectores recién inicializados.
Dim newConnectors As Map Dim newConnectors As Map
newConnectors.Initialize newConnectors.Initialize
' Guardamos una referencia al mapa de conectores actualmente activos.
Dim oldConnectors As Map Dim oldConnectors As Map
Dim reloadSuccessful As Boolean = True Dim reloadSuccessful As Boolean = True
' *** INICIO DEL BLOQUE CRÍTICO 1: Obtener oldConnectors con ReentrantLock *** ' *** INICIO DEL BLOQUE CRÍTICO 1: Obtener oldConnectors con ReentrantLock ***
Dim lock1Acquired As Boolean = False ' Bandera para saber si el bloqueo fue adquirido. Dim lock1Acquired As Boolean = False
Try Try
Main.MainConnectorsLock.RunMethod("lock", Null) ' Adquirimos el bloqueo. Main.MainConnectorsLock.RunMethod("lock", Null)
lock1Acquired = True ' Marcamos que el bloqueo fue adquirido. lock1Acquired = True
oldConnectors = Main.Connectors ' Obtenemos la referencia al mapa actual de conectores. oldConnectors = Main.Connectors
Catch Catch
sbTemp.Append($" -> ERROR CRÍTICO: No se pudo adquirir el bloqueo para leer conectores antiguos: ${LastException.Message}"$).Append("<br>" & CRLF) sbTemp.Append($" -> ERROR CRÍTICO: No se pudo adquirir el bloqueo para leer conectores antiguos: ${LastException.Message}"$).Append(" " & CRLF)
reloadSuccessful = False ' Si falla aquí, la recarga no puede continuar. reloadSuccessful = False
End Try End Try
If lock1Acquired Then
Main.MainConnectorsLock.RunMethod("unlock", Null) ' Liberamos el bloqueo si fue adquirido.
End If
' *** FIN DEL BLOQUE CRÍTICO 1 ***
If Not(reloadSuccessful) Then ' Si el primer bloqueo falló o la asignación, salimos temprano. If lock1Acquired Then
sb.Append(sbTemp.ToString) ' Añadimos los logs acumulados al StringBuilder principal. Main.MainConnectorsLock.RunMethod("unlock", Null)
sb.Append($"¡ERROR: La recarga de configuración falló en la fase de bloqueo inicial! Los conectores antiguos siguen activos."$).Append("<br>" & CRLF) End If
Return ' Salir del Handle ya que ocurrió un error crítico irrecuperable.
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 End If
' 2. Iterar sobre las bases de datos configuradas y crear *nuevas* instancias de RDCConnector. ' 2. Iterar sobre las bases de datos configuradas y crear *nuevas* instancias de RDCConnector.
For Each dbKey As String In Main.listaDeCP For Each dbKey As String In Main.listaDeCP
Try Try
Dim newRDC As RDCConnector Dim newRDC As RDCConnector
newRDC.Initialize(dbKey) ' Inicializa la nueva instancia con la configuración fresca. 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) newConnectors.Put(dbKey, newRDC)
Dim newPoolStats As Map = newRDC.GetPoolStats Dim newPoolStats As Map = newRDC.GetPoolStats
sbTemp.Append($" -> ${dbKey}: Nuevo conector inicializado. Conexiones: ${newPoolStats.Get("TotalConnections")}"$).Append("<br>" & CRLF) sbTemp.Append($" -> ${dbKey}: Nuevo conector inicializado. Conexiones: ${newPoolStats.Get("TotalConnections")}"$).Append(" " & CRLF)
Catch Catch
sbTemp.Append($" -> ERROR CRÍTICO al inicializar nuevo conector para ${dbKey}: ${LastException.Message}"$).Append("<br>" & CRLF) sbTemp.Append($" -> ERROR CRÍTICO al inicializar nuevo conector para ${dbKey}: ${LastException.Message}"$).Append(" " & CRLF)
reloadSuccessful = False reloadSuccessful = False
Exit ' Si uno falla, abortamos la recarga completa para evitar un estado inconsistente. Exit ' Si uno falla, abortamos la recarga.
End Try End Try
Next Next
sb.Append(sbTemp.ToString) ' Añadimos los logs acumulados de la inicialización al StringBuilder principal. sb.Append(sbTemp.ToString)
If reloadSuccessful Then If reloadSuccessful Then
' 3. Si todos los nuevos conectores se inicializaron con éxito,
' realizamos el "cambio de cartel" (hot-swap) de forma atómica. ' 3. Si todo fue exitoso, hacemos el Hot-Swap atómico.
' *** INICIO DEL BLOQUE CRÍTICO 2: Reemplazar Main.Connectors con ReentrantLock *** ' *** INICIO DEL BLOQUE CRÍTICO 2: Reemplazar Main.Connectors con ReentrantLock ***
Dim lock2Acquired As Boolean = False ' Bandera para saber si el bloqueo fue adquirido.
Dim lock2Acquired As Boolean = False
Try Try
Main.MainConnectorsLock.RunMethod("lock", Null) ' Adquirimos el bloqueo. Main.MainConnectorsLock.RunMethod("lock", Null)
lock2Acquired = True ' Marcamos que el bloqueo fue adquirido. lock2Acquired = True
Main.Connectors = newConnectors ' Reemplazamos el mapa de conectores completo por el nuevo. Main.Connectors = newConnectors ' Reemplazamos el mapa de conectores completo por el nuevo.
Catch Catch
sb.Append($" -> ERROR CRÍTICO: No se pudo adquirir el bloqueo para reemplazar conectores: ${LastException.Message}"$).Append("<br>" & CRLF) sb.Append($" -> ERROR CRÍTICO: No se pudo adquirir el bloqueo para reemplazar conectores: ${LastException.Message}"$).Append(" " & CRLF)
reloadSuccessful = False ' Si falla aquí, la recarga no se completó con éxito. reloadSuccessful = False
End Try End Try
If lock2Acquired Then If lock2Acquired Then
Main.MainConnectorsLock.RunMethod("unlock", Null) ' Liberamos el bloqueo si fue adquirido. Main.MainConnectorsLock.RunMethod("unlock", Null)
End If End If
' *** FIN DEL BLOQUE CRÍTICO 2 ***
If reloadSuccessful Then ' Si el segundo bloqueo y swap fue exitoso If reloadSuccessful Then ' Si el swap fue exitoso
sb.Append($"¡Recarga de configuración completada con éxito (Hot-Swap)!"$).Append("<br>" & CRLF)
sb.Append($"Nuevos conectores activos. Verificando estado final..."$).Append("<br>" & CRLF)
' Mostrar el estado de los *nuevos* conectores después del swap. ' <<< PASO CLAVE 3: APLICAR EL NUEVO ESTADO GLOBAL GRANULAR Y REINICIAR TIMER >>>
If Main.Connectors.IsInitialized Then
Dim liveStats As Map ' 3a. Reemplazar el mapa de estados de logging granular
liveStats.Initialize Main.SQLiteLoggingStatusByDB.Clear ' Limpiamos el mapa global
For Each dbKey As String In Main.Connectors.Keys
Dim currentConnector As RDCConnector = Main.Connectors.Get(dbKey).As(RDCConnector) Dim isAnyEnabled As Boolean = False
liveStats.Put(dbKey, currentConnector.GetPoolStats) ' Obtiene las estadísticas en tiempo real. For Each dbKey As String In Main.listaDeCP
Next ' Recuperamos el estado logueado temporalmente.
j.Initialize(liveStats) Dim isEnabled As Boolean = newConnectors.Get(dbKey & "_LOG_STATE").As(Boolean)
sb.Append($"Estado actual de los pools: ${j.ToString}"$).Append(CRLF) ' No <br> para JSON puro 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 End If
' <<< FIN PASO CLAVE 3 >>>
' 4. Cerrar explícitamente los pools de conexión de las instancias antiguas. sb.Append($"¡Recarga de configuración completada con éxito (Hot-Swap)!"$).Append(" " & CRLF)
If oldConnectors.IsInitialized Then
sb.Append("Cerrando conectores antiguos...").Append("<br>" & CRLF) ' ... (Resto del código: Mostrar estado de pools y Cierre explícito de oldConnectors) ...
For Each dbKey As String In oldConnectors.Keys
Dim oldRDC As RDCConnector = oldConnectors.Get(dbKey).As(RDCConnector) Else
If oldRDC <> Null And oldRDC.IsInitialized Then sb.Append($"¡ERROR: La recarga de configuración falló en la fase de reemplazo de conectores! Los conectores antiguos siguen activos."$).Append(" " & CRLF)
oldRDC.Close ' Llama al método Close que hemos añadido al RDCConnector.
sb.Append($" -> Pool antiguo de ${dbKey} cerrado."$).Append("<br>" & CRLF)
End If
Next
End If
Else
sb.Append($"¡ERROR: La recarga de configuración falló en la fase de reemplazo de conectores! Los conectores antiguos siguen activos."$).Append("<br>" & CRLF)
End If End If
Else
sb.Append($"¡ERROR: La recarga de configuración falló durante la inicialización de nuevos conectores! Los conectores antiguos siguen activos."$).Append("<br>" & CRLF) Else ' Falla en inicialización (Punto 2)
' Si la recarga falló, los conectores antiguos (oldConnectors) se mantienen activos
' y siguen sirviendo para evitar un paro del servicio. ' 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 End If
Else If Command = "slowqueries" Then ' <<< INICIO: NUEVA Lógica para mostrar las queries lentas
sb.Append("<h2>Consultas Lentas Recientes</h2>") Else If Command = "slowqueries" Then ' <<< INICIO: NUEVA Lógica para mostrar las queries lentas
sb.Append("<h2 style=""margin-top:0px;margin-bottom:0px;"">Consultas Lentas Recientes</h2>")
sb.Append("(Este registro depende de que los logs estén habilitados con del parámetro ""enableSQLiteLogs=1"" en config properties)<br><br>")
Try 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. ' 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. ' 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 ORDER BY duration_ms DESC LIMIT 20") 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("<table border='1' style='width:100%; text-align:left; border-collapse: collapse;'>") sb.Append("<table border='1' style='width:100%; text-align:left; border-collapse: collapse;'>")
sb.Append("<thead><tr><th>Query</th><th>Duración (ms)</th><th>Fecha/Hora Local</th><th>DB Key</th><th>Cliente IP</th><th>Conex. Ocupadas</th><th>Peticiones Activas</th></tr></thead>") sb.Append("<thead><tr><th>Query</th><th>Duración (ms)</th><th>Fecha/Hora Local</th><th>DB Key</th><th>Cliente IP</th><th>Conex. Ocupadas</th><th>Peticiones Activas</th></tr></thead>")

View File

@@ -20,7 +20,7 @@ Public Sub ValidateAndAdjustParameters (CommandName As String, DBKey As String,
res.Initialize res.Initialize
res.Success = True ' Asumimos éxito inicialmente res.Success = True ' Asumimos éxito inicialmente
Log(">>>> IsToleranceEnabled: " & IsToleranceEnabled) ' Log(">>>> IsToleranceEnabled: " & IsToleranceEnabled)
' Aseguramos que receivedParams esté inicializada, incluso si está vacía o Null ' Aseguramos que receivedParams esté inicializada, incluso si está vacía o Null
If receivedParams = Null Or receivedParams.IsInitialized = False Then If receivedParams = Null Or receivedParams.IsInitialized = False Then

View File

@@ -17,7 +17,7 @@ Sub Class_Globals
' Bandera para activar/desactivar el modo de depuración de queries. ' Bandera para activar/desactivar el modo de depuración de queries.
' Cuando está en True, los comandos SQL se recargan en cada petición (útil en desarrollo). ' Cuando está en True, los comandos SQL se recargan en cada petición (útil en desarrollo).
Private DebugQueries As Boolean ' Private DebugQueries As Boolean
' Almacena los comandos SQL específicos de esta base de datos, cargados de su archivo de configuración. ' Almacena los comandos SQL específicos de esta base de datos, cargados de su archivo de configuración.
Public commands As Map Public commands As Map
@@ -157,11 +157,11 @@ Public Sub Initialize(DB As String)
End Try End Try
' Configuración de depuración de queries. Se activa automáticamente si el proyecto se ejecuta en modo DEBUG. ' Configuración de depuración de queries. Se activa automáticamente si el proyecto se ejecuta en modo DEBUG.
#If DEBUG ' #If DEBUG
' DebugQueries = True ' Descomentar para activar la recarga de comandos en cada petición en desarrollo. ' DebugQueries = True ' Descomentar para activar la recarga de comandos en cada petición en desarrollo.
#Else ' #Else
DebugQueries = False ' DebugQueries = False
#End If ' #End If
' Se obtiene el puerto del servidor HTTP desde la configuración de esta base de datos. ' Se obtiene el puerto del servidor HTTP desde la configuración de esta base de datos.
' Nota: En el diseño actual, el puerto principal lo define DB1 (config.properties). ' Nota: En el diseño actual, el puerto principal lo define DB1 (config.properties).
@@ -208,7 +208,7 @@ Public Sub GetConnection(DB As String) As SQL
' En modo de depuración, recarga los comandos SQL en cada petición. ' En modo de depuración, recarga los comandos SQL en cada petición.
' Esto permite modificar queries en config.properties sin reiniciar el servidor durante el desarrollo. ' Esto permite modificar queries en config.properties sin reiniciar el servidor durante el desarrollo.
If DebugQueries Then LoadSQLCommands(LoadConfigMap(DB), DB) ' If DebugQueries Then LoadSQLCommands(LoadConfigMap(DB), DB)
' <<<< Bloque de Logs de Depuración de Adquisición de Conexión (descomentar si es necesario) >>>> ' <<<< Bloque de Logs de Depuración de Adquisición de Conexión (descomentar si es necesario) >>>>
' Log($"[DEBUG - ${DB}] RDCConnector.GetConnection: Solicitando conexión del pool..."$) ' Log($"[DEBUG - ${DB}] RDCConnector.GetConnection: Solicitando conexión del pool..."$)

View File

@@ -52,6 +52,7 @@ Version=10.3
'Non-UI application (console / server application) 'Non-UI application (console / server application)
#Region Project Attributes #Region Project Attributes
#CommandLineArgs: #CommandLineArgs:
#MergeLibraries: True #MergeLibraries: True
' VERSION 5.09.16 ' VERSION 5.09.16
@@ -84,9 +85,8 @@ Sub Process_Globals
Public const VERSION As Float = 2.23 Public const VERSION As Float = 2.23
' Tipos personalizados (clases) para la serialización y deserialización de datos ' Tipos personalizados (clases) para la serialización y deserialización de datos
' entre el cliente B4X (DBRequestManager) y el servidor jRDC2. Type DBCommand (Name As String, Parameters() As Object)
Type DBCommand (Name As String, Parameters() As Object) ' Define un comando SQL. Type DBResult (Tag As Object, Columns As Map, Rows As List)
Type DBResult (Tag As Object, Columns As Map, Rows As List) ' Define la estructura de un resultado de consulta.
' Contiene una lista de los identificadores de bases de datos configuradas (ej. "DB1", "DB2"). ' Contiene una lista de los identificadores de bases de datos configuradas (ej. "DB1", "DB2").
Public listaDeCP As List Public listaDeCP As List
@@ -95,86 +95,92 @@ Sub Process_Globals
Private cpFiles As List Private cpFiles As List
' Mapas globales para gestionar los conectores de base de datos y los comandos SQL. ' Mapas globales para gestionar los conectores de base de datos y los comandos SQL.
' Connectors: Almacena las instancias de RDCConnector por cada base de datos (DB1, DB2, etc.).
' commandsMap: Almacena los comandos SQL cargados de los archivos de configuración para cada DB.
Public Connectors, commandsMap As Map Public Connectors, commandsMap As Map
' Objeto SQL para interactuar con la base de datos de usuarios y logs (SQLite). ' Objeto SQL para interactuar con la base de datos de usuarios y logs (SQLite).
Public SQL1 As SQL Public SQL1 As SQL
' Objeto para realizar operaciones de hashing de contraseñas de forma segura (para autenticación de Manager). ' Objeto para realizar operaciones de hashing de contraseñas de forma segura.
Private bc As BCrypt Private bc As BCrypt
' Objeto de bloqueo (ReentrantLock) para proteger el mapa Main.Connectors durante operaciones de recarga (Hot-Swap). ' Objeto de bloqueo (ReentrantLock) para proteger Main.Connectors durante Hot-Swap.
Public MainConnectorsLock As JavaObject Public MainConnectorsLock As JavaObject
' Timer para ejecutar tareas periódicas, como la limpieza de logs. ' Timer para ejecutar tareas periódicas, como la limpieza de logs.
Public timerLogs As Timer Public timerLogs As Timer
' NUEVAS VARIABLES para control granular de logs
' Mapa para almacenar el estado de logging (True/False) por cada DBKey (DB1, DB2, etc.).
Public SQLiteLoggingStatusByDB As Map
' Bandera global que indica si AL MENOS una base de datos tiene los logs habilitados.
Public IsAnySQLiteLoggingEnabled As Boolean
' Tipo para encapsular el resultado de la validación de parámetros. ' Tipo para encapsular el resultado de la validación de parámetros.
Type ParameterValidationResult ( _ Type ParameterValidationResult ( _
Success As Boolean, _ Success As Boolean, _
ErrorMessage As String, _ ErrorMessage As String, _
ParamsToExecute As List _ ' La lista de parámetros final a usar en la ejecución SQL ParamsToExecute As List _ ' La lista de parámetros final a usar en la ejecución SQL
) )
End Sub End Sub
Sub AppStart (Args() As String) Sub AppStart (Args() As String)
' --- Subrutina principal que se ejecuta al iniciar la aplicación --- ' --- Subrutina principal que se ejecuta al iniciar la aplicación ---
bc.Initialize("BC") bc.Initialize("BC")
' 1. Inicializa la base de datos local de usuarios (SQLite) y la tabla de logs. ' 1. Inicializa la base de datos local de usuarios (SQLite) y la tabla de logs.
' Esta base de datos se crea automáticamente si no existe o se migra si es necesario.
InitializeSQLiteDatabase InitializeSQLiteDatabase
' <<<< Bloque de inicialización del Timer para la limpieza de logs >>>>
' Inicializa y configura el Timer para borrar logs antiguos cada 10 minutos (600,000 milisegundos).
timerLogs.Initialize("TimerLogs", 600000) ' 10 minutos = 600 * 1000 = 600000 ms
timerLogs.Enabled = True ' Habilita el timer para que empiece a correr.
Log("Main.AppStart: Timer de limpieza de 'query_logs' inicializado para ejecutarse cada 10 minutos.")
' <<<< Fin del bloque del Timer >>>>
' 2. Inicializa los mapas globales definidos en GlobalParameters.bas. ' 2. Inicializa los mapas globales definidos en GlobalParameters.bas.
' Estos mapas se usan para monitorear el servidor y gestionar configuraciones dinámicas. GlobalParameters.mpLogs.Initialize
GlobalParameters.mpLogs.Initialize ' Mapa para almacenar logs de actividad general. GlobalParameters.mpTotalRequests.Initialize
GlobalParameters.mpTotalRequests.Initialize ' Mapa para contar peticiones por endpoint/DB. GlobalParameters.mpTotalConnections.Initialize
GlobalParameters.mpTotalConnections.Initialize ' Mapa para almacenar el estado de los pools de conexión por DB. GlobalParameters.mpBlockConnection.Initialize
GlobalParameters.mpBlockConnection.Initialize ' Mapa para gestionar IPs bloqueadas (si la funcionalidad está activa).
' Aseguramos que el mapa de conteo de peticiones activas sea thread-safe para un manejo concurrente seguro. ' Aseguramos que el mapa de conteo de peticiones activas sea thread-safe.
GlobalParameters.ActiveRequestsCountByDB = srvr.CreateThreadSafeMap GlobalParameters.ActiveRequestsCountByDB = srvr.CreateThreadSafeMap
' 3. Inicializa las estructuras principales del servidor HTTP. ' 3. Inicializa las estructuras principales del servidor HTTP.
listaDeCP.Initialize ' Inicializa la lista que contendrá los IDs de las bases de datos. listaDeCP.Initialize
srvr.Initialize("") ' Inicializa el objeto servidor HTTP. srvr.Initialize("")
Connectors = srvr.CreateThreadSafeMap ' Crea un mapa seguro para almacenar instancias de RDCConnector (un conector por DB). Connectors = srvr.CreateThreadSafeMap
commandsMap.Initialize ' Inicializa el mapa que almacenará los comandos SQL cargados de los archivos de configuración. commandsMap.Initialize
' Creamos una instancia de ReentrantLock para proteger Main.Connectors durante operaciones atómicas de Hot-Swap. ' NUEVO: Inicializar el mapa de estado de logs granular
SQLiteLoggingStatusByDB.Initialize
' Creamos una instancia de ReentrantLock para proteger Main.Connectors.
MainConnectorsLock.InitializeNewInstance("java.util.concurrent.locks.ReentrantLock", Null) MainConnectorsLock.InitializeNewInstance("java.util.concurrent.locks.ReentrantLock", Null)
' === 4. INICIALIZACIÓN DEL CONECTOR PARA LA BASE DE DATOS PRINCIPAL (DB1) === ' === 4. INICIALIZACIÓN DEL CONECTOR PARA LA BASE DE DATOS PRINCIPAL (DB1) ===
' DB1 siempre usa el archivo 'config.properties' por defecto.
Try Try
Dim con1 As RDCConnector ' Declara una variable específica y única para el conector de DB1. Dim con1 As RDCConnector
con1.Initialize("DB1") ' Inicializa la instancia del conector para "DB1". con1.Initialize("DB1")
Connectors.Put("DB1", con1) ' Asocia el identificador "DB1" con su instancia de RDCConnector. Connectors.Put("DB1", con1)
srvr.Port = con1.serverPort ' El puerto del servidor HTTP se obtiene del config.properties de DB1. srvr.Port = con1.serverPort
listaDeCP.Add("DB1") ' Añade "DB1" a la lista de bases de datos gestionadas. listaDeCP.Add("DB1")
Log($"Main.AppStart: Conector 'DB1' inicializado exitosamente en puerto: ${srvr.Port}"$) Log($"Main.AppStart: Conector 'DB1' inicializado exitosamente en puerto: ${srvr.Port}"$)
' Lógica de Logs para DB1 (Fuente principal de configuración)
Dim enableLogsSetting As Int = con1.config.GetDefault("enableSQLiteLogs", 1).As(Int)
Dim isEnabled As Boolean = (enableLogsSetting = 1)
SQLiteLoggingStatusByDB.Put("DB1", isEnabled) ' Guardar el estado
Catch Catch
Dim ErrorMsg As String = $"Main.AppStart: ERROR CRÍTICO al inicializar conector 'DB1': ${LastException.Message}"$ Dim ErrorMsg As String = $"Main.AppStart: ERROR CRÍTICO al inicializar conector 'DB1': ${LastException.Message}"$
Log(ErrorMsg) Log(ErrorMsg)
LogServerError("ERROR", "Main.AppStart", ErrorMsg, "DB1", Null, Null) LogServerError("ERROR", "Main.AppStart", ErrorMsg, "DB1", Null, Null)
' Si DB1 falla, el servidor no puede arrancar correctamente.
ExitApplication ExitApplication
End Try End Try
' === 5. DETECCIÓN E INICIALIZACIÓN DE BASES DE DATOS ADICIONALES (DB2, DB3, DB4) === ' === 5. DETECCIÓN E INICIALIZACIÓN DE BASES DE DATOS ADICIONALES (DB2, DB3, DB4) ===
' El servidor busca archivos de configuración adicionales (ej. config.DB2.properties)
' en el mismo directorio donde se ejecuta el JAR.
cpFiles = File.ListFiles("./") cpFiles = File.ListFiles("./")
If cpFiles.Size > 0 Then If cpFiles.Size > 0 Then
For i = 0 To cpFiles.Size - 1 For i = 0 To cpFiles.Size - 1
' Procesa 'config.DB2.properties' ' Procesa 'config.DB2.properties'
If cpFiles.Get(i) = "config.DB2.properties" Then If cpFiles.Get(i) = "config.DB2.properties" Then
Try Try
@@ -183,13 +189,19 @@ Sub AppStart (Args() As String)
Connectors.Put("DB2", con2) Connectors.Put("DB2", con2)
listaDeCP.Add("DB2") listaDeCP.Add("DB2")
Log("Main.AppStart: Conector 'DB2' inicializado exitosamente.") Log("Main.AppStart: Conector 'DB2' inicializado exitosamente.")
' Lógica de Logs para DB2
Dim enableLogsSetting As Int = con2.config.GetDefault("enableSQLiteLogs", 0).As(Int)
Dim isEnabled As Boolean = (enableLogsSetting = 1)
SQLiteLoggingStatusByDB.Put("DB2", isEnabled) ' Guardar el estado
Catch Catch
Dim ErrorMsg As String = $"Main.AppStart: ERROR al inicializar conector 'DB2': ${LastException.Message}"$ Dim ErrorMsg As String = $"Main.AppStart: ERROR al inicializar conector 'DB2': ${LastException.Message}"$
Log(ErrorMsg) Log(ErrorMsg)
LogServerError("ERROR", "Main.AppStart", ErrorMsg, "DB2", Null, Null) LogServerError("ERROR", "Main.AppStart", ErrorMsg, "DB2", Null, Null)
' Si un conector secundario falla, el servidor puede continuar, pero es importante saberlo.
End Try End Try
End If End If
' Procesa 'config.DB3.properties' ' Procesa 'config.DB3.properties'
If cpFiles.Get(i) = "config.DB3.properties" Then If cpFiles.Get(i) = "config.DB3.properties" Then
Try Try
@@ -198,13 +210,19 @@ Sub AppStart (Args() As String)
Connectors.Put("DB3", con3) Connectors.Put("DB3", con3)
listaDeCP.Add("DB3") listaDeCP.Add("DB3")
Log("Main.AppStart: Conector 'DB3' inicializado exitosamente.") Log("Main.AppStart: Conector 'DB3' inicializado exitosamente.")
' Lógica de Logs para DB3
Dim enableLogsSetting As Int = con3.config.GetDefault("enableSQLiteLogs", 0).As(Int)
Dim isEnabled As Boolean = (enableLogsSetting = 1)
SQLiteLoggingStatusByDB.Put("DB3", isEnabled) ' Guardar el estado
Catch Catch
Dim ErrorMsg As String = $"Main.AppStart: ERROR al inicializar conector 'DB3': ${LastException.Message}"$ Dim ErrorMsg As String = $"Main.AppStart: ERROR al inicializar conector 'DB3': ${LastException.Message}"$
Log(ErrorMsg) Log(ErrorMsg)
LogServerError("ERROR", "Main.AppStart", ErrorMsg, "DB3", Null, Null) LogServerError("ERROR", "Main.AppStart", ErrorMsg, "DB3", Null, Null)
' Si un conector secundario falla, el servidor puede continuar, pero es importante saberlo.
End Try End Try
End If End If
' Procesa 'config.DB4.properties' ' Procesa 'config.DB4.properties'
If cpFiles.Get(i) = "config.DB4.properties" Then If cpFiles.Get(i) = "config.DB4.properties" Then
Try Try
@@ -213,13 +231,19 @@ Sub AppStart (Args() As String)
Connectors.Put("DB4", con4) Connectors.Put("DB4", con4)
listaDeCP.Add("DB4") listaDeCP.Add("DB4")
Log("Main.AppStart: Conector 'DB4' inicializado exitosamente.") Log("Main.AppStart: Conector 'DB4' inicializado exitosamente.")
' Lógica de Logs para DB4
Dim enableLogsSetting As Int = con4.config.GetDefault("enableSQLiteLogs", 0).As(Int)
Dim isEnabled As Boolean = (enableLogsSetting = 1)
SQLiteLoggingStatusByDB.Put("DB4", isEnabled) ' Guardar el estado
Catch Catch
Dim ErrorMsg As String = $"Main.AppStart: ERROR al inicializar conector 'DB4': ${LastException.Message}"$ Dim ErrorMsg As String = $"Main.AppStart: ERROR al inicializar conector 'DB4': ${LastException.Message}"$
Log(ErrorMsg) Log(ErrorMsg)
LogServerError("ERROR", "Main.AppStart", ErrorMsg, "DB4", Null, Null) LogServerError("ERROR", "Main.AppStart", ErrorMsg, "DB4", Null, Null)
' Si un conector secundario falla, el servidor puede continuar, pero es importante saberlo.
End Try End Try
End If End If
Next Next
Else Else
Log("Main.AppStart: No se encontraron archivos de configuración adicionales (config.DBx.properties).") Log("Main.AppStart: No se encontraron archivos de configuración adicionales (config.DBx.properties).")
@@ -232,26 +256,46 @@ Sub AppStart (Args() As String)
sbListaDeCP_Log.Append(item).Append(", ") sbListaDeCP_Log.Append(item).Append(", ")
Next Next
If sbListaDeCP_Log.Length > 0 Then If sbListaDeCP_Log.Length > 0 Then
sbListaDeCP_Log.Remove(sbListaDeCP_Log.Length - 2, sbListaDeCP_Log.Length) ' Elimina la última ", " sbListaDeCP_Log.Remove(sbListaDeCP_Log.Length - 2, sbListaDeCP_Log.Length)
End If End If
Log($"Main.AppStart: Bases de datos configuradas y listas: [${sbListaDeCP_Log.ToString}]"$) Log($"Main.AppStart: Bases de datos configuradas y listas: [${sbListaDeCP_Log.ToString}]"$)
' <<<< Bloque de inicialización del Timer para la limpieza de logs >>>>
' Inicialización INCONDICIONAL del Timer (Garantiza que el objeto exista y prevenga el IllegalStateException)
timerLogs.Initialize("TimerLogs", 600000) ' 10 minutos = 600 * 1000 = 600000 ms
' CONTROL CONDICIONAL BASADO EN EL ESTADO GRANULAR
IsAnySQLiteLoggingEnabled = False
For Each dbStatus As Boolean In SQLiteLoggingStatusByDB.Values
If dbStatus Then
IsAnySQLiteLoggingEnabled = True
Exit ' Si uno está activo, es suficiente para encender el Timer
End If
Next
If IsAnySQLiteLoggingEnabled Then
timerLogs.Enabled = True
Log("Main.AppStart: Timer de limpieza de logs ACTIVADO (al menos una DB requiere logs).")
Else
timerLogs.Enabled = False
Log("Main.AppStart: Timer de limpieza de logs DESHABILITADO (ninguna DB requiere logs).")
End If
' <<<< Fin del bloque del Timer >>>>
' === 6. REGISTRO DE HANDLERS HTTP PARA EL SERVIDOR === ' === 6. REGISTRO DE HANDLERS HTTP PARA EL SERVIDOR ===
' Asocia rutas URL específicas con clases que manejarán las peticiones correspondientes. srvr.AddHandler("/ping", "ping", False)
' El último parámetro (True) indica que el handler se ejecutará en un nuevo hilo, srvr.AddHandler("/test", "TestHandler", False)
' lo que es recomendable para la mayoría de los casos para evitar bloqueos. srvr.AddHandler("/login", "LoginHandler", False)
srvr.AddHandler("/ping", "ping", False) ' Endpoint simple para verificar si el servidor está activo. srvr.AddHandler("/dologin", "DoLoginHandler", False)
srvr.AddHandler("/test", "TestHandler", False) ' Endpoint para pruebas de conexión y estado del servidor. srvr.AddHandler("/logout", "LogoutHandler", False)
srvr.AddHandler("/login", "LoginHandler", False) ' Muestra la página HTML de login. srvr.AddHandler("/changepass", "ChangePassHandler", False)
srvr.AddHandler("/dologin", "DoLoginHandler", False) ' Procesa el intento de inicio de sesión. srvr.AddHandler("/manager", "Manager", False)
srvr.AddHandler("/logout", "LogoutHandler", False) ' Cierra la sesión del usuario. srvr.AddHandler("/DBJ", "DBHandlerJSON", False)
srvr.AddHandler("/changepass", "ChangePassHandler", False) ' Permite a los usuarios cambiar su contraseña. srvr.AddHandler("/dbrquery", "DBHandlerJSON", False)
srvr.AddHandler("/manager", "Manager", False) ' Panel de administración del servidor (requiere autenticación). srvr.AddHandler("/favicon.ico", "faviconHandler", False)
srvr.AddHandler("/DBJ", "DBHandlerJSON", False) ' Handler para clientes web (ej. JavaScript, Node.js) que usan JSON. srvr.AddHandler("/*", "DBHandlerB4X", False)
srvr.AddHandler("/dbrquery", "DBHandlerJSON", False) ' Un alias para el handler JSON, por si se usa en clientes específicos.
srvr.AddHandler("/favicon.ico", "faviconHandler", False) ' Sirve el icono de la página (favicon).
srvr.AddHandler("/*", "DBHandlerB4X", False) ' Handler por defecto para clientes B4X (DBRequestManager),
' procesa peticiones dinámicamente según la URL.
' 7. Inicia el servidor HTTP. ' 7. Inicia el servidor HTTP.
srvr.Start srvr.Start
@@ -259,80 +303,72 @@ Sub AppStart (Args() As String)
Log($"-=== jRDC está funcionando en el puerto: ${srvr.Port} (versión = $1.2{VERSION}) ===-"$) Log($"-=== jRDC está funcionando en el puerto: ${srvr.Port} (versión = $1.2{VERSION}) ===-"$)
Log("===========================================================") Log("===========================================================")
' 8. Inicia el bucle de mensajes de B4J. Es esencial para que la aplicación ' 8. Inicia el bucle de mensajes de B4J.
' de servidor continúe ejecutándose y procesando eventos.
StartMessageLoop StartMessageLoop
End Sub End Sub
' --- Subrutina para inicializar la base de datos de usuarios local (SQLite) --- ' --- Subrutina para inicializar la base de datos de usuarios local (SQLite) ---
' Esta base de datos se utiliza para almacenar credenciales de usuarios que pueden
' acceder al panel de administración del servidor jRDC y los logs de queries.
Sub InitializeSQLiteDatabase
Dim dbFileName As String = "users.db" ' Nombre del archivo de la base de datos SQLite.
' Verifica si el archivo de la base de datos ya existe en el directorio de la aplicación. Sub InitializeSQLiteDatabase
Dim dbFileName As String = "users.db"
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)
' Inicializa la conexión a la base de datos SQLite, creándola si no existe (último parámetro en True).
SQL1.InitializeSQLite(File.DirApp, dbFileName, True) SQL1.InitializeSQLite(File.DirApp, dbFileName, True)
' Define y ejecuta la sentencia SQL para crear la tabla 'users' para la autenticación del Manager. ' 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)
' >>> INICIO: Creación de la tabla query_logs con las nuevas columnas desde CERO <<< ' Crear tabla 'query_logs'
Log("Creando tabla 'query_logs' con columnas de rendimiento.") Log("Creando tabla 'query_logs' con columnas de rendimiento.")
' Esta tabla almacena métricas detalladas de cada query ejecutada.
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)
' >>> FIN: Creación de la tabla query_logs <<<
' Crea un usuario por defecto para facilitar el primer acceso al panel de administración. ' Insertar usuario por defecto
Dim defaultUser As String = "admin" Dim defaultUser As String = "admin"
Dim defaultPass As String = "12345" Dim defaultPass As String = "12345"
' Genera un hash seguro de la contraseña usando BCrypt, lo cual es crucial para la seguridad.
Dim hashedPass As String = bc.hashpw(defaultPass, bc.gensalt) Dim hashedPass As String = bc.hashpw(defaultPass, bc.gensalt)
' Inserta el usuario por defecto en la tabla 'users'.
SQL1.ExecNonQuery2("INSERT INTO users (username, password_hash) VALUES (?, ?)", Array As Object(defaultUser, hashedPass)) SQL1.ExecNonQuery2("INSERT INTO users (username, password_hash) VALUES (?, ?)", Array As Object(defaultUser, hashedPass))
Log($"Usuario por defecto creado -> user: ${defaultUser}, pass: ${defaultPass}"$) Log($"Usuario por defecto creado -> user: ${defaultUser}, pass: ${defaultPass}"$)
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)" ' Crear tabla 'errores'
SQL1.ExecNonQuery(createQueryLogsTable)
' >>> INICIO: Creación de la tabla errores con columnas de error/advertencia <<<
Log("Creando tabla 'errores' para registrar eventos.") 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)" 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)
' >>> FIN: Creación de la tabla errores <<<
Else Else
' Si el archivo de la base de datos ya existe, simplemente se abre.
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.")
' >>> 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 <<<
Log("Verificando y migrando tabla 'query_logs' si es necesario.") Log("Verificando y migrando tabla 'query_logs' si es necesario.")
' Primero, verificar si la tabla query_logs existe.
If SQL1.ExecQuerySingleResult("SELECT name FROM sqlite_master WHERE type='table' AND name='query_logs'") = Null Then If SQL1.ExecQuerySingleResult("SELECT name FROM sqlite_master WHERE type='table' AND name='query_logs'") = Null Then
Log("Tabla 'query_logs' no encontrada, creándola con columnas de rendimiento.") Log("Tabla 'query_logs' no encontrada, creándola con columnas de rendimiento.")
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)
Else Else
' Si la tabla query_logs ya existe, entonces verificamos y añadimos las columnas faltantes (busy_connections, handler_active_requests). ' Si la tabla query_logs ya existe, entonces verificamos y añadimos las columnas faltantes (busy_connections, handler_active_requests).
Dim columnExists As Boolean Dim columnExists As Boolean
Dim rs As ResultSet Dim rs As ResultSet
' --- VERIFICAR Y AÑADIR busy_connections --- ' --- VERIFICAR Y AÑADIR busy_connections ---
columnExists = False columnExists = False
' Ejecutamos PRAGMA para obtener la información de la tabla y verificar la existencia de la columna.
rs = SQL1.ExecQuery("PRAGMA table_info(query_logs)") rs = SQL1.ExecQuery("PRAGMA table_info(query_logs)")
Do While rs.NextRow Do While rs.NextRow
If rs.GetString("name").EqualsIgnoreCase("busy_connections") Then If rs.GetString("name").EqualsIgnoreCase("busy_connections") Then
columnExists = True columnExists = True
Exit ' La columna ya existe, salimos del bucle. Exit ' La columna ya existe, salimos del bucle.
End If End If
Loop Loop
rs.Close ' ¡Importante cerrar el ResultSet para liberar recursos! rs.Close
If columnExists = False Then If columnExists = False Then
Log("Añadiendo columna 'busy_connections' a query_logs.") Log("Añadiendo columna 'busy_connections' a query_logs.")
@@ -341,85 +377,96 @@ Sub InitializeSQLiteDatabase
' --- VERIFICAR Y AÑADIR handler_active_requests --- ' --- VERIFICAR Y AÑADIR handler_active_requests ---
columnExists = False columnExists = False
rs = SQL1.ExecQuery("PRAGMA table_info(query_logs)") ' Ejecutamos PRAGMA nuevamente para esta columna. rs = SQL1.ExecQuery("PRAGMA table_info(query_logs)")
Do While rs.NextRow Do While rs.NextRow
If rs.GetString("name").EqualsIgnoreCase("handler_active_requests") Then If rs.GetString("name").EqualsIgnoreCase("handler_active_requests") Then
columnExists = True columnExists = True
Exit ' La columna ya existe, salimos del bucle. Exit ' La columna ya existe, salimos del bucle.
End If End If
Loop Loop
rs.Close ' ¡Importante cerrar el ResultSet! rs.Close
If columnExists = False Then If columnExists = False Then
Log("Añadiendo columna 'handler_active_requests' a query_logs.") Log("Añadiendo columna 'handler_active_requests' a query_logs.")
SQL1.ExecNonQuery("ALTER TABLE query_logs ADD COLUMN handler_active_requests INTEGER DEFAULT 0") SQL1.ExecNonQuery("ALTER TABLE query_logs ADD COLUMN handler_active_requests INTEGER DEFAULT 0")
End If End If
' >>> INICIO: Lógica de migración para 'errores' si la DB ya existía <<< ' >>> INICIO: Lógica de migración para 'errores' si la DB ya existía <<<
Log("Verificando y migrando tabla 'errores' si es necesario.") Log("Verificando y migrando tabla 'errores' si es necesario.")
If SQL1.ExecQuerySingleResult("SELECT name FROM sqlite_master WHERE type='table' AND name='errores'") = Null Then If SQL1.ExecQuerySingleResult("SELECT name FROM sqlite_master WHERE type='table' AND name='errores'") = Null Then
Log("Tabla 'errores' no encontrada, creándola.") Log("Tabla 'errores' no encontrada, creándola.")
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)
Else Else
' Si la tabla ya existe, podrías añadir lógica para ALTER TABLE si se añaden nuevas columnas en el futuro.
' Por ahora, asumimos que la estructura inicial es suficiente.
Log("Tabla 'errores' ya existe.") Log("Tabla 'errores' ya existe.")
End If End If
' >>> FIN: Lógica de migración para 'errores' <<< ' >>> FIN: Lógica de migración para 'errores' <<<
End If End If
' >>> FIN: Lógica de migración (ALTER TABLE) <<< ' >>> FIN: Lógica de migración (ALTER TABLE) <<<
End If End If
End Sub End Sub
' Subrutina para registrar las métricas de rendimiento de las queries en la tabla 'query_logs'. ' --- Subrutina para registrar las métricas de rendimiento de las queries en la tabla 'query_logs'. ---
' ¡MODIFICADA PARA USAR FILTRADO GRANULAR POR DBKEY!
Public Sub LogQueryPerformance(QueryName As String, DurationMs As Long, DbKey As String, ClientIp As String, HandlerActiveRequests As Int, PoolBusyConnections As Int) Public Sub LogQueryPerformance(QueryName As String, DurationMs As Long, DbKey As String, ClientIp As String, HandlerActiveRequests As Int, PoolBusyConnections As Int)
Try
' Los valores de PoolBusyConnections y HandlerActiveRequests ya se reciben directamente del handler,
' eliminando la necesidad de obtenerlos del conector en este punto.
' Insertamos los datos en la tabla query_logs de SQLite. ' Obtener el estado de logging para esta DBKey. Usar False si la DBKey no existe en el mapa.
SQL1.ExecNonQuery2("INSERT INTO query_logs (query_name, duration_ms, timestamp, db_key, client_ip, busy_connections, handler_active_requests) VALUES (?, ?, ?, ?, ?, ?, ?)", _ Dim isEnabled As Boolean = SQLiteLoggingStatusByDB.GetDefault(DbKey, False)
Array As Object(QueryName, DurationMs, DateTime.Now, DbKey, ClientIp, PoolBusyConnections, HandlerActiveRequests))
Catch If isEnabled Then
Log("Error al guardar log de query en SQLite (Main.LogQueryPerformance): " & LastException.Message) Try
End Try SQL1.ExecNonQuery2("INSERT INTO query_logs (query_name, duration_ms, timestamp, db_key, client_ip, busy_connections, handler_active_requests) VALUES (?, ?, ?, ?, ?, ?, ?)", _
Array As Object(QueryName, DurationMs, DateTime.Now, DbKey, ClientIp, PoolBusyConnections, HandlerActiveRequests))
Catch
Log("Error al guardar log de query en SQLite (Main.LogQueryPerformance): " & LastException.Message)
End Try
End If
End Sub End Sub
' Subrutina para registrar errores y advertencias en la tabla 'errores'. ' --- Subrutina para registrar errores y advertencias en la tabla 'errores'. ---
' Type: "ERROR" o "ADVERTENCIA" ' ¡MODIFICADA PARA USAR FILTRADO GRANULAR POR DBKEY!
' Source: Módulo.Subrutina donde ocurrió el evento (ej. "DBHandlerJSON.Handle")
' Message: El mensaje descriptivo del error/advertencia.
' DBKey: La clave de la base de datos involucrada (ej. "DB1", "DB2"), Null si no aplica.
' CommandName: El nombre del comando SQL (ej. "select_user"), Null si no aplica.
' ClientIp: La dirección IP del cliente que originó la petición, Null si no aplica.
Public Sub LogServerError(Type0 As String, Source As String, Message As String, DBKey As String, CommandName As String, ClientIp As String) Public Sub LogServerError(Type0 As String, Source As String, Message As String, DBKey As String, CommandName As String, ClientIp As String)
Try
SQL1.ExecNonQuery2("INSERT INTO errores (timestamp, type, source, message, db_key, command_name, client_ip) VALUES (?, ?, ?, ?, ?, ?, ?)", _ ' Obtener el estado de logging para esta DBKey. Usar False si la DBKey es Null o no está en el mapa.
Array As Object(DateTime.Now, Type0, Source, Message, DBKey, CommandName, ClientIp)) Dim isEnabled As Boolean = SQLiteLoggingStatusByDB.GetDefault(DBKey, False)
Catch
Log("ERROR CRÍTICO: Fallo al guardar el log de error/advertencia en SQLite (Main.LogServerError): " & LastException.Message) If isEnabled Then
' En este punto, no podemos hacer mucho más que loggear el fallo en la consola, Try
' para evitar un bucle infinito de errores de logging. SQL1.ExecNonQuery2("INSERT INTO errores (timestamp, type, source, message, db_key, command_name, client_ip) VALUES (?, ?, ?, ?, ?, ?, ?)", _
End Try Array As Object(DateTime.Now, Type0, Source, Message, DBKey, CommandName, ClientIp))
Catch
Log("ERROR CRÍTICO: Fallo al guardar el log de error/advertencia en SQLite (Main.LogServerError): " & LastException.Message)
End Try
End If
End Sub End Sub
' Subrutina de evento para el Timer 'timerLogs'. ' --- Subrutina de evento para el Timer 'timerLogs'. ---
' Se ejecuta periódicamente (cada 10 minutos) para limpiar la tabla de logs. ' El estado 'Enabled' del Timer ya está controlado por IsAnySQLiteLoggingEnabled en AppStart y Manager.
Sub TimerLogs_Tick Sub TimerLogs_Tick
Try Try
borraArribaDe15000Logs ' Llama a la función para limpiar los logs. borraArribaDe15000Logs
Catch Catch
Dim ErrorMsg As String = "ERROR en TimerLogs_Tick al intentar borrar logs: " & LastException.Message Dim ErrorMsg As String = "ERROR en TimerLogs_Tick al intentar borrar logs: " & LastException.Message
Log(ErrorMsg) Log(ErrorMsg)
LogServerError("ERROR", "Main.TimerLogs_Tick", ErrorMsg, Null, "log_cleanup", Null) ' <-- Nuevo Log LogServerError("ERROR", "Main.TimerLogs_Tick", ErrorMsg, Null, "log_cleanup", Null)
End Try End Try
End Sub End Sub
' Borra los registros más antiguos de la tabla 'query_logs', manteniendo solo los 15,000 más recientes. ' --- Borra los registros más antiguos de la tabla 'query_logs' y hace VACUUM. ---
' Luego, optimiza el espacio de la base de datos SQLite con un 'vacuum'. ' ¡MODIFICADA PARA USAR FILTRADO GLOBAL!
Sub borraArribaDe15000Logs 'ignore Sub borraArribaDe15000Logs 'ignore
Log("Recortando la tabla de 'query_logs', límite de 15,000 registros.")
SQL1.ExecNonQuery("DELETE FROM query_logs WHERE timestamp NOT in (SELECT timestamp FROM query_logs ORDER BY timestamp desc LIMIT 15000 )") If IsAnySQLiteLoggingEnabled Then ' Solo ejecutar si al menos una DB requiere logs.
SQL1.ExecNonQuery("vacuum;") ' Optimiza el espacio de almacenamiento de la base de datos. Log("Recortando la tabla de 'query_logs', límite de 15,000 registros.")
SQL1.ExecNonQuery("DELETE FROM query_logs WHERE timestamp NOT in (SELECT timestamp FROM query_logs ORDER BY timestamp desc LIMIT 15000 )")
SQL1.ExecNonQuery("vacuum;")
Else
' Si IsAnySQLiteLoggingEnabled es False, el Timer no debería estar activo.
Log("AVISO: Tarea de limpieza de logs omitida. El logging global de SQLite está deshabilitado.")
End If
End Sub End Sub

View File

@@ -43,6 +43,6 @@ ModuleClosedNodes6=
ModuleClosedNodes7= ModuleClosedNodes7=
ModuleClosedNodes8= ModuleClosedNodes8=
ModuleClosedNodes9= ModuleClosedNodes9=
NavigationStack=DBHandlerB4X,ExecuteBatch2,386,0,DBHandlerB4X,ExecuteBatch,449,0,DBHandlerB4X,ExecuteQuery,506,0,DBHandlerJSON,Handle,148,0,Main,Process_Globals,59,0,Manager,Handle,375,0,TestHandler,Handle,25,1,ChangePassHandler,Handle,17,0,Main,AppStart,157,0,Cambios,Process_Globals,13,6 NavigationStack=DBHandlerB4X,CleanupAndLog,198,0,DBHandlerJSON,CleanupAndLog,223,0,ParameterValidationUtils,ValidateAndAdjustParameters,45,0,Main,Process_Globals,53,0,Main,AppStart,186,0,Main,LogQueryPerformance,367,0,Main,LogServerError,384,6,Manager,Handle,164,6,Main,borraArribaDe15000Logs,412,0,Cambios,Process_Globals,25,6
SelectedBuild=0 SelectedBuild=0
VisibleModules=3,4,13,1,10,11,14,2 VisibleModules=3,4,13,1,10,11,14,2