- VERSION 5.10.27

- feat(arquitectura): Consolidación de estabilidad y diagnóstico.
- refactor: Arquitectura de base de datos local y políticas de logs.
- arch(sqlite): Aislamiento total de las conexiones SQLite en SQL_Auth y SQL_Logs. Esto protege las operaciones de autenticación críticas de la alta carga de I/O generada por el subsistema de logs.
- feat(logs): Implementación de modo de almacenamiento flexible para logs (disco o en memoria), mejorando la capacidad de testing.
- refactor(logs): Se estandariza el límite de retención de registros a 10,000 para todas las tablas de logs, y se renombra la subrutina de limpieza a borraArribaDe10000Logs.
This commit is contained in:
jaguerrau
2025-10-29 05:25:49 -06:00
parent 4c7639f867
commit 9c9e2975e9
12 changed files with 1390 additions and 1374 deletions

View File

@@ -22,6 +22,16 @@ Sub Process_Globals
' - Que en el reporte de "Queries lentos" se pueda especificar de cuanto tiempo, ahorita esta solo de la ultima hora, pero que se pueda seleccionar desde una
' lista, por ejemplo 15, 30, 45 y 60 minutos antes.
- VERSION 5.10.27
feat(arquitectura): Consolidación de estabilidad y diagnóstico.
refactor: Arquitectura de base de datos local y políticas de logs.
- arch(sqlite): Aislamiento total de las conexiones SQLite en SQL_Auth y SQL_Logs. Esto protege las operaciones de autenticación críticas de la alta carga de I/O generada por el subsistema de logs.
- feat(logs): Implementación de modo de almacenamiento flexible para logs (disco o en memoria), mejorando la capacidad de testing.
- fix(manager): Se corrige la inestabilidad del reporte /slowqueries (NumberFormatException) al omitir parámetros y se habilita el ordenamiento dinámico de las columnas.
- refactor(logs): Se estandariza el límite de retención de registros a 10,000 para todas las tablas de logs, y se renombra la subrutina de limpieza a borraArribaDe10000Logs.
' - VERSION: 5.10.25
' - refactor(hikari): Migración completa de C3P0 a HikariCP. Corrección de Hot-Swap y estabilización de la concurrencia.
' - El cambio principal es la sustitución del pool de conexiones C3P0 por HikariCP (versión 4.0.3). Esto resuelve problemas de estabilidad y reduce el overhead de sincronización, moviendo la infraestructura de pooling a un estándar industrial más robusto y rápido.

View File

@@ -14,7 +14,7 @@ Public Sub Initialize
End Sub
Public Sub Handle(req As ServletRequest, resp As ServletResponse)
Log("--- CHANGEPASSHANDLER FUE LLAMADO ---") ' <--- ¡PON ESTA LÍNEA AQUÍ!
Log("--- CHANGEPASSHANDLER FUE LLAMADO ---")
If req.GetSession.GetAttribute2("user_is_authorized", False) = False Then
resp.SendRedirect("/login")
Return
@@ -26,27 +26,26 @@ Public Sub Handle(req As ServletRequest, resp As ServletResponse)
Dim confirmPass As String = req.GetParameter("confirm_password")
If newPass <> confirmPass Then
resp.Write("<script>alert('Error: La nueva contraseña no coincide con la confirmación.'); history.back();</script>")
resp.Write("Las contraseñas no coinciden.")
Return
End If
Try
Dim storedHash As String = Main.SQL1.ExecQuerySingleResult2("SELECT password_hash FROM users WHERE username = ?", Array As String(currentUser))
' 1. Verification of the current password hash using SQL_Auth.
Dim storedHash As String = Main.SQL_Auth.ExecQuerySingleResult2("SELECT password_hash FROM users WHERE username = ?", Array As String(currentUser))
Log("Valor de la BD (storedHash): " & storedHash)
If storedHash = Null Or bc.checkpw(currentPass, storedHash) = False Then ' <<--- CAMBIO CLAVE AQUÍ
resp.Write("<script>alert('Error: La contraseña actual es incorrecta.'); history.back();</script>")
If storedHash = Null Or bc.checkpw(currentPass, storedHash) = False Then
resp.Write("Contraseña actual incorrecta.")
Return
End If
' <<--- CORRECCIÓN 2: Usamos el método seguro y consistente con 'Main'.
' 2. Hashing and updating the new password using SQL_Auth.
Dim newHashedPass As String = bc.hashpw(newPass, bc.gensalt)
Main.SQL1.ExecNonQuery2("UPDATE users SET password_hash = ? WHERE username = ?", Array As Object(newHashedPass, currentUser))
resp.Write("<script>alert('Contraseña actualizada correctamente.'); window.location.href='/manager';</script>")
Main.SQL_Auth.ExecNonQuery2("UPDATE users SET password_hash = ? WHERE username = ?", Array As Object(newHashedPass, currentUser))
resp.Write("Contraseña cambiada exitosamente.")
Catch
Log(LastException)
resp.Write("<script>alert('Error del servidor al intentar cambiar la contraseña.'); history.back();</script>")
resp.Write("Error interno al cambiar la contraseña.")
End Try
End Sub

View File

@@ -1,184 +0,0 @@
B4J=true
Group=Default Group
ModulesStructureVersion=1
Type=Class
Version=6.01
@EndOfDesignText@
'Class module
'Author: Oliver Ackermann
'Created on: 2018/05/08
Sub Class_Globals
Private pools As Map
Private pool As Object 'Connection pool object
Private poolJO As JavaObject
Private standardConnection As Boolean
Private activePool As Boolean
Private defaultPoolType As String
End Sub
'Initialze ConnectionPoolWrapper
Public Sub Initialize
Reset
End Sub
Private Sub Reset
' Dim H2Pool As H2ConnectionPool
' H2Pool.Initialize
' Dim HSQLPool As HSQLDBConnectionPool
' HSQLPool.Initialize
Dim HikariPool As HikariConnectionPool
HikariPool.Initialize
' Dim C3P0Pool As C3P0ConnectionPool
' C3P0Pool.Initialize
' Dim TomcatPool As TomcatConnectionPool
' TomcatPool.Initialize
' Dim ViburPool As ViburConnectionPool
' ViburPool.Initialize
pools.Initialize
' pools.Put("H2", H2Pool)
' pools.Put("HSQLDB", HSQLPool)
pools.Put("Hikari", HikariPool)
' pools.Put("C3P0", C3P0Pool)
' pools.Put("Tomcat", TomcatPool)
' pools.Put("Vibur", ViburPool)
defaultPoolType = "Hikari" ' <-- Esto ya estaba bien, pero lo dejamos incondicionalmente [5]
standardConnection = False
activePool = False
End Sub
'Create a given pool type
' poolType - type of connection pool to create.
' driverClass - JDBC driver class.
' jdbcUrl - JDBC connection url.
' aUser / aPassword - Connection credentials.
' poolSize - Maximum size of connection pool.
Public Sub CreatePool(poolType As String, driverClass As String, jdbcUrl As String, aUser As String, aPassword As String, poolSize As Int)
'Check poolType
If pools.ContainsKey(poolType) = False Then
Log($"Warning: Connection pool type ${poolType} not supported"$)
If poolType = defaultPoolType Then
Log($" Error: Default pool type ${poolType} not supported."$)
Return
Else
Log($" Switching to default pool type ${defaultPoolType}"$)
If pools.ContainsKey(defaultPoolType) Then
poolType = defaultPoolType
Else
Log($" Error: Default pool type ${defaultPoolType} not supported."$)
Return
End If
End If
End If
Dim thePool As Object = pools.Get(poolType)
'Check jdbcUrl
If jdbcUrl.StartsWith("jdbc:") = False Then
Log($"Error: Invalid JDBC URL: ${jdbcUrl}"$)
Return
End If
If SubExists(thePool, "SupportUrl") And CallSub2(thePool, "SupportUrl", jdbcUrl) = False Then
Log($"Error: Unsupported JDBC URL: ${jdbcUrl}"$)
Return
End If
'Initialize pool
Dim options As Map
options.Initialize
options.Put("driver", driverClass)
options.Put("url", jdbcUrl)
options.Put("user", aUser)
options.Put("password", aPassword)
options.Put("poolsize", poolSize)
CallSub2(thePool, "CreatePool3", options)
'See if we can use own GetConnection without use of CallSub
If SubExists(thePool, "IsStandardConnection") And CallSub(thePool, "IsStandardConnection") Then
standardConnection = True
Log($"Info: ${poolType} supports getConnection JavaObject method."$)
poolJO = CallSub(thePool, "GetPoolJO")
Else
Log($"Info: ${poolType} does not support getConnection JavaObject Method."$)
Log($"Info: Checking if ${poolType} has alternative GetConnection method."$)
If SubExists(thePool, "GetConnection") = False Then
Log($"Error: ${poolType} has no GetConnection method."$)
Return
End If
End If
'Everthing looks good sofar
pool = thePool
activePool = True
End Sub
Public Sub SetProperties(properties As Map)
CallSub2(pool, "SetProperties", properties)
End Sub
'Retrieves a connection from the pool. Make sure to close the connection when you are done with it.
Public Sub GetConnection As SQL
Dim newSQL As SQL
If standardConnection Then
Dim jo As JavaObject = newSQL
jo.SetField("connection", poolJO.RunMethod("getConnection", Null))
Else
newSQL = CallSub(pool, "GetConnection")
End If
Return newSQL
End Sub
' Devuelve el wrapper del pool (ej. HikariConnectionPool) basado en el tipo.
Public Sub GetPool(poolType As String) As Object
If pools.ContainsKey(poolType) Then
Return pools.Get(poolType)
Else
' Si el poolType no está en el mapa, devolvemos Null.
Return Null
End If
End Sub
'Closes all unused pooled connections.
Public Sub ClosePool
Log("ConnectionManager ClosePool")
If activePool Then
Log("About to call Connection Pool's ClosePool")
CallSub(pool, "ClosePool")
Log("Returned from Connection Pool's ClosePool")
End If
Log("Re-initializing Connection Pool Manager")
Reset
Log("Done")
End Sub
' Cierra un pool específico identificado por su tipo (ej. "Hikari", "C3P0").
' Esto permite el cierre granular necesario para el Hot-Swap.
Public Sub ClosePoolByType(poolType As String)
Log($"ConnectionManager: Intentando cerrar pool de tipo ${poolType}"$)
If pools.ContainsKey(poolType) Then
Dim thePool As Object = pools.Get(poolType)
If thePool <> Null And SubExists(thePool, "ClosePool") Then
Log($"ConnectionManager: Delegando ClosePool a ${poolType} wrapper."$)
CallSub(thePool, "ClosePool")
' NOTA: Generalmente, mantenemos el wrapper en el mapa 'pools' (no lo removemos)
' porque la inicialización del Manager crea estos objetos una sola vez al inicio.
Else
Log($"ERROR: Wrapper de pool ${poolType} no disponible o no tiene método ClosePool."$)
End If
Else
Log($"ADVERTENCIA: Tipo de pool ${poolType} no registrado en el Manager."$)
End If
End Sub

View File

@@ -4,139 +4,134 @@ ModulesStructureVersion=1
Type=Class
Version=10.3
@EndOfDesignText@
' Módulo de clase: DBHandlerJSON
' Este handler se encarga de procesar las peticiones HTTP que esperan o envían datos en formato JSON.
' Es ideal para clientes web (JavaScript, axios, etc.) o servicios que interactúan con el servidor
' mediante un API RESTful. Soporta tanto GET con JSON en un parámetro 'j' como POST con JSON
' en el cuerpo de la petición.
' Class module: DBHandlerJSON
' This handler is responsible for processing HTTP requests that expect or send data in JSON format.
' It is ideal for web clients (JavaScript, axios, etc.) or services that interact with the server
' via a RESTful API. It supports both GET with JSON in a 'j' parameter and POST with JSON
' in the request body.
Sub Class_Globals
' Declara una variable privada para mantener una instancia del conector RDC.
' Este objeto maneja la comunicación con la base de datos específica de la petición.
' Declares a private variable to hold an instance of the RDC connector.
' This object manages communication with the request's specific database.
Private Connector As RDCConnector
End Sub
' Subrutina de inicialización de la clase. Se llama cuando se crea un objeto de esta clase.
' Class initialization subroutine. Called when an object of this class is created.
Public Sub Initialize
' No se requiere inicialización específica para esta clase en este momento.
' No specific initialization is required for this class at this time.
End Sub
' Este es el método principal que maneja las peticiones HTTP entrantes (req) y prepara la respuesta (resp).
' This is the main method that handles incoming HTTP requests (req) and prepares the response (resp).
Sub Handle(req As ServletRequest, resp As ServletResponse)
' --- Headers CORS (Cross-Origin Resource Sharing) ---
' Estos encabezados son esenciales para permitir que aplicaciones web (clientes)
' alojadas en diferentes dominios puedan comunicarse con este servidor.
resp.SetHeader("Access-Control-Allow-Origin", "*") ' Permite peticiones desde cualquier origen.
resp.SetHeader("Access-Control-Allow-Methods", "GET, POST, OPTIONS") ' Métodos HTTP permitidos.
resp.SetHeader("Access-Control-Allow-Headers", "Content-Type") ' Encabezados permitidos.
' CORS (Cross-Origin Resource Sharing) Headers
' These headers are essential to allow web applications (clients)
' hosted on different domains to communicate with this server.
resp.SetHeader("Access-Control-Allow-Origin", "*") ' Allows requests from any origin.
resp.SetHeader("Access-Control-Allow-Methods", "GET, POST, OPTIONS") ' Allowed HTTP methods.
resp.SetHeader("Access-Control-Allow-Headers", "Content-Type") ' Allowed headers.
' Las peticiones OPTIONS son pre-vuelos de CORS y no deben procesar lógica de negocio ni contadores.
' OPTIONS requests are CORS pre-flights and should not process business logic or counters.
If req.Method = "OPTIONS" Then
Return ' Salimos directamente para estas peticiones.
Return ' We exit directly for these requests.
End If
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 ' Record the request start time to calculate duration.
' Declaraciones de variables con alcance en toda la subrutina para asegurar la limpieza final.
Dim con As SQL ' La conexión a la BD, se inicializará más tarde.
Dim queryNameForLog As String = "unknown_json_command" ' Nombre del comando para el log, con valor por defecto.
Dim duration As Long ' La duración total de la petición, calculada antes del log.
Dim poolBusyConnectionsForLog As Int = 0 ' Contiene el número de conexiones ocupadas del pool.
Dim finalDbKey As String = "DB1" ' Identificador de la base de datos, con valor por defecto "DB1".
Dim requestsBeforeDecrement As Int = 0 ' Contador de peticiones activas antes de decrementar, inicializado en 0.
' Variable declarations with scope throughout the sub to ensure final cleanup.
Dim con As SQL ' The DB connection, will be initialized later.
Dim queryNameForLog As String = "unknown_json_command" ' Command name for the log, with a default value.
Dim duration As Long ' The total request duration, calculated before logging.
Dim poolBusyConnectionsForLog As Int = 0 ' Contains the number of busy connections from the pool.
Dim finalDbKey As String = "DB1" ' Database identifier, defaulting to "DB1".
Dim requestsBeforeDecrement As Int = 0 ' Active request counter before decrementing, initialized to 0.
Dim Total As Int = 0
Try ' --- INICIO: Bloque Try que envuelve la lógica principal del Handler ---
Try ' --- START: Try block wrapping the main Handler logic ---
Dim jsonString As String
' <<<< INICIO: Lógica para manejar peticiones POST con JSON en el cuerpo >>>>
' Logic to handle POST requests with JSON in the body
If req.Method = "POST" And req.ContentType.Contains("application/json") Then
' Si es un POST con JSON en el cuerpo, leemos directamente del InputStream.
' If it's a POST with JSON in the body, read directly from the InputStream.
Dim Is0 As InputStream = req.InputStream
Dim bytes() As Byte = Bit.InputStreamToBytes(Is0) ' Lee el cuerpo completo de la petición.
jsonString = BytesToString(bytes, 0, bytes.Length, "UTF8") ' Convierte los bytes a una cadena JSON.
Is0.Close ' Cierra explícitamente el InputStream para liberar recursos.
Dim bytes() As Byte = Bit.InputStreamToBytes(Is0) ' Read the entire request body.
jsonString = BytesToString(bytes, 0, bytes.Length, "UTF8") ' Convert bytes to a JSON string.
Is0.Close ' Explicitly close the InputStream to free resources.
Else
' De lo contrario, asumimos que el JSON viene en el parámetro 'j' de la URL (método legacy/GET).
' Otherwise, assume the JSON comes in the 'j' parameter of the URL (legacy/GET method).
jsonString = req.GetParameter("j")
End If
' <<<< FIN: Lógica para manejar peticiones POST con JSON en el cuerpo >>>>
' Validación inicial: Si no hay JSON, se envía un error 400.
' Initial validation: If there is no JSON, send a 400 error.
If jsonString = Null Or jsonString = "" Then
Dim ErrorMsg As String = "Falta el parámetro 'j' en el URL o el cuerpo JSON en la petición."
SendErrorResponse(resp, 400, ErrorMsg)
Main.LogServerError("ERROR", "DBHandlerJSON.Handle", ErrorMsg, finalDbKey, queryNameForLog, req.RemoteAddress) ' <-- Nuevo Log
Main.LogServerError("ERROR", "DBHandlerJSON.Handle", ErrorMsg, finalDbKey, queryNameForLog, req.RemoteAddress)
duration = DateTime.Now - start
CleanupAndLog(finalDbKey, queryNameForLog, duration, req.RemoteAddress, requestsBeforeDecrement, poolBusyConnectionsForLog, con)
Return
End If
Dim parser As JSONParser
parser.Initialize(jsonString) ' Inicializa el parser JSON con la cadena recibida.
Dim RootMap As Map = parser.NextObject ' Parsea el JSON a un objeto Map.
parser.Initialize(jsonString) ' Initialize the JSON parser with the received string.
Dim RootMap As Map = parser.NextObject ' Parse the JSON into a Map object.
Dim execType As String = RootMap.GetDefault("exec", "") ' Obtiene el tipo de ejecución (ej. "ExecuteQuery").
Dim execType As String = RootMap.GetDefault("exec", "") ' Get the execution type (e.g., "ExecuteQuery").
' Obtiene el nombre de la query. Si no está en "query", busca en "exec".
' Get the query name. If not in "query", look in "exec".
queryNameForLog = RootMap.GetDefault("query", "")
If queryNameForLog = "" Then queryNameForLog = RootMap.GetDefault("exec", "unknown_json_command")
Dim paramsList As List = RootMap.Get("params") ' Obtiene la lista de parámetros para la query.
Dim paramsList As List = RootMap.Get("params") ' Get the list of parameters for the query.
If paramsList = Null Or paramsList.IsInitialized = False Then
paramsList.Initialize ' Si no hay parámetros, inicializa una lista vacía.
paramsList.Initialize ' If no parameters, initialize an empty list.
End If
' <<<< ¡CORRECCIÓN CLAVE: RESOLVEMOS finalDbKey del JSON ANTES de usarla para los contadores! >>>>
' Esto asegura que el contador y el conector usen la DB correcta.
' Resolve finalDbKey from the JSON BEFORE using it for counters.
' This ensures the counter and connector use the correct DB.
If RootMap.Get("dbx") <> Null Then finalDbKey = RootMap.Get("dbx")
' <<<< ¡FIN DE CORRECCIÓN CLAVE! >>>>
' --- INICIO: Conteo de peticiones activas para esta finalDbKey (Incrementar) ---
' Este bloque incrementa un contador global que rastrea cuántas peticiones están
' activas para una base de datos específica en un momento dado.
' 1. Aseguramos que el valor inicial sea un Int y lo recuperamos como Int (usando .As(Int)).
' --- START: Active request count for this finalDbKey (Increment) ---
' This block increments a global counter tracking how many requests
' are active for a specific database at any given time.
' 1. Ensure the initial value is an Int and retrieve it as Int (using .As(Int)).
Dim currentCountFromMap As Int = GlobalParameters.ActiveRequestsCountByDB.GetDefault(finalDbKey, 0).As(Int)
GlobalParameters.ActiveRequestsCountByDB.Put(finalDbKey, currentCountFromMap + 1)
' requestsBeforeDecrement es el valor del contador justo después de que esta petición lo incrementa.
' Este es el valor que se registrará en la tabla 'query_logs'.
' requestsBeforeDecrement is the counter value right after this request increments it.
' This is the value that will be recorded in the 'query_logs' table.
requestsBeforeDecrement = currentCountFromMap + 1
' Los logs de depuración para el incremento del contador pueden ser descomentados para una depuración profunda.
' Log($"[DEBUG] Handle Increment (JSON): dbKey=${finalDbKey}, currentCountFromMap=${currentCountFromMap}, requestsBeforeDecrement=${requestsBeforeDecrement}, Map state: ${GlobalParameters.ActiveRequestsCountByDB}"$)
' --- FIN: Conteo de peticiones activas ---
' --- END: Active request count ---
' Inicializa el Connector con la finalDbKey resuelta.
' Initialize the Connector with the resolved finalDbKey.
Connector = Main.Connectors.Get(finalDbKey)
' Validación: Si el dbKey no es válido o no está configurado en Main.listaDeCP.
' Validation: If the dbKey is invalid or not configured in Main.listaDeCP.
If Main.listaDeCP.IndexOf(finalDbKey) = -1 Then
Dim ErrorMsg As String = "Parámetro 'DB' inválido. El nombre '" & finalDbKey & "' no es válido."
SendErrorResponse(resp, 400, ErrorMsg)
Main.LogServerError("ERROR", "DBHandlerJSON.Handle", ErrorMsg, finalDbKey, queryNameForLog, req.RemoteAddress) ' <-- Nuevo Log
Main.LogServerError("ERROR", "DBHandlerJSON.Handle", ErrorMsg, finalDbKey, queryNameForLog, req.RemoteAddress)
duration = DateTime.Now - start
CleanupAndLog(finalDbKey, queryNameForLog, duration, req.RemoteAddress, requestsBeforeDecrement, poolBusyConnectionsForLog, con)
Return
End If
con = Connector.GetConnection(finalDbKey) ' ¡La conexión a la BD se obtiene aquí del pool de conexiones!
con = Connector.GetConnection(finalDbKey) ' The DB connection is obtained here from the connection pool!
' <<<< ¡CAPTURAMOS BUSY_CONNECTIONS INMEDIATAMENTE DESPUÉS DE OBTENER LA CONEXIÓN! >>>>
' Este bloque captura el número de conexiones actualmente ocupadas en el pool
' *después* de que esta petición ha obtenido la suya.
' Capture BUSY_CONNECTIONS IMMEDIATELY AFTER getting the connection.
' This block captures the number of connections currently busy in the pool
' *after* this request has obtained its own.
If Connector.IsInitialized Then
Dim poolStats As Map = Connector.GetPoolStats
If poolStats.ContainsKey("BusyConnections") Then
' <<<< ¡CORRECCIÓN CLAVE: Aseguramos que el valor sea Int! >>>>
poolBusyConnectionsForLog = poolStats.Get("BusyConnections").As(Int) ' Capturamos el valor.
' Ensure the value is Int!
poolBusyConnectionsForLog = poolStats.Get("BusyConnections").As(Int) ' We capture the value.
' Log($">>>>>>>>>> ${poolStats.Get("BusyConnections")} "$)
End If
End If
' <<<< ¡FIN DE CAPTURA! >>>>
Dim cachedStatsJSON As Map = Main.LatestPoolStats.Get(finalDbKey).As(Map)
If cachedStatsJSON.IsInitialized Then
' Los valores ya fueron capturados: poolBusyConnectionsForLog y requestsBeforeDecrement
' Values were already captured: poolBusyConnectionsForLog and requestsBeforeDecrement
cachedStatsJSON.Put("BusyConnections", poolBusyConnectionsForLog)
cachedStatsJSON.Put("HandlerActiveRequests", requestsBeforeDecrement)
If poolStats.ContainsKey("TotalConnections") Then
@@ -145,167 +140,161 @@ Sub Handle(req As ServletRequest, resp As ServletResponse)
If poolStats.ContainsKey("IdleConnections") Then
cachedStatsJSON.Put("IdleConnections", poolStats.Get("IdleConnections"))
End If
' Re-escribir el mapa en el cache global (es Thread-Safe)
' Re-write the map to the global cache (it's Thread-Safe)
Main.LatestPoolStats.Put(finalDbKey, cachedStatsJSON)
' Log(Main.LatestPoolStats)
End If
' Log($"Total: ${poolStats.Get("TotalConnections")}, Idle: ${poolStats.Get("IdleConnections")}, busy: ${poolBusyConnectionsForLog}, active: ${requestsBeforeDecrement}"$)
' Obtiene la sentencia SQL correspondiente al nombre del comando desde config.properties.
' Get the SQL statement corresponding to the command name from config.properties.
Dim sqlCommand As String = Connector.GetCommand(finalDbKey, queryNameForLog)
' Validación: Si el comando SQL no fue encontrado en la configuración.
' Validation: If the SQL command was not found in the configuration.
If sqlCommand = Null Or sqlCommand = "null" Or sqlCommand.Trim = "" Then
Dim errorMessage As String = $"El comando '${queryNameForLog}' no fue encontrado en el config.properties de '${finalDbKey}'."$
Log(errorMessage)
Main.LogServerError("ERROR", "DBHandlerJSON.Handle", errorMessage, finalDbKey, queryNameForLog, req.RemoteAddress) ' <-- Nuevo Log
Main.LogServerError("ERROR", "DBHandlerJSON.Handle", errorMessage, finalDbKey, queryNameForLog, req.RemoteAddress)
SendErrorResponse(resp, 400, errorMessage)
duration = DateTime.Now - start
CleanupAndLog(finalDbKey, queryNameForLog, duration, req.RemoteAddress, requestsBeforeDecrement, poolBusyConnectionsForLog, con)
Return
End If
' --- Lógica para ejecutar diferentes tipos de comandos basados en el parámetro 'execType' ---
' Logic to execute different command types based on the 'execType' parameter
If execType.ToLowerCase = "executequery" Then
' --- INICIO VALIDACIÓN DE PARÁMETROS CENTRALIZADA ---
' --- START CENTRALIZED PARAMETER VALIDATION ---
Dim validationResult As ParameterValidationResult = ParameterValidationUtils.ValidateAndAdjustParameters(queryNameForLog, finalDbKey, sqlCommand, paramsList, Connector.IsParameterToleranceEnabled)
If validationResult.Success = False Then
SendErrorResponse(resp, 400, validationResult.ErrorMessage)
duration = DateTime.Now - start
CleanupAndLog(finalDbKey, queryNameForLog, duration, req.RemoteAddress, requestsBeforeDecrement, poolBusyConnectionsForLog, con)
Return ' Salida temprana.
Return ' Early exit.
End If
Dim rs As ResultSet
' Ejecuta la consulta SQL con la lista de parámetros validada.
' Execute the SQL query with the validated parameter list.
rs = con.ExecQuery2(sqlCommand, validationResult.ParamsToExecute)
' --- FIN VALIDACIÓN DE PARÁMETROS CENTRALIZADA ---
' --- END CENTRALIZED PARAMETER VALIDATION ---
Dim ResultList As List
ResultList.Initialize ' Lista para almacenar los resultados de la consulta.
Dim jrs As JavaObject = rs ' Objeto Java subyacente del ResultSet para metadatos.
Dim rsmd As JavaObject = jrs.RunMethod("getMetaData", Null) ' Metadatos del ResultSet.
Dim cols As Int = rsmd.RunMethod("getColumnCount", Null) ' Número de columnas.
ResultList.Initialize ' List to store query results.
Dim jrs As JavaObject = rs ' Underlying Java object of the ResultSet for metadata.
Dim rsmd As JavaObject = jrs.RunMethod("getMetaData", Null) ' ResultSet metadata.
Dim cols As Int = rsmd.RunMethod("getColumnCount", Null) ' Number of columns.
Do While rs.NextRow ' Itera sobre cada fila del resultado.
Do While rs.NextRow ' Iterate over each row in the result.
Dim RowMap As Map
RowMap.Initialize ' Mapa para almacenar los datos de la fila actual.
For i = 1 To cols ' Itera sobre cada columna.
Dim ColumnName As String = rsmd.RunMethod("getColumnName", Array(i)) ' Nombre de la columna.
Dim value As Object = jrs.RunMethod("getObject", Array(i)) ' Valor de la columna.
RowMap.Put(ColumnName, value) ' Añade la columna y su valor al mapa de la fila.
RowMap.Initialize ' Map to store the current row's data.
For i = 1 To cols ' Iterate over each column.
Dim ColumnName As String = rsmd.RunMethod("getColumnName", Array(i)) ' Column name.
Dim value As Object = jrs.RunMethod("getObject", Array(i)) ' Column value.
RowMap.Put(ColumnName, value) ' Add the column and its value to the row map.
Next
ResultList.Add(RowMap) ' Añade el mapa de la fila a la lista de resultados.
ResultList.Add(RowMap) ' Add the row map to the results list.
Loop
rs.Close ' Cierra el ResultSet.
SendSuccessResponse(resp, CreateMap("result": ResultList)) ' Envía la respuesta JSON de éxito.
rs.Close ' Close the ResultSet.
SendSuccessResponse(resp, CreateMap("result": ResultList)) ' Send the success JSON response.
Else If execType.ToLowerCase = "executecommand" Then
' --- INICIO VALIDACIÓN DE PARÁMETROS CENTRALIZADA ---
' --- START CENTRALIZED PARAMETER VALIDATION ---
Dim validationResult As ParameterValidationResult = ParameterValidationUtils.ValidateAndAdjustParameters(queryNameForLog, finalDbKey, sqlCommand, paramsList, Connector.IsParameterToleranceEnabled)
If validationResult.Success = False Then
SendErrorResponse(resp, 400, validationResult.ErrorMessage)
duration = DateTime.Now - start
CleanupAndLog(finalDbKey, queryNameForLog, duration, req.RemoteAddress, requestsBeforeDecrement, poolBusyConnectionsForLog, con)
Return ' Salida temprana.
Return ' Early exit.
End If
Dim affectedCount As Int = 1 ' Asumimos éxito (1) si ExecNonQuery2 no lanza una excepción.
con.ExecNonQuery2(sqlCommand, validationResult.ParamsToExecute) ' Ejecuta un comando con la lista de parámetros validada.
SendSuccessResponse(resp, CreateMap("affectedRows": affectedCount, "message": "Command executed successfully")) ' Envía confirmación de éxito.
' --- FIN VALIDACIÓN DE PARÁMETROS CENTRALIZADA ---
Dim affectedCount As Int = 1 ' Assume success (1) if ExecNonQuery2 doesn't throw an exception.
con.ExecNonQuery2(sqlCommand, validationResult.ParamsToExecute) ' Execute a command with the validated parameter list.
SendSuccessResponse(resp, CreateMap("affectedRows": affectedCount, "message": "Command executed successfully")) ' Send success confirmation.
' --- END CENTRALIZED PARAMETER VALIDATION ---
Else
Dim ErrorMsg As String = "Parámetro 'exec' inválido. '" & execType & "' no es un valor permitido."
SendErrorResponse(resp, 400, ErrorMsg)
Main.LogServerError("ERROR", "DBHandlerJSON.Handle", ErrorMsg, finalDbKey, queryNameForLog, req.RemoteAddress) ' <-- Nuevo Log
' El flujo continúa hasta la limpieza final si no hay un Return explícito.
Main.LogServerError("ERROR", "DBHandlerJSON.Handle", ErrorMsg, finalDbKey, queryNameForLog, req.RemoteAddress)
' Flow continues to final cleanup if there is no explicit Return.
End If
Catch ' --- CATCH: Maneja errores generales de ejecución o de SQL/JSON ---
Log(LastException) ' Registra la excepción completa en el log.
Main.LogServerError("ERROR", "DBHandlerJSON.Handle", LastException.Message, finalDbKey, queryNameForLog, req.RemoteAddress) ' <-- Nuevo Log
SendErrorResponse(resp, 500, LastException.Message) ' Envía un error 500 al cliente.
queryNameForLog = "error_processing_json" ' Para registrar que hubo un error en el log.
End Try ' --- FIN: Bloque Try principal ---
Catch ' --- CATCH: Handle general execution or SQL/JSON errors ---
Log(LastException) ' Log the full exception.
Main.LogServerError("ERROR", "DBHandlerJSON.Handle", LastException.Message, finalDbKey, queryNameForLog, req.RemoteAddress)
SendErrorResponse(resp, 500, LastException.Message) ' Send a 500 error to the client.
queryNameForLog = "error_processing_json" ' To log that there was an error.
End Try ' --- END: Main Try block ---
' --- Lógica de logging y limpieza final (para rutas de ejecución normal o después de Catch) ---
' 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.
duration = DateTime.Now - start ' Calcula la duración total de la petición.
' Llama a la subrutina centralizada para registrar el rendimiento y limpiar recursos.
' --- Final logging and cleanup logic (for normal execution paths or after Catch) ---
' This block ensures that, regardless of how the request ends (success or error),
' the duration is calculated and the cleanup and logging subs are called.
duration = DateTime.Now - start ' Calculate the total request duration.
' Call the centralized subroutine to log performance and clean up resources.
CleanupAndLog(finalDbKey, queryNameForLog, duration, req.RemoteAddress, requestsBeforeDecrement, poolBusyConnectionsForLog, con)
End Sub
' --- NUEVA SUBRUTINA: Centraliza el logging de rendimiento y la limpieza de recursos ---
' Esta subrutina es llamada por Handle en todos los puntos de salida, asegurando
' que los contadores se decrementen y las conexiones se cierren de forma consistente.
' --- Subroutine: Centralizes performance logging and resource cleanup ---
' This subroutine is called by Handle at all exit points, ensuring
' that counters are decremented and connections are closed consistently.
Private Sub CleanupAndLog(dbKey As String, qName As String, durMs As Long, clientIp As String, handlerReqs As Int, poolBusyConns As Int, conn As SQL)
' Los logs de depuración para CleanupAndLog pueden ser descomentados para una depuración profunda.
' Log($"[DEBUG] CleanupAndLog Entry (JSON): dbKey=${dbKey}, handlerReqs=${handlerReqs}, Map state: ${GlobalParameters.ActiveRequestsCountByDB}"$)
' 1. Llama a la subrutina centralizada en Main para registrar el rendimiento en SQLite.
' 1. Call the centralized subroutine in Main to log performance to SQLite.
Main.LogQueryPerformance(qName, durMs, dbKey, clientIp, handlerReqs, poolBusyConns)
' <<<< ¡CORRECCIÓN CLAVE: Aseguramos que currentCount sea Int al obtenerlo del mapa! >>>>
' 2. Decrementa el contador de peticiones activas para esta dbKey de forma robusta.
' 2. Robustly decrement the active request counter for this dbKey.
Dim currentCount As Int = GlobalParameters.ActiveRequestsCountByDB.GetDefault(dbKey, 0).As(Int)
' Log($"[DEBUG] CleanupAndLog Before Decrement (JSON): dbKey=${dbKey}, currentCount (as Int)=${currentCount}, Map state: ${GlobalParameters.ActiveRequestsCountByDB}"$)
If currentCount > 0 Then
' Si el contador es positivo, lo decrementamos.
' If the counter is positive, decrement it.
GlobalParameters.ActiveRequestsCountByDB.Put(dbKey, currentCount - 1)
Else
' 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.
' If the counter is already 0 or negative (which shouldn't happen with current logic,
' but is handled for robustness), we log a warning and ensure it is 0.
' Log($"ADVERTENCIA: Intento de decrementar ActiveRequestsCountByDB para ${dbKey} que ya estaba en ${currentCount}. Asegurando a 0."$)
GlobalParameters.ActiveRequestsCountByDB.Put(dbKey, 0)
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}"$)
' <<<< ¡FIN DE CORRECCIÓN CLAVE! >>>>
' 3. Asegura que la conexión a la BD siempre se cierre y se devuelva al pool de conexiones.
' 3. Ensure the DB connection is always closed and returned to the connection pool.
If conn <> Null And conn.IsInitialized Then conn.Close
End Sub
' --- Subrutinas de ayuda para respuestas JSON ---
' --- Helper subroutines for JSON responses ---
' Construye y envía una respuesta JSON de éxito.
' resp: El objeto ServletResponse para enviar la respuesta.
' dataMap: Un mapa que contiene los datos a incluir en la respuesta JSON.
' Builds and sends a success JSON response.
' resp: The ServletResponse object to send the response.
' dataMap: A map containing the data to include in the JSON response.
Private Sub SendSuccessResponse(resp As ServletResponse, dataMap As Map)
' Añade el campo "success": true al mapa de datos para indicar que todo salió bien.
' Add the "success": true field to the data map to indicate everything went well.
dataMap.Put("success", True)
' Crea un generador de JSON.
' Create a JSON generator.
Dim jsonGenerator As JSONGenerator
jsonGenerator.Initialize(dataMap)
' Establece el tipo de contenido de la respuesta a "application/json".
' Set the response content type to "application/json".
resp.ContentType = "application/json"
' Escribe la cadena JSON generada en el cuerpo de la respuesta HTTP.
' Write the generated JSON string to the HTTP response body.
resp.Write(jsonGenerator.ToString)
End Sub
' Construye y envía una respuesta JSON de error.
' resp: El objeto ServletResponse para enviar la respuesta.
' statusCode: El código de estado HTTP (ej. 400 para error del cliente, 500 para error del servidor).
' errorMessage: El mensaje de error que se enviará al cliente.
' Builds and sends an error JSON response.
' resp: The ServletResponse object to send the response.
' statusCode: The HTTP status code (e.g., 400 for client error, 500 for server error).
' errorMessage: The error message to be sent to the client.
Private Sub SendErrorResponse(resp As ServletResponse, statusCode As Int, errorMessage As String)
' Personaliza el mensaje de error si es un error común de parámetros de Oracle o JDBC.
' Customize the error message if it's a common Oracle or JDBC parameter error.
If errorMessage.Contains("Índice de columnas no válido") Or errorMessage.Contains("ORA-17003") Then
errorMessage = "NUMERO DE PARAMETROS EQUIVOCADO: " & errorMessage
End If
' Crea un mapa con el estado de error y el mensaje.
' Create a map with the error status and message.
Dim resMap As Map = CreateMap("success": False, "error": errorMessage)
' Genera la cadena JSON a partir del mapa.
' Generate the JSON string from the map.
Dim jsonGenerator As JSONGenerator
jsonGenerator.Initialize(resMap)
' Establece el código de estado HTTP (ej. 400 para error del cliente, 500 para error del servidor).
' Set the HTTP status code (e.g., 400 for client error, 500 for server error).
resp.Status = statusCode
' Establece el tipo de contenido y escribe la respuesta de error.
' Set the content type and write the error response.
resp.ContentType = "application/json"
resp.Write(jsonGenerator.ToString)
End Sub
End Sub

View File

@@ -19,18 +19,15 @@ Public Sub Handle(req As ServletRequest, resp As ServletResponse)
Dim p As String = req.GetParameter("password")
Log(u)
Try
' Buscamos el hash en la base de datos usando el usuario limpio
Dim storedHash As String = Main.SQL1.ExecQuerySingleResult2("SELECT password_hash FROM users WHERE username = ?", Array As String(u))
' Buscamos el hash en la base de datos de autenticación (SQL_Auth)
Dim storedHash As String = Main.SQL_Auth.ExecQuerySingleResult2("SELECT password_hash FROM users WHERE username = ?", Array As String(u))
Log($"${storedHash}"$)
' Log($"${bc.checkpw(p, storedHash)}"$)
' Verificamos la contraseña contra el hash
If storedHash <> Null And bc.checkpw(p, storedHash) Then
' CREDENCIALES CORRECTAS
' 1. Autorizamos la sesión
req.GetSession.SetAttribute("user_is_authorized", True)
' 2. ¡Y guardamos el nombre de usuario! (Esta es la línea que faltaba)
req.GetSession.SetAttribute("username", u)
resp.SendRedirect("/manager")
Else
' Credenciales incorrectas

File diff suppressed because it is too large Load Diff

View File

@@ -4,7 +4,7 @@ ModulesStructureVersion=1
Type=Class
Version=6.01
@EndOfDesignText@
'Class module
'Class module : HikariConnectionPool
'Author: Oliver Ackermann
'Created on: 2018/05/07
'Based on: https://github.com/AnywhereSoftware/B4J_Server/blob/master/src/anywheresoftware/b4j/object/ConnectionPool.java
@@ -27,7 +27,7 @@ End Sub
Public Sub Initialize
Dim properties As Map
properties.Initialize
'
'Frequently used
properties.Put("autocommit", "boolean")
properties.Put("connectiontimeout", "int")
@@ -73,7 +73,6 @@ Public Sub CreatePool2(driverClass As String, jdbcUrl As String, aUser As String
poolJO.RunMethod("setUsername", Array As Object(aUser))
poolJO.RunMethod("setPassword", Array As Object(aPassword))
If poolSize > 0 Then poolJO.RunMethod("setMaximumPoolSize", Array As Object(poolSize))
End Sub
Public Sub CreatePool3(options As Map)
@@ -85,7 +84,7 @@ End Sub
'that HikariConfig object.
Public Sub SetProperties(properties As Map)
' Valores óptimos por defecto para HikariCP (en milisegundos)
' Optimal default values for HikariCP (in milliseconds)
Private const DEFAULT_MAX_LIFE As Long = 1750000
Private const DEFAULT_CONN_TIMEOUT As Long = 30000
Private const DEFAULT_LEAK_THR As Long = 35000
@@ -93,112 +92,105 @@ Public Sub SetProperties(properties As Map)
Private const DEFAULT_MIN_IDLE As Int = 10
Private const DEFAULT_MAX_SIZE As Int = 10
Private const DEFAULT_KEEP_ALIVE_TIME As Long = 300000
Dim rawValue As String
Dim processedKeys As List
processedKeys.Initialize
Dim maxSize As Int ' Variable temporal para el tamaño máximo calculado
' --- INICIO: Procesamiento de Propiedades Críticas ---
' 1. MaxLifetime (Long) - Clave: maxlifetime
Dim maxSize As Int ' Temporary variable for the calculated maximum size
' 1. MaxLifetime (Long) - Key: maxlifetime
Dim maxLife As Long
If properties.ContainsKey("maxlifetime") Then
rawValue = properties.Get("maxlifetime").As(String).Trim
maxLife = rawValue
poolJO.RunMethod("setMaxLifetime", Array As Object(maxLife))
Log($"Ponemos (LONG, Config): MaxLifetime, ${maxLife}"$)
Log($"Ponemos MaxLifetime: ${maxLife}"$)
Else
maxLife = DEFAULT_MAX_LIFE
poolJO.RunMethod("setMaxLifetime", Array As Object(maxLife))
Log($"Ponemos (LONG, Default): MaxLifetime, ${DEFAULT_MAX_LIFE}"$)
End If
processedKeys.Add("maxlifetime")
MaxLifeConfig = maxLife ' <-- ALMACENAMIENTO CRÍTICO
' 2. ConnectionTimeout (Long) - Clave: connectiontimeout
MaxLifeConfig = maxLife ' Store the config value
' 2. ConnectionTimeout (Long) - Key: connectiontimeout
Dim connTimeout As Long
If properties.ContainsKey("connectiontimeout") Then
rawValue = properties.Get("connectiontimeout").As(String).Trim
connTimeout = rawValue
poolJO.RunMethod("setConnectionTimeout", Array As Object(connTimeout))
Log($"Ponemos (LONG, Config): ConnectionTimeout, ${connTimeout}"$)
Log($"Ponemos ConnectionTimeout: ${connTimeout}"$)
Else
connTimeout = DEFAULT_CONN_TIMEOUT
poolJO.RunMethod("setConnectionTimeout", Array As Object(connTimeout))
Log($"Ponemos (LONG, Default): ConnectionTimeout, ${DEFAULT_CONN_TIMEOUT}"$)
Log($"Ponemos ConnectionTimeout: ${DEFAULT_CONN_TIMEOUT}"$)
End If
processedKeys.Add("connectiontimeout")
ConnTimeoutConfig = connTimeout ' <-- ALMACENAMIENTO CRÍTICO
' 3. LeakDetectionThreshold (Long) - Clave: leakdetectionthreshold
ConnTimeoutConfig = connTimeout ' Store the config value
' 3. LeakDetectionThreshold (Long) - Key: leakdetectionthreshold
Dim leakThr As Long
If properties.ContainsKey("leakdetectionthreshold") Then
rawValue = properties.Get("leakdetectionthreshold").As(String).Trim
leakThr = rawValue
poolJO.RunMethod("setLeakDetectionThreshold", Array As Object(leakThr))
Log($"Ponemos (LONG, Config): LeakDetectionThreshold, ${leakThr}"$)
Log($"Ponemos LeakDetectionThreshold: ${leakThr}"$)
Else
leakThr = DEFAULT_LEAK_THR
poolJO.RunMethod("setLeakDetectionThreshold", Array As Object(leakThr))
Log($"Ponemos (LONG, Default): LeakDetectionThreshold, ${DEFAULT_LEAK_THR}"$)
End If
processedKeys.Add("leakdetectionthreshold")
LeakDetectionThresholdConfig = leakThr ' <-- ALMACENAMIENTO CRÍTICO
LeakDetectionThresholdConfig = leakThr ' Store the config value
' ********** LÓGICA DE FIXED POOL: MaximumPoolSize primero **********
' 4. MaximumPoolSize (Int) - Clave: maximumpoolsize
' 4. MaximumPoolSize (Int) - Key: maximumpoolsize
If properties.ContainsKey("maximumpoolsize") Then
rawValue = properties.Get("maximumpoolsize").As(String).Trim
maxSize = rawValue.As(Int)
poolJO.RunMethod("setMaximumPoolSize", Array As Object(maxSize))
Log($"Ponemos (INT, Config): MaximumPoolSize, ${maxSize}"$)
Log($"Ponemos MaximumPoolSize: ${maxSize}"$)
Else
maxSize = DEFAULT_MAX_SIZE
poolJO.RunMethod("setMaximumPoolSize", Array As Object(DEFAULT_MAX_SIZE))
Log($"Ponemos (INT, Default): MaximumPoolSize, ${DEFAULT_MAX_SIZE}"$)
Log($"Ponemos MaximumPoolSize: ${DEFAULT_MAX_SIZE}"$)
End If
processedKeys.Add("maximumpoolsize")
PoolSizeConfig = maxSize ' <-- ALMACENAMIENTO CRÍTICO
' 5. MinimumIdle (Int) - Clave: minimumidle
PoolSizeConfig = maxSize ' Store the config value
' 5. MinimumIdle (Int) - Key: minimumidle
Dim minIdleFinal As Int
If properties.ContainsKey("minimumidle") Then
rawValue = properties.Get("minimumidle").As(String).Trim
minIdleFinal = rawValue.As(Int)
poolJO.RunMethod("setMinimumIdle", Array As Object(minIdleFinal))
Log($"Ponemos (INT, Config): MinimumIdle, ${minIdleFinal}"$)
Log($"Ponemos MinimumIdle: ${minIdleFinal}"$)
Else
' APLICAMOS EL FIXED POOL AXIOM: MinimumIdle = MaximumPoolSize (maxSize)
' APPLY FIXED POOL AXIOM: MinimumIdle = MaximumPoolSize (maxSize)
minIdleFinal = maxSize
poolJO.RunMethod("setMinimumIdle", Array As Object(minIdleFinal))
Log($"Ponemos (INT, Fixed Default): MinimumIdle, ${minIdleFinal} (Igual a MaximumPoolSize)"$)
Log($"Ponemos MinimumIdle: ${minIdleFinal} (Igual a MaximumPoolSize)"$)
End If
processedKeys.Add("minimumidle")
MinIdleConfig = minIdleFinal ' <-- ALMACENAMIENTO CRÍTICO
' ********** FIN DE LA LÓGICA DE FIXED POOL **********
' 6. RegisterMbeans (Boolean) - Clave: registermbeans
MinIdleConfig = minIdleFinal ' Store the config value
' 6. RegisterMbeans (Boolean) - Key: registermbeans
If properties.ContainsKey("registermbeans") Then
Dim regMbeans As Boolean = properties.Get("registermbeans")
poolJO.RunMethod("setRegisterMbeans", Array As Object(regMbeans))
Log($"Ponemos (BOOL, Config): RegisterMbeans, ${regMbeans}"$)
Log($"Ponemos RegisterMbeans: ${regMbeans}"$)
Else
poolJO.RunMethod("setRegisterMbeans", Array As Object(DEFAULT_REG_MBEANS))
Log($"Ponemos (BOOL, Default): RegisterMbeans, ${DEFAULT_REG_MBEANS}"$)
Log($"Ponemos RegisterMbeans: ${DEFAULT_REG_MBEANS}"$)
End If
processedKeys.Add("registermbeans")
' 7. KeepaliveTime (Long) - Clave: keepalivetime
' 7. KeepaliveTime (Long) - Key: keepalivetime
Dim keepAlive As Long
If properties.ContainsKey("keepalivetime") Then
rawValue = properties.Get("keepalivetime").As(String).Trim
keepAlive = rawValue
' El valor mínimo aceptado es 30,000 ms [4].
' The minimum accepted value is 30,000 ms [4].
If keepAlive < 30000 Then keepAlive = 30000
poolJO.RunMethod("setKeepaliveTime", Array As Object(keepAlive))
@@ -206,13 +198,12 @@ Public Sub SetProperties(properties As Map)
Else
keepAlive = DEFAULT_KEEP_ALIVE_TIME
poolJO.RunMethod("setKeepaliveTime", Array As Object(keepAlive))
Log($"Ponemos (LONG, Default): KeepaliveTime, ${DEFAULT_KEEP_ALIVE_TIME} (50 minutos)"$)
Log($"Ponemos KeepaliveTime: ${DEFAULT_KEEP_ALIVE_TIME} (50 minutes)"$)
End If
processedKeys.Add("keepalivetime")
KeepAliveTimeConfig = keepAlive ' <-- ALMACENAMIENTO CRÍTICO
' --- INICIO: PROCESAMIENTO DE PROPIEDADES RESTANTES ---
' ... (El resto del código de procesamiento de propiedades restantes se mantiene igual)
KeepAliveTimeConfig = keepAlive ' Store the config value
' Process remaining properties
Dim intValue As Int
Dim booleanValue As Boolean
Dim variableType As String
@@ -238,9 +229,9 @@ Public Sub SetProperties(properties As Map)
Else
Log($"Connection pool property ${k} has unsupported variable type of ${variableType}"$)
End If
Log($"Ponemos (Restante): ${k}, ${properties.Get(k)}"$)
Log($"Ponemos (Remaining): ${k}, ${properties.Get(k)}"$)
Catch
Log($"Warning (Restante): Method: ${dynamicMethodName} not matched or type mismatch for property ${k}. Error: ${LastException.Message}"$)
Log($"Warning (Remaining): Method: ${dynamicMethodName} not matched or type mismatch for property ${k}. Error: ${LastException.Message}"$)
End Try
Else
Log($"Warning: Property ${k} not supported"$)
@@ -249,23 +240,19 @@ Public Sub SetProperties(properties As Map)
Next
End Sub
' // ---------------------------------------------------------------------------------------------------------------------------------------------------
' // NUEVA SUBRUTINA: Aplica propiedades específicas del Driver JDBC (Ej: MySQL caching)
' // Estas propiedades se aplican usando addDataSourceProperty, que no se soporta en SetProperties estándar.
' // ---------------------------------------------------------------------------------------------------------------------------------------------------
' Applies specific JDBC Driver properties (e.g., MySQL caching)
' These properties are applied using addDataSourceProperty, which is not supported in standard SetProperties.
Public Sub SetDriverProperties(properties As Map)
' properties es el mapa que RDCConnector.LoadDriverProperties extrae (ej: driver.mysql.*).
' properties is the map extracted by RDCConnector.LoadDriverProperties (e.g., driver.mysql.*).
Dim value As Object
For Each k As String In properties.Keys
value = properties.Get(k)
' CRÍTICO: Usar addDataSourceProperty para configurar el Driver, no el Pool.
' Use addDataSourceProperty to configure the Driver, not the Pool.
poolJO.RunMethod("addDataSourceProperty", Array As Object(k, value))
Log($"HikariCP: Driver Property [${k}] added with value: ${value}"$)
Next
End Sub
'Check if JDBC URL is supported
Public Sub SupportUrl(jdbcUrl As String) As Boolean
If jdbcUrl.StartsWith("jdbc:sqlite") Then
@@ -300,48 +287,47 @@ Public Sub ClosePool
End If
End Sub
' ** ADVERTENCIA: Esta subrutina traduce los nombres de métodos internos de HikariCP
' ** a las claves genéricas (ej. BusyConnections) que RDCConnector espera.
' ** CRÍTICO: Ahora incluye la CONFIGURACIÓN ESTÁTICA guardada en las variables de clase.
' WARNING: This subroutine translates internal HikariCP method names
' to the generic keys (e.g., BusyConnections) that RDCConnector expects.
' It now includes the STATIC CONFIGURATION saved in the class variables.
Public Sub GetStats As Map
Dim stats As Map
stats.Initialize
' 1. AGREGAR PROPIEDADES ESTÁTICAS (CONFIGURACIÓN ALMACENADA)
' 1. ADD STATIC PROPERTIES (STORED CONFIGURATION)
stats.Put("MaxPoolSize", PoolSizeConfig)
stats.Put("MinPoolSize", MinIdleConfig)
stats.Put("MaxLifetime", MaxLifeConfig)
stats.Put("ConnectionTimeout", ConnTimeoutConfig)
stats.Put("LeakDetectionThreshold", LeakDetectionThresholdConfig)
stats.Put("KeepaliveTime", KeepAliveTimeConfig)
' Nota: Aquí puedes agregar las propiedades estáticas adicionales que sean relevantes.
' 2. OBTENER MÉTRICAS DE TIEMPO DE EJECUCIÓN (JMX/MBean)
' Note: You can add other relevant static properties here.
' 2. GET RUNTIME METRICS (JMX/MBean)
If poolJO.IsInitialized Then
Try
Dim poolMBean As JavaObject = poolJO.RunMethod("getHikariPoolMXBean", Null)
Dim busyConn As Object = poolMBean.RunMethod("getActiveConnections", Null)
stats.Put("BusyConnections", busyConn)
Dim idleConn As Object = poolMBean.RunMethod("getIdleConnections", Null)
stats.Put("IdleConnections", idleConn)
Dim activeCount As Int = busyConn
Dim idleCount As Int = idleConn
stats.Put("TotalConnections", activeCount + idleCount)
Dim awaitingConn As Object = poolMBean.RunMethod("getThreadsAwaitingConnection", Null)
stats.Put("ThreadsAwaitingConnection", awaitingConn)
Catch
Dim ErrorMsg As String = "HikariCP.GetStats: Error al obtener métricas dinámicas del MBean: " & LastException.Message
Dim ErrorMsg As String = "HikariCP.GetStats: Error getting dynamic metrics from MBean: " & LastException.Message
Log(ErrorMsg)
stats.Put("Error_Runtime", ErrorMsg)
End Try
Else
stats.Put("Error", "Pool JO no inicializado")
stats.Put("Error", "Pool JO not initialized")
End If
Return stats
End Sub
End Sub

View File

@@ -4,34 +4,30 @@ ModulesStructureVersion=1
Type=Class
Version=10.3
@EndOfDesignText@
' Módulo de clase: Manager
' Este handler proporciona un panel de administración web para el servidor jRDC2-Multi.
' Permite monitorear el estado del servidor, recargar configuraciones de bases de datos,
' ver estasticas de rendimiento, reiniciar servicios externos, y gestionar la autenticación de usuarios.
' Class module: Manager
' This handler provides a web administration panel for the jRDC2-Multi server.
' It allows monitoring server status, reloading database configurations,
' viewing performance statistics, restarting external services, and managing user authentication.
Sub Class_Globals
' Objeto para generar respuestas JSON. Se utiliza para mostrar mapas de datos de forma legible.
' Object to generate JSON responses. Used to display data maps legibly.
Dim j As JSONGenerator
' La clase BCrypt no se usa directamente en este módulo, pero se mantiene si hubiera planes futuros.
' The BCrypt class is not used directly in this module, but is kept for any future plans.
' Private bc As BCrypt
End Sub
' Subrutina de inicialización de la clase. Se llama cuando se crea un objeto de esta clase.
' Class initialization subroutine. Called when an object of this class is created.
Public Sub Initialize
' No se requiere inicialización específica para esta clase en este momento.
' No specific initialization is required for this class at this time.
End Sub
' Método principal que maneja las peticiones HTTP para el panel de administración.
' req: El objeto ServletRequest que contiene la información de la petición entrante.
' resp: El objeto ServletResponse para construir y enviar la respuesta al cliente.
' Módulo de clase: Manager
' ... (tu código de Class_Globals e Initialize se queda igual) ...
' Método principal que maneja las peticiones HTTP para el panel de administración.
' Refactorizado para funcionar como una API con un frontend estático.
' Main method that handles HTTP requests for the administration panel.
' req: The ServletRequest object containing incoming request information.
' resp: The ServletResponse object for building and sending the response to the client.
' Refactored to work as an API with a static frontend.
Sub Handle(req As ServletRequest, resp As ServletResponse)
' --- 1. Bloque de Seguridad ---
' Security Block
If req.GetSession.GetAttribute2("user_is_authorized", False) = False Then
resp.SendRedirect("/login")
Return
@@ -39,7 +35,7 @@ Sub Handle(req As ServletRequest, resp As ServletResponse)
Dim Command As String = req.GetParameter("command")
' --- 2. Servidor de la Página Principal ---
' Main Page Server
If Command = "" Then
Try
resp.ContentType = "text/html; charset=utf-8"
@@ -50,10 +46,10 @@ Sub Handle(req As ServletRequest, resp As ServletResponse)
Return
End If
' --- 3. Manejo de Comandos como API ---
' API Command Handling
Select Command.ToLowerCase
' --- Comandos que devuelven JSON (Métricas del Pool) ---
' Commands that return JSON (Pool Metrics)
Case "getstatsold"
resp.ContentType = "application/json; charset=utf-8"
Dim allPoolStats As Map
@@ -63,49 +59,107 @@ Sub Handle(req As ServletRequest, resp As ServletResponse)
If connector.IsInitialized Then
allPoolStats.Put(dbKey, connector.GetPoolStats)
Else
allPoolStats.Put(dbKey, CreateMap("Error": "Conector no inicializado"))
allPoolStats.Put(dbKey, CreateMap("Error": "Connector not initialized"))
End If
Next
j.Initialize(allPoolStats)
resp.Write(j.ToString)
Return
Case "getstats"
resp.ContentType = "application/json; charset=utf-8"
Dim allPoolStats As Map
' Leemos del caché global actualizado por el Timer SSE
' We read from the global cache updated by the SSE Timer
allPoolStats = Main.LatestPoolStats
For Each dbKey As String In Main.listaDeCP
If allPoolStats.ContainsKey(dbKey) = False Then
allPoolStats.Put(dbKey, CreateMap("Error": "Métricas no disponibles/Pool no inicializado"))
allPoolStats.Put(dbKey, CreateMap("Error": "Metrics unavailable/Pool not initialized"))
End If
Next
j.Initialize(allPoolStats)
resp.Write(j.ToString)
Return
Case "slowqueries"
resp.ContentType = "application/json; charset=utf-8"
Dim results As List
results.Initialize
Try
' Verifica la existencia de la tabla de logs antes de consultar
Dim tableExists As Boolean = Main.SQL1.ExecQuerySingleResult($"SELECT name FROM sqlite_master WHERE type='table' AND name='query_logs';"$) <> Null
' --- 1. VALIDACIÓN Y PARSEO DEFENSIVO DE PARÁMETROS ---
' Capturamos los parámetros numéricos como Strings primero para chequear si están vacíos.
Dim limitStr As String = req.GetParameter("limit")
Dim minutesStr As String = req.GetParameter("minutes")
Dim limit As Int = 0
Dim minutes As Int = 0
' 1a. Parseo seguro de 'limit'. Evita NumberFormatException si el string es "" o Null.
If limitStr <> Null And limitStr.Trim.Length > 0 Then
limit = limitStr.As(Int)
End If
' 1b. Parseo seguro de 'minutes'.
If minutesStr <> Null And minutesStr.Trim.Length > 0 Then
minutes = minutesStr.As(Int)
End If
' Parámetros de ordenamiento (ya son strings, no necesitan chequeo de NFE)
Dim sortby As String = req.GetParameter("sortby").ToLowerCase.Trim ' Columna para ordenar (Default: duration_ms)
Dim sortorder As String = req.GetParameter("sortorder").ToUpperCase.Trim ' Orden (Default: DESC)
' Establecer Defaults (ahora esta lógica funciona porque limit y minutes son 0 si el parámetro estaba vacío o ausente)
If limit = 0 Then limit = 20 ' Límite de registros (Default: 20)
If minutes = 0 Then minutes = 60 ' Filtro de tiempo en minutos (Default: 60 - última hora)
' --- Lista Blanca (Whitelist) para prevención de Inyección SQL en ORDER BY ---
Dim allowedSortColumns As List
allowedSortColumns.Initialize
allowedSortColumns.AddAll(Array As Object("duration_ms", "timestamp", "db_key", "client_ip", "busy_connections", "handler_active_requests", "query_name"))
If allowedSortColumns.IndexOf(sortby) = -1 Then
sortby = "duration_ms" ' Usar default seguro si no se encuentra
End If
If sortorder <> "ASC" And sortorder <> "DESC" Then
sortorder = "DESC"
End If
' --- 2. PREPARACIÓN DE LA CONSULTA SQL ---
' We use SQL_Logs instance to check for the table existence.
Dim tableExists As Boolean = Main.SQL_Logs.ExecQuerySingleResult($"SELECT name FROM sqlite_master WHERE type='table' AND name='query_logs'"$) <> Null
If tableExists = False Then
j.Initialize(CreateMap("message": "La tabla de logs ('query_logs') no existe. Habilita 'enableSQLiteLogs=1' en la configuración."))
j.Initialize(CreateMap("message": "La tabla de logs ('query_logs') no existe. Habilite 'enableSQLiteLogs=1' en la configuración."))
resp.Write(j.ToString)
Return
End If
' Consulta las 20 queries más lentas de la última hora
Dim oneHourAgoMs As Long = DateTime.Now - 3600000
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"$)
' Calcular el tiempo de corte (flexible)
Dim cutOffTimeMs As Long = DateTime.Now - (minutes * 60000)
Dim sqlQuery As String = $"
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 >= ${cutOffTimeMs}
ORDER BY ${sortby} ${sortorder}
LIMIT ${limit}"$
Dim rs As ResultSet = Main.SQL_Logs.ExecQuery(sqlQuery) ' --- Execute query on SQL_Logs instance
Do While rs.NextRow
Dim row As Map
row.Initialize
@@ -118,33 +172,39 @@ Sub Handle(req As ServletRequest, resp As ServletResponse)
row.Put("Peticiones_Activas", rs.GetInt("handler_active_requests"))
results.Add(row)
Loop
rs.Close
Dim root As Map
root.Initialize
root.Put("data", results)
' Añadir meta información para diagnóstico
Dim meta As Map = CreateMap("limit_applied": limit, "sorted_by": sortby, "sort_order": sortorder, "minutes_filter": minutes)
root.Put("meta", meta)
j.Initialize(root)
resp.Write(j.ToString)
Catch
Log("Error CRÍTICO al obtener queries lentas en Manager API: " & LastException.Message)
Log("CRITICAL Error getting slow queries in Manager API: " & LastException.Message)
resp.Status = 500
Dim root As Map
root.Initialize
root.Put("data", results)
j.Initialize(root)
j.Initialize(CreateMap("message": "Error interno al procesar logs. Detalle: " & LastException.Message))
resp.Write(j.ToString)
End Try
Return
Case "logs", "totalrequests", "totalblocked"
resp.ContentType = "application/json; charset=utf-8"
Dim mp As Map
If Command = "logs" And GlobalParameters.mpLogs.IsInitialized Then mp = GlobalParameters.mpLogs
If Command = "totalrequests" And GlobalParameters.mpTotalRequests.IsInitialized Then mp = GlobalParameters.mpTotalRequests
If Command = "totalblocked" And GlobalParameters.mpBlockConnection.IsInitialized Then mp = GlobalParameters.mpBlockConnection
If mp.IsInitialized Then
j.Initialize(mp)
resp.Write(j.ToString)
@@ -152,108 +212,107 @@ Sub Handle(req As ServletRequest, resp As ServletResponse)
resp.Write("{}")
End If
Return
' --- Comandos que devuelven TEXTO PLANO ---
' Commands that return PLAIN TEXT
Case "ping"
resp.ContentType = "text/plain"
resp.Write($"Pong ($DateTime{DateTime.Now})"$)
Return
Case "reload"
resp.ContentType = "text/plain; charset=utf-8"
Dim sbTemp As StringBuilder
sbTemp.Initialize
' ***** LÓGICA DE RECARGA GRANULAR/SELECTIVA *****
Dim dbKeyToReload As String = req.GetParameter("db").ToUpperCase ' Leer parámetro 'db' opcional (ej: /manager?command=reload&db=DB3)
Dim targets As List ' Lista de DBKeys a recargar.
Dim dbKeyToReload As String = req.GetParameter("db").ToUpperCase ' Read optional 'db' parameter (e.g., /manager?command=reload&db=DB3)
Dim targets As List ' List of DBKeys to reload.
targets.Initialize
' 1. Determinar el alcance de la recarga (selectiva o total)
' 1. Determine the scope of the reload (selective or total)
If dbKeyToReload.Length > 0 Then
' Recarga selectiva
' Selective reload
If Main.listaDeCP.IndexOf(dbKeyToReload) = -1 Then
resp.Write($"ERROR: DBKey '${dbKeyToReload}' no es válida o no está configurada."$)
resp.Write($"ERROR: DBKey '${dbKeyToReload}' is not valid or not configured."$)
Return
End If
targets.Add(dbKeyToReload)
sbTemp.Append($"Iniciando recarga selectiva de ${dbKeyToReload} (Hot-Swap)..."$).Append(" " & CRLF)
sbTemp.Append($"Starting selective reload of ${dbKeyToReload} (Hot-Swap)..."$).Append(" " & CRLF)
Else
' Recarga completa (comportamiento por defecto)
' Full reload (default behavior)
targets.AddAll(Main.listaDeCP)
sbTemp.Append($"Iniciando recarga COMPLETA de configuración (Hot-Swap) ($DateTime{DateTime.Now})"$).Append(" " & CRLF)
sbTemp.Append($"Starting COMPLETE configuration reload (Hot-Swap) ($DateTime{DateTime.Now})"$).Append(" " & CRLF)
End If
' 2. Deshabilitar el Timer de logs (si es necesario)
' 2. Disable the log Timer (if necessary)
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)
sbTemp.Append(" -> Log cleanup timer (SQLite) stopped temporarily.").Append(" " & CRLF)
End If
Dim reloadSuccessful As Boolean = True
Dim oldConnectorsToClose As Map ' Guardaremos los conectores antiguos aquí.
Dim oldConnectorsToClose As Map ' We will store the old connectors here.
oldConnectorsToClose.Initialize
' 3. Procesar solo los conectores objetivos
' 3. Process only the target connectors
For Each dbKey As String In targets
sbTemp.Append($" -> Procesando recarga de ${dbKey}..."$).Append(CRLF)
sbTemp.Append($" -> Processing reload of ${dbKey}..."$).Append(CRLF)
Dim newRDC As RDCConnector
Try
' Crear el nuevo conector con la configuración fresca
' Create the new connector with the fresh configuration
newRDC.Initialize(dbKey)
' Adquirimos el lock para el reemplazo atómico
' Acquire the lock for atomic replacement
Main.MainConnectorsLock.RunMethod("lock", Null)
' Guardamos el conector antiguo (si existe)
' Save the old connector (if it exists)
Dim oldRDC As RDCConnector = Main.Connectors.Get(dbKey)
' Reemplazo atómico en el mapa global compartido
' Atomic replacement in the shared global map
Main.Connectors.Put(dbKey, newRDC)
' Liberamos el bloqueo inmediatamente
' Release the lock immediately
Main.MainConnectorsLock.RunMethod("unlock", Null)
' Si había un conector antiguo, lo guardamos para cerrarlo después
' If there was an old connector, save it to close later
If oldRDC.IsInitialized Then
oldConnectorsToClose.Put(dbKey, oldRDC)
End If
' 4. Actualizar el estado de logs (Granular)
' 4. Update log status (Granular)
Dim enableLogsSetting As Int = newRDC.config.GetDefault("enableSQLiteLogs", 0)
Dim isEnabled As Boolean = (enableLogsSetting = 1)
Main.SQLiteLoggingStatusByDB.Put(dbKey, isEnabled)
sbTemp.Append($" -> ${dbKey} recargado. Logs (config): ${isEnabled}"$).Append(CRLF)
sbTemp.Append($" -> ${dbKey} reloaded. Logs (config): ${isEnabled}"$).Append(CRLF)
Catch
' Si falla la inicialización del pool, no actualizamos Main.Connectors
' ¡CRÍTICO! Aseguramos que el lock se libere si hubo excepción antes de liberar.
' If pool initialization fails, we don't update Main.Connectors
' Ensure the lock is released if an exception occurred before release.
If Main.MainConnectorsLock.RunMethod("isHeldByCurrentThread", Null).As(Boolean) Then
Main.MainConnectorsLock.RunMethod("unlock", Null)
End If
sbTemp.Append($" -> ERROR CRÍTICO al inicializar conector para ${dbKey}: ${LastException.Message}"$).Append(" " & CRLF)
sbTemp.Append($" -> CRITICAL ERROR initializing connector for ${dbKey}: ${LastException.Message}"$).Append(" " & CRLF)
reloadSuccessful = False
Exit
End Try
Next
' 5. Cerrar los pools antiguos liberados (FUERA del Lock)
' 5. Close the old released pools (OUTSIDE the Lock)
If reloadSuccessful Then
For Each dbKey As String In oldConnectorsToClose.Keys
Dim oldRDC As RDCConnector = oldConnectorsToClose.Get(dbKey)
oldRDC.Close ' Cierre limpio del pool C3P0
sbTemp.Append($" -> Pool antiguo de ${dbKey} cerrado limpiamente."$).Append(" " & CRLF)
oldRDC.Close ' Clean closure of the C3P0 pool
sbTemp.Append($" -> Old pool for ${dbKey} closed cleanly."$).Append(" " & CRLF)
Next
' 6. Re-evaluar el estado global de Logs (CRÍTICO: debe revisar TODAS las DBs)
' 6. Re-evaluate the global Log status (must check ALL DBs)
Main.IsAnySQLiteLoggingEnabled = False
For Each dbKey As String In Main.listaDeCP
' Revisamos el estado de log de CADA conector activo
' We check the log status of EACH active connector
If Main.SQLiteLoggingStatusByDB.GetDefault(dbKey, False) Then
Main.IsAnySQLiteLoggingEnabled = True
Exit
@@ -261,19 +320,19 @@ Sub Handle(req As ServletRequest, resp As ServletResponse)
Next
If Main.IsAnySQLiteLoggingEnabled Then
Main.timerLogs.Enabled = True
sbTemp.Append($" -> Timer de limpieza de logs ACTIVADO (estado global: HABILITADO)."$).Append(" " & CRLF)
sbTemp.Append($" -> Log cleanup timer ACTIVATED (global status: ENABLED)."$).Append(" " & CRLF)
Else
Main.timerLogs.Enabled = False
sbTemp.Append($" -> Timer de limpieza de logs DESHABILITADO (estado global: DESHABILITADO)."$).Append(" " & CRLF)
sbTemp.Append($" -> Log cleanup timer DISABLED (global status: DISABLED)."$).Append(" " & CRLF)
End If
sbTemp.Append($"¡Recarga de configuración completada con éxito!"$).Append(" " & CRLF)
sbTemp.Append($"Configuration reload completed successfully!"$).Append(" " & CRLF)
Else
' Si falló, restauramos el estado del timer anterior.
' If it failed, restore the previous timer state.
If oldTimerState Then
Main.timerLogs.Enabled = True
sbTemp.Append(" -> Restaurando Timer de limpieza de logs al estado ACTIVO debido a fallo en recarga.").Append(" " & CRLF)
sbTemp.Append(" -> Restoring Log cleanup timer to ACTIVE state due to reload failure.").Append(" " & CRLF)
End If
sbTemp.Append($"¡ERROR: La recarga de configuración falló! Los conectores antiguos siguen activos."$).Append(" " & CRLF)
sbTemp.Append($"ERROR: Configuration reload failed! Old connectors are still active."$).Append(" " & CRLF)
End If
resp.Write(sbTemp.ToString)
Return
@@ -281,57 +340,57 @@ Sub Handle(req As ServletRequest, resp As ServletResponse)
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.)
sb.Append("--- STARTING CONNECTIVITY TEST TO ALL CONFIGURED POOLS ---").Append(CRLF).Append(CRLF)
' We iterate over the list of DB Keys loaded at startup (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
Dim con As SQL ' Connection for the test
Try
' 1. Obtener el RDCConnector para esta DBKey
' 1. Get the RDCConnector for this DBKey
Dim connector As RDCConnector = Main.Connectors.Get(dbKey)
If connector.IsInitialized = False Then
errorMsg = "Conector no inicializado (revisa logs de AppStart)"
errorMsg = "Connector not initialized (check AppStart logs)"
Else
' 2. Forzar la adquisición de una conexión del pool C3P0
' 2. Force acquisition of a connection from the C3P0 pool
con = connector.GetConnection(dbKey)
If con.IsInitialized Then
' 3. Si la conexión es válida, la cerramos inmediatamente para devolverla al pool
' 3. If the connection is valid, close it immediately to return it to the pool
con.Close
success = True
Else
errorMsg = "La conexión devuelta no es válida (SQL.IsInitialized = False)"
errorMsg = "Returned connection is not valid (SQL.IsInitialized = False)"
End If
End If
Catch
' Capturamos cualquier excepción (ej. fallo de JDBC, timeout de C3P0)
' We catch any exception (e.g., JDBC failure, C3P0 timeout)
errorMsg = LastException.Message
End Try
If success Then
sb.Append($"* ${dbKey}: Conexión adquirida y liberada correctamente."$).Append(CRLF)
sb.Append($"* ${dbKey}: Connection acquired and released successfully."$).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)
' If it fails, log the error for the administrator.
Main.LogServerError("ERROR", "Manager.TestCommand", $"Connectivity test failed for ${dbKey}: ${errorMsg}"$, dbKey, "test_command", req.RemoteAddress)
sb.Append($"[FAILED] ${dbKey}: CRITICAL ERROR getting connection. Message: ${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)
sb.Append(CRLF).Append("--- END OF CONNECTION TEST ---").Append(CRLF)
' We keep the original list of loaded config files (this is informational)
sb.Append(CRLF).Append("Loaded configuration files:").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)
sb.Append($" -> Using ${configName}.properties"$).Append(CRLF)
Next
resp.Write(sb.ToString)
Return
Case "rsx", "rpm2", "revivebow", "restartserver"
@@ -341,161 +400,154 @@ Sub Handle(req As ServletRequest, resp As ServletResponse)
Case "rsx": batFile = "start.bat"
Case "rpm2": batFile = "reiniciaProcesoPM2.bat"
Case "reviveBow": batFile = "reiniciaProcesoBow.bat"
Case "restartserver": batFile = "restarServer.bat" ' Nota: este bat no estaba definido, se usó el nombre del comando
Case "restartserver": batFile = "restarServer.bat" ' Note: this bat was not defined, command name was used
End Select
Log($"Ejecutando ${File.DirApp}\${batFile}"$)
Log($"Executing ${File.DirApp}\${batFile}"$)
Try
Dim shl As Shell
shl.Initialize("shl","cmd", Array("/c", File.DirApp & "\" & batFile & " " & Main.srvr.Port))
shl.WorkingDirectory = File.DirApp
shl.Run(-1)
resp.Write($"Comando '${Command}' ejecutado. Script invocado: ${batFile}"$)
resp.Write($"Command '${Command}' executed. Script invoked: ${batFile}"$)
Catch
resp.Write($"Error al ejecutar el script para '${Command}': ${LastException.Message}"$)
resp.Write($"Error executing script for '${Command}': ${LastException.Message}"$)
End Try
Return
Case "paused", "continue"
resp.ContentType = "text/plain; charset=utf-8"
resp.ContentType = "text/plain; charset=utf-ab"
If Command = "paused" Then
GlobalParameters.IsPaused = 1
resp.Write("Servidor pausado.")
resp.Write("Server paused.")
Else
GlobalParameters.IsPaused = 0
resp.Write("Servidor reanudado.")
resp.Write("Server resumed.")
End If
Return
Case "block", "unblock"
resp.ContentType = "text/plain; charset=utf-8"
Dim ip As String = req.GetParameter("IP")
If ip = "" Then
resp.Write("Error: El parámetro IP es requerido.")
resp.Write("Error: The IP parameter is required.")
Return
End If
If GlobalParameters.mpBlockConnection.IsInitialized Then
If Command = "block" Then
GlobalParameters.mpBlockConnection.Put(ip, ip)
resp.Write($"IP bloqueada: ${ip}"$)
resp.Write($"IP blocked: ${ip}"$)
Else
GlobalParameters.mpBlockConnection.Remove(ip)
resp.Write($"IP desbloqueada: ${ip}"$)
resp.Write($"IP unblocked: ${ip}"$)
End If
Else
resp.Write("Error: El mapa de bloqueo no está inicializado.")
resp.Write("Error: The block map is not initialized.")
End If
Return
Case "getconfiginfo"
resp.ContentType = "text/plain; charset=utf-8"
Dim sbInfo As StringBuilder
sbInfo.Initialize
Dim allKeys As List
allKeys.Initialize
allKeys.AddAll(Main.listaDeCP)
sbInfo.Append("======================================================================").Append(CRLF)
sbInfo.Append($"=== CONFIGURACIÓN jRDC2-Multi V$1.2{Main.VERSION} (ACTIVA) ($DateTime{DateTime.Now}) ==="$).Append(CRLF)
sbInfo.Append($"=== jRDC2-Multi V$1.2{Main.VERSION} CONFIGURATION (ACTIVE) ($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 (HIKARICP) ###").Append(CRLF)
sbInfo.Append("### GLOSSARY OF ALLOWED PARAMETERS IN CONFIG.PROPERTIES (HIKARICP) ###").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)
' --- Parámetros de HIKARICP (Foco en el mínimo set de tuning) ---
sbInfo.Append("pool.hikari.maximumPoolSize: Máximo de conexiones simultáneas permitido. (Recomendado N*Cores DB),").Append(CRLF)
sbInfo.Append("pool.hikari.minimumIdle: Mínimo de conexiones inactivas. Recomendado igual a maximumPoolSize para pool de tamaño fijo,").Append(CRLF)
sbInfo.Append("pool.hikari.maxLifetime (ms): Tiempo máximo de vida de una conexión. CRÍTICO: Debe ser menor que el timeout del firewall/DB,").Append(CRLF)
sbInfo.Append("pool.hikari.connectionTimeout (ms): Tiempo máximo de espera del cliente por una conexión disponible (Default: 30000 ms),").Append(CRLF)
sbInfo.Append("pool.hikari.idleTimeout (ms): Tiempo inactivo antes de retirar la conexión (ms). Solo aplica si minimumIdle < maximumPoolSize,").Append(CRLF)
sbInfo.Append("pool.hikari.leakDetectionThreshold (ms): Umbral (ms) para detectar conexiones no devueltas (fugas).").Append(CRLF)
sbInfo.Append("DriverClass: JDBC driver class (e.g., oracle.jdbc.driver.OracleDriver).").Append(CRLF)
sbInfo.Append("JdbcUrl: Database connection URL (IP, port, service).").Append(CRLF)
sbInfo.Append("User/Password: DB access credentials.").Append(CRLF)
sbInfo.Append("ServerPort: B4J server listening port (only taken from config.properties).").Append(CRLF)
sbInfo.Append("Debug: If 'true', SQL commands are reloaded on each request (DISABLED, USE RELOAD COMMAND).").Append(CRLF)
sbInfo.Append("parameterTolerance: Defines whether to trim (1) or reject (0) SQL parameters exceeding those required by the query.").Append(CRLF)
sbInfo.Append("enableSQLiteLogs: Granular control. Enables (1) or disables (0) writing logs to users.db for this DB.").Append(CRLF)
sbInfo.Append("pool.hikari.maximumPoolSize: Maximum simultaneous connections allowed. (Recommended N*Cores DB),").Append(CRLF)
sbInfo.Append("pool.hikari.minimumIdle: Minimum idle connections. Recommended equal to maximumPoolSize for fixed-size pool,").Append(CRLF)
sbInfo.Append("pool.hikari.maxLifetime (ms): Maximum lifetime of a connection. CRITICAL: Must be less than firewall/DB timeout,").Append(CRLF)
sbInfo.Append("pool.hikari.connectionTimeout (ms): Maximum time client waits for an available connection (Default: 30000 ms),").Append(CRLF)
sbInfo.Append("pool.hikari.idleTimeout (ms): Idle time before retiring the connection (ms). Only applies if minimumIdle < maximumPoolSize,").Append(CRLF)
sbInfo.Append("pool.hikari.leakDetectionThreshold (ms): Threshold (ms) to detect unreturned connections (leaks).").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)
If connector.IsInitialized Then
Dim configMap As Map = connector.config
' Obtener las métricas y la configuración REAL aplicada por HikariCP
' Get metrics and REAL configuration applied by HikariCP
Dim poolStats As Map = connector.GetPoolStats
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 (HIKARICP - Valores Aplicados) ---").Append(CRLF)
sbInfo.Append($"MaximumPoolSize (Aplicado): ${poolStats.GetDefault("MaxPoolSize", 10).As(Int)}"$).Append(CRLF)
sbInfo.Append($"MinimumIdle (Aplicado): ${poolStats.GetDefault("MinPoolSize", 10).As(Int)}"$).Append(CRLF)
' Reportamos los timeouts en Milisegundos (ms)
sbInfo.Append("--- POOL CONFIGURATION (HIKARICP - Applied Values) ---").Append(CRLF)
sbInfo.Append($"MaximumPoolSize (Applied): ${poolStats.GetDefault("MaxPoolSize", 10).As(Int)}"$).Append(CRLF)
sbInfo.Append($"MinimumIdle (Applied): ${poolStats.GetDefault("MinPoolSize", 10).As(Int)}"$).Append(CRLF)
' Report timeouts in Milliseconds (ms)
sbInfo.Append($"MaxLifetime (ms): ${poolStats.GetDefault("MaxLifetime", 1800000).As(Long)}"$).Append(CRLF)
sbInfo.Append($"ConnectionTimeout (ms): ${poolStats.GetDefault("ConnectionTimeout", 30000).As(Long)}"$).Append(CRLF)
sbInfo.Append($"IdleTimeout (ms): ${poolStats.GetDefault("IdleTimeout", 600000).As(Long)}"$).Append(CRLF)
sbInfo.Append($"LeakDetectionThreshold (ms): ${poolStats.GetDefault("LeakDetectionThreshold", 0).As(Long)}"$).Append(CRLF).Append(CRLF)
' *** NUEVA SECCIÓN: PROPIEDADES ESPECÍFICAS DEL DRIVER ***
If connector.driverProperties.IsInitialized And connector.driverProperties.Size > 0 Then
sbInfo.Append("--- PROPIEDADES DE RENDIMIENTO DEL DRIVER JDBC (Optimización de Sentencias) ---").Append(CRLF)
sbInfo.Append("--- JDBC DRIVER PERFORMANCE PROPERTIES (Statement Optimization) ---").Append(CRLF)
For Each propKey As String In connector.driverProperties.Keys
Dim propValue As Object = connector.driverProperties.Get(propKey)
sbInfo.Append($"[Driver] ${propKey}: ${propValue}"$).Append(CRLF)
Next
sbInfo.Append(CRLF)
End If
' *** FIN DE LA NUEVA SECCIÓN ***
' Reportamos métricas de runtime del pool (si están disponibles).
sbInfo.Append("--- ESTADO DE RUNTIME (Métricas Dinámicas) ---").Append(CRLF)
sbInfo.Append("--- RUNTIME STATUS (Dynamic Metrics) ---").Append(CRLF)
sbInfo.Append($"Total Connections: ${poolStats.GetDefault("TotalConnections", "N/A")}"$).Append(CRLF)
sbInfo.Append($"Busy Connections: ${poolStats.GetDefault("BusyConnections", "N/A")}"$).Append(CRLF)
sbInfo.Append($"Idle Connections: ${poolStats.GetDefault("IdleConnections", "N/A")}"$).Append(CRLF)
sbInfo.Append($"Handler Active Requests: ${poolStats.GetDefault("HandlerActiveRequests", "N/A")}"$).Append(CRLF).Append(CRLF)
sbInfo.Append("--- COMPORTAMIENTO ---").Append(CRLF)
sbInfo.Append($"Debug (Recarga Queries - DESHABILITADO): ${configMap.GetDefault("Debug", "false")}"$).Append(CRLF)
sbInfo.Append("--- BEHAVIOR ---").Append(CRLF)
sbInfo.Append($"Debug (Reload Queries - DISABLED): ${configMap.GetDefault("Debug", "false")}"$).Append(CRLF)
Dim tolerance As Int = configMap.GetDefault("parameterTolerance", 0).As(Int)
sbInfo.Append($"ParameterTolerance: ${tolerance} (0=Estricto, 1=Habilitado)"$).Append(CRLF)
sbInfo.Append($"ParameterTolerance: ${tolerance} (0=Strict, 1=Enabled)"$).Append(CRLF)
Dim isLogsEnabledRuntime As Boolean = Main.SQLiteLoggingStatusByDB.GetDefault(dbKey, False).As(Boolean)
Dim logsEnabledRuntimeInt As Int = 0
If isLogsEnabledRuntime Then
logsEnabledRuntimeInt = 1
End If
sbInfo.Append($"EnableSQLiteLogs: ${logsEnabledRuntimeInt} (0=Deshabilitado, 1=Habilitado)"$).Append(CRLF)
sbInfo.Append($"EnableSQLiteLogs: ${logsEnabledRuntimeInt} (0=Disabled, 1=Enabled)"$).Append(CRLF)
sbInfo.Append(CRLF)
Else
sbInfo.Append($"ERROR: Conector ${dbKey} no inicializado o falló al inicio."$).Append(CRLF).Append(CRLF)
sbInfo.Append($"ERROR: Connector ${dbKey} not initialized or failed at startup."$).Append(CRLF).Append(CRLF)
End If
Next
resp.Write(sbInfo.ToString)
Return
@@ -505,24 +557,22 @@ Sub Handle(req As ServletRequest, resp As ServletResponse)
Dim dbKeyToChange As String = req.GetParameter("db").ToUpperCase
Dim status As String = req.GetParameter("status")
If Main.listaDeCP.IndexOf(dbKeyToChange) = -1 Then
resp.Write($"ERROR: DBKey '${dbKeyToChange}' no es válida."$)
resp.Write($"ERROR: DBKey '${dbKeyToChange}' is not valid."$)
Return
End If
Dim isEnabled As Boolean = (status = "1")
Dim resultMsg As String
' *** 1. Adquisición del Lock (CRÍTICO) ***
Main.MainConnectorsLock.RunMethod("lock", Null)
Try
' 2. Lógica Crítica de Modificación de Estado (Protegida)
Main.SQLiteLoggingStatusByDB.Put(dbKeyToChange, isEnabled)
Private hab As String = "DESHABILITADOS"
If isEnabled Then hab = "HABILITADOS"
resultMsg = $"Logs de ${dbKeyToChange} ${hab} en caliente."$
' 3. Re-evaluación del estado global
Private hab As String = "DISABLED"
If isEnabled Then hab = "ENABLED"
resultMsg = $"Logs for ${dbKeyToChange} ${hab} on-the-fly."$
Main.IsAnySQLiteLoggingEnabled = False
For Each dbKey As String In Main.listaDeCP
If Main.SQLiteLoggingStatusByDB.GetDefault(dbKey, False) Then
@@ -531,27 +581,21 @@ Sub Handle(req As ServletRequest, resp As ServletResponse)
End If
Next
' 4. Ajustar el Timer
If Main.IsAnySQLiteLoggingEnabled Then
If Main.timerLogs.Enabled = False Then Main.timerLogs.Enabled = True
resultMsg = resultMsg & " Timer de limpieza ACTIVADO."
resultMsg = resultMsg & " Cleanup timer ACTIVATED."
Else
Main.timerLogs.Enabled = False
resultMsg = resultMsg & " Timer de limpieza DESHABILITADO globalmente."
resultMsg = resultMsg & " Cleanup timer DISABLED globally."
End If
' ** LIBERACIÓN EN CASO DE ÉXITO **
' En el camino de éxito, liberamos inmediatamente antes de que la subrutina termine.
If Main.MainConnectorsLock.RunMethod("isHeldByCurrentThread", Null).As(Boolean) Then
Main.MainConnectorsLock.RunMethod("unlock", Null)
End If
Catch
' 5. Manejo de Excepción y ** LIBERACIÓN EN CASO DE FALLO **
resultMsg = $"ERROR CRÍTICO al modificar el estado de logs: ${LastException.Message}"$
' ¡ESTE ES EL EQUIVALENTE AL FINALLY EN B4X!
' Verificamos si este hilo retiene el lock y, si es así, lo liberamos de inmediato.
resultMsg = $"CRITICAL ERROR modifying log status: ${LastException.Message}"$
If Main.MainConnectorsLock.RunMethod("isHeldByCurrentThread", Null).As(Boolean) Then
Main.MainConnectorsLock.RunMethod("unlock", Null)
End If
@@ -559,10 +603,10 @@ Sub Handle(req As ServletRequest, resp As ServletResponse)
resp.Write(resultMsg)
Return
Case Else
resp.ContentType = "text/plain; charset=utf-8"
resp.SendError(404, $"Comando desconocido: '{Command}'"$)
resp.SendError(404, $"Unknown command: '{Command}'"$)
Return
End Select
End Sub
End Sub

View File

@@ -4,43 +4,43 @@ ModulesStructureVersion=1
Type=StaticCode
Version=10.3
@EndOfDesignText@
' Archivo: ParameterValidationUtils.bas
' Módulo de utilidad: ParameterValidationUtils
' Centraliza la lógica de validación y ajuste de parámetros SQL.
' Ahora soporta recorte de parámetros excesivos.
' File: ParameterValidationUtils.bas
' Utility module: ParameterValidationUtils
' Centralizes SQL parameter validation and adjustment logic.
' Now supports trimming of excessive parameters.
Sub Process_Globals
' El Type ParameterValidationResult está declarado en Main.bas, no se declara aquí.
' The ParameterValidationResult Type is declared in Main.bas, not here.
End Sub
' Valida y ajusta la lista de parámetros para la ejecución SQL, aplicando la lógica de tolerancia.
' Retorna un ParameterValidationResult indicando éxito/error y los parámetros a usar.
' Validates and adjusts the parameter list for SQL execution, applying tolerance logic.
' Returns a ParameterValidationResult indicating success/error and the parameters to use.
Public Sub ValidateAndAdjustParameters (CommandName As String, DBKey As String, sqlCommand As String, receivedParams As List, IsToleranceEnabled As Boolean) As ParameterValidationResult
Dim res As ParameterValidationResult
res.Initialize
res.Success = True ' Asumimos éxito inicialmente
res.Success = True ' Assume success initially
' Log(">>>> IsToleranceEnabled: " & IsToleranceEnabled)
' Aseguramos que receivedParams esté inicializada, incluso si está vacía o Null
' Ensure receivedParams is initialized, even if it's empty or Null
If receivedParams = Null Or receivedParams.IsInitialized = False Then
receivedParams.Initialize ' Inicializa una lista vacía si es Null o no inicializada.
receivedParams.Initialize ' Initialize an empty list if Null or uninitialized.
End If
' Contar cuántos '?' hay en la sentencia SQL para saber cuántos parámetros se esperan.
' Count how many '?' are in the SQL statement to know how many parameters are expected.
Dim expectedParams As Int = sqlCommand.Length - sqlCommand.Replace("?", "").Length
Dim receivedParamsSize As Int = receivedParams.Size
If receivedParamsSize < expectedParams Then
' Caso 1: Se recibieron MENOS parámetros de los esperados. Esto es un error.
' Fewer parameters were received than expected. This is an error.
res.Success = False
res.ErrorMessage = $"ERROR: Número de parámetros insuficiente para "${CommandName}" (DB: ${DBKey}). Se esperaban ${expectedParams} y se recibieron ${receivedParamsSize}."$
res.ErrorMessage = $"ERROR: Insufficient number of parameters for "${CommandName}" (DB: ${DBKey}). Expected ${expectedParams} and received ${receivedParamsSize}."$
Log(res.ErrorMessage)
Main.LogServerError("ERROR", "ParameterValidationUtils.ValidateAndAdjustParameters", res.ErrorMessage, DBKey, CommandName, Null) ' <-- Nuevo Log
Main.LogServerError("ERROR", "ParameterValidationUtils.ValidateAndAdjustParameters", res.ErrorMessage, DBKey, CommandName, Null)
Return res
Else If receivedParamsSize > expectedParams Then
' Caso 2: Se recibieron MÁS parámetros de los esperados.
If IsToleranceEnabled Then ' Solo recortamos si la tolerancia está habilitada
' More parameters were received than expected.
If IsToleranceEnabled Then ' We only trim if tolerance is enabled
Dim adjustedParams As List
adjustedParams.Initialize
For i = 0 To expectedParams - 1
@@ -48,23 +48,23 @@ Public Sub ValidateAndAdjustParameters (CommandName As String, DBKey As String,
Next
res.ParamsToExecute = adjustedParams
res.Success = True
Dim WarningMsg As String = $"ADVERTENCIA: Se recibieron más parámetros de los esperados para "${CommandName}" (DB: ${DBKey}). Se esperaban ${expectedParams} y se recibieron ${receivedParamsSize}. Se ajustó la lista de parámetros a ${expectedParams} elementos."$
Dim WarningMsg As String = $"WARNING: More parameters received than expected for "${CommandName}" (DB: ${DBKey}). Expected ${expectedParams} and received ${receivedParamsSize}. Adjusted parameter list to ${expectedParams} items."$
' Log(WarningMsg)
' Log("Cache: " & Main.LOG_CACHE_THRESHOLD & "|" & Main.ErrorLogCache.Size)
Main.LogServerError("ADVERTENCIA", "ParameterValidationUtils.ValidateAndAdjustParameters", WarningMsg, DBKey, CommandName, Null)
Main.LogServerError("WARNING", "ParameterValidationUtils.ValidateAndAdjustParameters", WarningMsg, DBKey, CommandName, Null)
Else
' Si la tolerancia NO está habilitada, esto es un error crítico.
' If tolerance is NOT enabled, this is a critical error.
res.Success = False
res.ErrorMessage = $"ERROR: Número de parámetros excesivo para "${CommandName}" (DB: ${DBKey}). Se esperaban ${expectedParams} y se recibieron ${receivedParamsSize}. La tolerancia a parámetros extra está DESHABILITADA."$
res.ErrorMessage = $"ERROR: Excessive number of parameters for "${CommandName}" (DB: ${DBKey}). Expected ${expectedParams} and received ${receivedParamsSize}. Extra parameter tolerance is DISABLED."$
Log(res.ErrorMessage)
Main.LogServerError("ERROR", "ParameterValidationUtils.ValidateAndAdjustParameters", res.ErrorMessage, DBKey, CommandName, Null)
Return res
End If
Else
' Caso 3: Se recibieron el número EXACTO de parámetros. Todo bien.
res.ParamsToExecute = receivedParams ' Usamos la lista original tal cual.
res.Success = True ' Confirmamos éxito.
' The EXACT number of parameters was received. All good.
res.ParamsToExecute = receivedParams ' Use the original list as-is.
res.Success = True ' Confirm success.
End If
Return res
End Sub
End Sub

View File

@@ -4,171 +4,167 @@ ModulesStructureVersion=1
Type=Class
Version=4.19
@EndOfDesignText@
' Módulo de clase: RDCConnector
' Esta clase gestiona el pool de conexiones a una base de datos específica.
' REFRACTORIZADO: Usa ConnectionPoolManager y delega a HikariCP.
' Class module: RDCConnector
' This class manages the connection pool for a specific database.
' Uses ConnectionPoolManager and delegates to HikariCP.
Sub Class_Globals
' --- Variables globales de la clase ---
Private pool As ConnectionPoolManager ' Usa el Manager para la modularidad
Private MyHikariPool As HikariConnectionPool ' <-- NUEVO: Pool dedicado a esta DBKey.
Private DebugQueries As Boolean = False ' Bandera para activar/desactivar el modo de depuración
Public commands As Map ' Comandos SQL cargados
' --- Class global variables ---
Private MyHikariPool As HikariConnectionPool ' Dedicated pool for this DBKey.
Private DebugQueries As Boolean = False ' Flag to enable/disable debug mode
Public commands As Map ' Loaded SQL commands
Public serverPort As Int
Public usePool As Boolean = True
Public config As Map ' Configuración completa (JdbcUrl, User, Password, etc.)
Public IsParameterToleranceEnabled As Boolean ' Tolerancia a parámetros de más
Dim driverProperties As Map ' CRÍTICO: Propiedades específicas del driver (MySQL statement caching)
Private configLocation As String ' Ubicación del archivo de configuración
Public config As Map ' Complete configuration (JdbcUrl, User, Password, etc.)
Public IsParameterToleranceEnabled As Boolean ' Tolerance for extra parameters
Dim driverProperties As Map ' Specific driver properties (MySQL statement caching)
Private configLocation As String ' Configuration file location
Private poolProperties As Map
End Sub
' Subrutina de inicialización para el conector de una base de datos específica.
' Initialization subroutine for a specific database connector.
Public Sub Initialize(DB As String)
' Nota: Este código asume que MyHikariPool ya está declarado en Class_Globals
Dim dbKey As String = DB ' Usaremos DB como la llave
' Note: This code assumes MyHikariPool is already declared in Class_Globals
Dim dbKey As String = DB ' We will use DB as the key
If DB.EqualsIgnoreCase("DB1") Then dbKey = ""
poolProperties.Initialize
driverProperties.Initialize
' PASO 1: Cargar la configuración desde el archivo .properties correspondiente.
config = LoadConfigMap(dbKey) ' Aseguramos la carga en la variable de clase [1]
' Load the configuration from the corresponding .properties file.
config = LoadConfigMap(dbKey) ' Ensure it's loaded into the class variable
' Lectura de la configuración de tolerancia de parámetros
' Read the parameter tolerance configuration
Dim toleranceSetting As Int = config.GetDefault("parameterTolerance", 0).As(Int)
IsParameterToleranceEnabled = (toleranceSetting = 1)
If IsParameterToleranceEnabled Then
Log($"RDCConnector.Initialize para ${dbKey}: Tolerancia a parámetros extras, HABILITADA."$)
Log($"RDCConnector.Initialize for ${dbKey}: Extra parameter tolerance ENABLED."$)
Else
Log($"RDCConnector.Initialize para ${dbKey}: Tolerancia a parámetros extras, DESHABILITADA (modo estricto)."$)
Log($"RDCConnector.Initialize for ${dbKey}: Extra parameter tolerance DISABLED (strict mode)."$)
End If
' Bloque Try-Catch para la inicialización y configuración del pool.
' Try-Catch block for pool initialization and configuration.
Try
Dim driverClass As String = config.Get("DriverClass")
Dim jdbcUrl As String = config.Get("JdbcUrl")
Dim aUser As String = config.Get("User")
Dim aPassword As String = config.Get("Password")
Dim poolType As String = "Hikari" ' Forzamos Hikari
Dim poolType As String = "Hikari" ' Force Hikari
' *** INICIO DE LA LÓGICA DE PRECEDENCIA DE TAMAÑO (HIKARI) ***
' --- Size precedence logic (HIKARI) ---
Dim maxSizeKey As String = $"pool.${poolType.ToLowerCase}.maximumpoolsize"$
Dim poolSizeString As String
Dim poolSize As Int
' Intentamos leer el valor específico (pool.hikari.maximumpoolsize).
' Attempt to read the specific value (pool.hikari.maximumpoolsize).
If config.ContainsKey(maxSizeKey) Then
poolSizeString = config.Get(maxSizeKey)
poolSize = poolSizeString.As(Int)
Else
' Si no está definido, usamos el default recomendado por Hikari (10). [2]
' If not defined, use the recommended Hikari default (10).
poolSize = 10
End If
If poolSize < 1 Then poolSize = 10 ' Mantenemos la sensatez
Log($"RDCConnector: Usando MaximumPoolSize para ${poolType} calculado: ${poolSize}"$)
If poolSize < 1 Then poolSize = 10 ' Keep it sensible
Log($"RDCConnector: Using MaximumPoolSize for ${poolType} calculated: ${poolSize}"$)
' *** PASO 2: INICIALIZA/CREA EL POOL LOCALMENTE (Decoupling CRÍTICO) ***
If MyHikariPool.IsInitialized = False Then MyHikariPool.Initialize ' Inicializa el wrapper local
' Crea el pool subyacente (DataSource) en esta instancia dedicada. [3]
' Initialize/Create the pool locally
If MyHikariPool.IsInitialized = False Then MyHikariPool.Initialize ' Initialize the local wrapper
' Create the underlying pool (DataSource) in this dedicated instance.
MyHikariPool.CreatePool2(driverClass, jdbcUrl, aUser, aPassword, poolSize)
' PASO 3a: Cargar y filtrar SOLO las propiedades del Pool (ej. las que comienzan con 'pool.hikari.')
' Load and filter ONLY the Pool properties (e.g., those starting with 'pool.hikari.')
LoadPoolProperties(poolType, config)
' PASO 3b: Aplicar propiedades de ESTABILIDAD (Pool Properties)
' Apply STABILITY properties (Pool Properties)
If poolProperties.Size > 0 Then
' Aplicación directa al pool local. [4]
' Direct application to the local pool.
CallSub2(MyHikariPool, "SetProperties", poolProperties)
End If
' PASO 4: Cargar propiedades específicas del Driver (ej. Statement Caching)
' Load specific Driver properties (e.g., Statement Caching)
If config.ContainsKey("DriverShortName") Then
LoadDriverProperties(config.Get("DriverShortName"), config)
End If
' PASO 5: Aplicar propiedades de RENDIMIENTO (Driver Properties)
' Apply PERFORMANCE properties (Driver Properties)
If driverProperties.Size > 0 Then
' Aplicación directa al pool local. [5]
' Direct application to the local pool.
CallSub2(MyHikariPool, "SetDriverProperties", driverProperties)
Log($"RDCConnector.Initialize para ${DB}: {driverProperties.Size} propiedades del Driver aplicadas a HikariCP."$)
Log($"RDCConnector.Initialize for ${DB}: {driverProperties.Size} Driver properties applied to HikariCP."$)
End If
' PASO 6 (Prueba de vida): Forzar la creación de conexiones iniciales y verificar el estado.
' Esto garantiza el fail-fast. [6]
' Force initial connection creation and check status.
' This ensures fail-fast.
Dim tempCon As SQL = MyHikariPool.GetConnection
If tempCon.IsInitialized Then
tempCon.Close
End If
' Cargar configuración estática en el cache global
' Load static configuration into the global cache
Dim dbKeyToStore As String = DB
If dbKeyToStore = "" Then dbKeyToStore = "DB1"
' Almacenamos el mapa completo (configuración estática + métricas dinámicas iniciales) en el cache global.
' GetPoolStats ahora usa MyHikariPool.
' Store the complete map (static config + initial dynamic metrics) in the global cache.
' GetPoolStats now uses MyHikariPool.
Dim initialPoolStats As Map = GetPoolStats
Main.LatestPoolStats.Put(dbKeyToStore, initialPoolStats)
Catch
' Si ocurre un error durante la inicialización del pool o al forzar la conexión.
Dim ErrorMsg As String = $"RDCConnector.Initialize para ${DB}: ERROR CRÍTICO al inicializar/forzar conexión: ${LastException.Message}"$
' If an error occurs during pool initialization or when forcing the connection.
Dim ErrorMsg As String = $"RDCConnector.Initialize for ${DB}: CRITICAL ERROR initializing/forcing connection: ${LastException.Message}"$
Log(ErrorMsg)
Main.LogServerError("ERROR", "RDCConnector.Initialize", ErrorMsg, DB, Null, Null)
' Si falla la inicialización, la instancia local MyHikariPool se queda inutilizada.
' Aquí podríamos considerar la opción de llamar a MyHikariPool.ClosePool para asegurar
' que no queden recursos parciales, aunque HikariCP debería manejarse con fail-fast.
' If initialization fails, the local MyHikariPool instance remains unusable.
' We could consider calling MyHikariPool.ClosePool here to ensure
' that no partial resources are left, although HikariCP should handle fail-fast.
End Try
' Carga los comandos SQL predefinidos de esta base de datos en el mapa global 'commandsMap' de Main.
' Load predefined SQL commands for this database into Main's global 'commandsMap'.
If dbKey = "" Then dbKey = "DB1"
LoadSQLCommands(config, dbKey)
serverPort = config.Get("ServerPort")
End Sub
' Carga el mapa de configuración
' Load the configuration map
Private Sub LoadConfigMap(DB As String) As Map
Private DBX As String = ""
If DB <> "" Then DBX = "." & DB
Log($"RDCConnector.LoadConfigMap: Leemos el config${DBX}.properties"$)
Log($"RDCConnector.LoadConfigMap: Reading config${DBX}.properties"$)
Return File.ReadMap("./", "config" & DBX & ".properties")
End Sub
' Obtiene la sentencia SQL completa para un comando dado.
' Get the complete SQL statement for a given command.
Public Sub GetCommand(DB As String, Key As String) As String
commands = Main.commandsMap.Get(DB).As(Map)
If commands.ContainsKey("sql." & Key) = False Then
Dim ErrorMsg As String = $"RDCConnector.GetCommand: *** Comando no encontrado: '${Key}' para DB: '${DB}' ***"$
Dim ErrorMsg As String = $"RDCConnector.GetCommand: *** Command not found: '${Key}' for DB: '${DB}' ***"$
Log(ErrorMsg)
Main.LogServerError("ERROR", "RDCConnector.GetCommand", ErrorMsg, DB, Key, Null)
End If
Return commands.Get("sql." & Key)
End Sub
' Obtiene una conexión SQL funcional del pool de conexiones.
' Get a functional SQL connection from the connection pool.
Public Sub GetConnection(DB As String) As SQL
' If DB.EqualsIgnoreCase("DB1") Then DB = ""
' If DebugQueries Then LoadSQLCommands(LoadConfigMap(DB), DB) ' Deshabilitado por defecto. [13]
' Devolvemos la conexión del pool local, si está inicializado.
' El ConnectionPoolManager ha sido removido. La delegación debe ir directamente
' al pool dedicado de HikariCP. Esto simplifica la ruta crítica.
If MyHikariPool.IsInitialized Then
Return MyHikariPool.GetConnection
Else
Log($"ERROR: Intento de obtener conexión de DBKey ${DB}, pero MyHikariPool no está inicializado."$)
' Devolver Null o lanzar excepción, dependiendo del manejo de errores deseado.
Log($"ERROR: Attempt to get connection for DBKey ${DB}, but MyHikariPool is not initialized."$)
' Es crucial retornar Null si no está inicializado para que el handler capture el error.
Return Null
End If
' ANTES: Return Main.ConnectionPoolManager1.GetConnection
End Sub
' Carga todos los comandos SQL del mapa de configuración en el mapa global 'commandsMap' de Main.
' Load all SQL commands from the config map into Main's global 'commandsMap'.
Private Sub LoadSQLCommands(config2 As Map, DB As String)
Dim newCommands As Map
newCommands.Initialize
@@ -181,48 +177,45 @@ Private Sub LoadSQLCommands(config2 As Map, DB As String)
Main.commandsMap.Put(DB, commands)
End Sub
' ** Delegación de estasticas de C3P0 a HikariCP **
' Delegation of statistics from C3P0 to HikariCP
Public Sub GetPoolStats As Map
Dim stats As Map
stats.Initialize
If MyHikariPool.IsInitialized Then
Try
' 2. Llamamos al método delegado GetStats en el wrapper de HikariCP.
' 2. Call the delegated GetStats method in the HikariCP wrapper.
Dim hikariStats As Map = MyHikariPool.GetStats
Return hikariStats
Catch
' Fallo en el método GetStats del wrapper.
Dim ErrorMsg As String = $"RDCConnector.GetPoolStats: ERROR CRÍTICO al obtener estadísticas de HikariCP: ${LastException.Message}"$
' Failure in the wrapper's GetStats method.
Dim ErrorMsg As String = $"RDCConnector.GetPoolStats: CRITICAL ERROR getting stats from HikariCP: ${LastException.Message}"$
Log(ErrorMsg)
stats.Put("Error", LastException.Message)
End Try
Else
stats.Put("Error", "Pool local MyHikariPool no inicializado.")
stats.Put("Error", "Local pool MyHikariPool not initialized.")
End If
Return stats
End Sub
' *** NUEVA SUBRUTINA: Cierra el pool de conexiones de forma ordenada ***
' Close the connection pool cleanly
Public Sub Close
If MyHikariPool.IsInitialized Then
' Cierre limpio del pool subyacente.
' Clean closure of the underlying pool.
MyHikariPool.ClosePool
Log($"RDCConnector.Close: Pool Hikari cerrado limpiamente para este conector."$)
Log($"RDCConnector.Close: Hikari pool closed cleanly for this connector."$)
End If
' Ya NO delegamos el cierre al Manager.
' ANTES: Main.ConnectionPoolManager1.ClosePoolByType(poolType) [15]
' La línea de delegación a Main.ConnectionPoolManager1.ClosePoolByType(poolType) ha sido eliminada.
End Sub
' --- SUBRUTINAS DE UTILIDAD PARA CARGA DE PROPIEDADES ---
' --- UTILITY SUBROUTINES FOR LOADING PROPERTIES ---
' [2]
Private Sub LoadDriverProperties(driverShortName As String, config_ As Map)
driverProperties = ExtractProperties($"driver.${driverShortName.trim}."$, config_, Null, Null)
End Sub
' [3]
Private Sub ExtractProperties(prefix As String, input As Map, newPrefix As String, output As Map) As Map
Dim properties As Map
If output = Null Or output.IsInitialized = False Then
@@ -232,7 +225,7 @@ Private Sub ExtractProperties(prefix As String, input As Map, newPrefix As Strin
End If
If newPrefix.EqualsIgnoreCase(Null) Then newPrefix = ""
Dim prefixLength As Int = prefix.Length
' Log($"Prefijo=${prefix}, ${newPrefix}"$)
For Each k As String In input.Keys
' Log(k)
@@ -243,11 +236,11 @@ Private Sub ExtractProperties(prefix As String, input As Map, newPrefix As Strin
properties.Put($"${newPrefix}${standardizedKey}"$, input.Get(k))
End If
Next
Return properties
End Sub
Private Sub LoadPoolProperties(poolType As String, config_ As Map)
' Busca entradas como 'pool.hikari.<propiedad>' y las extrae.
' Find entries like 'pool.hikari.<property>' and extract them.
poolProperties = ExtractProperties($"pool.${poolType.ToLowerCase}."$, config_, Null, Null)
End Sub
End Sub

View File

@@ -30,26 +30,25 @@ Library7=jshell
Library8=json
Library9=jsql
Module1=Cambios
Module10=LoginHandler
Module11=LogoutHandler
Module12=Manager
Module13=ParameterValidationUtils
Module14=ping
Module15=RDCConnector
Module16=SSE
Module17=SSEHandler
Module18=TestHandler
Module10=LogoutHandler
Module11=Manager
Module12=ParameterValidationUtils
Module13=ping
Module14=RDCConnector
Module15=SSE
Module16=SSEHandler
Module17=TestHandler
Module2=ChangePassHandler
Module3=ConnectionPoolManager
Module4=DBHandlerB4X
Module5=DBHandlerJSON
Module6=DoLoginHandler
Module7=faviconHandler
Module8=GlobalParameters
Module9=HikariConnectionPool
Module3=DBHandlerB4X
Module4=DBHandlerJSON
Module5=DoLoginHandler
Module6=faviconHandler
Module7=GlobalParameters
Module8=HikariConnectionPool
Module9=LoginHandler
NumberOfFiles=9
NumberOfLibraries=9
NumberOfModules=18
NumberOfModules=17
Version=10.3
@EndOfDesignText@
'Non-UI application (console / server application)
@@ -95,51 +94,42 @@ Version=10.3
Sub Process_Globals
' The main B4J HTTP server object
Public srvr As Server
' The current version of this modified jRDC server
Public const VERSION As Float = 2.23
' Custom types for serializing/deserializing data
Type DBCommand (Name As String, Parameters() As Object)
Type DBResult (Tag As Object, Columns As Map, Rows As List)
' Holds a list of configured database identifiers (e.g., "DB1", "DB2")
Public listaDeCP As List
' A temporary list to store found configuration file names during startup
Private cpFiles As List
' Global maps to manage database connectors and loaded SQL commands
Public Connectors, commandsMap As Map
' SQL object for interacting with the local users and logs database (SQLite)
Public SQL1 As SQL
' SQL object for interacting with the local users database (Authentication)
Public SQL_Auth As SQL ' --- NEW INSTANCE FOR AUTHENTICATION
' SQL object for interacting with the local logs database (Performance/Errors)
Public SQL_Logs As SQL ' --- NEW INSTANCE FOR LOGS
' Defines the storage mode for the Log database (SQL_Logs). Default is DISK.
' Options: "DISK" (persistent) or "MEMORY" (in-memory, lost on exit).
Private const LOG_DB_MODE As String = "DISK"
' Object for securely hashing and verifying passwords
Private bc As BCrypt
' A Java ReentrantLock object to protect Main.Connectors during Hot-Swapping (thread-safety)
Public MainConnectorsLock As JavaObject
' A Java ReentrantLock object to protect the log caches (QueryLogCache and ErrorLogCache)
Public LogCacheLock As JavaObject
' Timer for executing periodic tasks, such as log cleanup
Public timerLogs As Timer
' Map to store the SQLite logging status (True/False) for each DBKey (DB1, DB2, etc.)
Public SQLiteLoggingStatusByDB As Map
' Global flag indicating if AT LEAST one database has SQLite logging enabled
Public IsAnySQLiteLoggingEnabled As Boolean
' Type to encapsulate the result of parameter validation
Type ParameterValidationResult ( _
Success As Boolean, _
ErrorMessage As String, _
ParamsToExecute As List _ ' The final list of parameters to use in the SQL execution
)
' In-memory cache for performance logs (query_logs)
Public QueryLogCache As List
' In-memory cache for error and warning logs
@@ -156,13 +146,11 @@ Sub Process_Globals
Private const VACUUM_CYCLES As Int = 48
' Granular control for TEXT file logging (CSV)
Public TextLoggingStatusByDB As Map
' Main object for managing all connection pools (RDCConnector instances)
Public ConnectionPoolManager1 As ConnectionPoolManager
End Sub
' --- Main application entry point ---
Sub AppStart (Args() As String)
' Initialize Server-Sent Events handler
SSE.Initialize
' Set logger flag based on build mode (DEBUG or RELEASE)
@@ -173,13 +161,13 @@ Sub AppStart (Args() As String)
#else
logger = False
#End If
Log("LOG_CACHE_THRESHOLD: " & LOG_CACHE_THRESHOLD)
Log("Log_Cache_Threshold: " & LOG_CACHE_THRESHOLD)
' Copy web admin panel files if they don't exist
CopiarRecursoSiNoExiste("manager.html", "www")
CopiarRecursoSiNoExiste("login.html", "www")
' Copy root files (configs, start/stop scripts) if they don't exist
CopiarRecursoSiNoExiste("config.properties", "")
' CopiarRecursoSiNoExiste("config.DB2.properties", "")
@@ -189,16 +177,16 @@ Sub AppStart (Args() As String)
CopiarRecursoSiNoExiste("stop.bat", "")
CopiarRecursoSiNoExiste("reiniciaProcesoBow.bat", "")
CopiarRecursoSiNoExiste("reiniciaProcesoPM2.bat", "")
' Initialize the BCrypt password hashing library
bc.Initialize("BC")
' Initialize in-memory log caches
QueryLogCache.Initialize
ErrorLogCache.Initialize
' === 1. Initialize the local user database (SQLite) and log tables ===
InitializeSQLiteDatabase
' === 2. Initialize global maps defined in GlobalParameters.bas ===
GlobalParameters.mpLogs.Initialize
GlobalParameters.mpTotalRequests.Initialize
@@ -228,11 +216,8 @@ Sub AppStart (Args() As String)
' Initialize the lock for log caches
LogCacheLock.InitializeNewInstance("java.util.concurrent.locks.ReentrantLock", Null)
' Initialize the Manager, which in turn initializes all pool wrappers.
ConnectionPoolManager1.Initialize
' === 4. INITIALIZATION OF THE MAIN DATABASE CONNECTOR (DB1) ===
Try
' Initialize the main 'DB1' connector
Dim con1 As RDCConnector
@@ -244,19 +229,19 @@ Sub AppStart (Args() As String)
' Add 'DB1' to the list of active database keys
listaDeCP.Add("DB1")
Log($"Main.AppStart: Connector 'DB1' initialized successfully on port: ${srvr.Port}"$)
' Read the 'enableSQLiteLogs' setting from config.properties (default to 0)
Dim enableLogsSetting As Int = con1.config.GetDefault("enableSQLiteLogs", 0).As(Int)
Dim isEnabled As Boolean = (enableLogsSetting = 1)
' Store the status in the granular map
SQLiteLoggingStatusByDB.Put("DB1", isEnabled)
' Read the 'enableTextLogging' setting
Dim enableTextLogsSetting As Int = con1.config.GetDefault("enableTextLogging", 0).As(Int)
Dim isTextEnabled As Boolean = (enableTextLogsSetting = 1)
' Store the text log status
TextLoggingStatusByDB.Put("DB1", isTextEnabled)
Catch
' This is a critical failure; the server cannot start without DB1
Dim ErrorMsg As String = $"Main.AppStart: CRITICAL ERROR initializing connector 'DB1': ${LastException.Message}"$
@@ -266,53 +251,53 @@ Sub AppStart (Args() As String)
' Stop the application
ExitApplication
End Try
' === 5. DETECTION AND INITIALIZATION OF ADDITIONAL DATABASES (DB2, DB3, DB4) ===
' Scan the application's root directory for configuration files
cpFiles = File.ListFiles("./")
If cpFiles.Size > 0 Then
For Each fileName As String In cpFiles
Dim keyPrefix As String = "config."
Dim keySuffix As String = ".properties"
' 1. Filter and exclude DB1 (which is already loaded)
' Find files matching "config.xxx.properties" but not "config.properties"
If fileName.StartsWith(keyPrefix) And fileName.EndsWith(keySuffix) And fileName <> "config.properties" Then
Try
' 2. Extract the key ("xxx" from config.xxx.properties)
Dim keyLength As Int = fileName.Length - keySuffix.Length
Dim dbKey As String = fileName.SubString2(keyPrefix.Length, keyLength)
' ROBUSTNESS: Ensure the key is UPPERCASE for consistency.
' Handlers normalize the key to uppercase, so we must match that.
dbKey = dbKey.ToUpperCase.Trim
Log($"Main.AppStart: Configuration file detected: '${fileName}'. Initializing connector '${dbKey}'."$)
Dim newCon As RDCConnector
' 3. Initialize the RDC Connector (which reads its own config.dbKey.properties file)
newCon.Initialize(dbKey)
' 4. Update global structures (Thread-Safe Maps)
Connectors.Put(dbKey, newCon)
listaDeCP.Add(dbKey)
' 5. Granular Logging Logic
' Capture the logging status for this new DB
Dim enableLogsSetting As Int = newCon.config.GetDefault("enableSQLiteLogs", 0).As(Int)
Dim isEnabled As Boolean = (enableLogsSetting = 1)
SQLiteLoggingStatusByDB.Put(dbKey, isEnabled)
' Capture text logging status for this new DB
Dim enableTextLogsSetting As Int = newCon.config.GetDefault("enableTextLogging", 0).As(Int)
Dim isTextEnabled As Boolean = (enableTextLogsSetting = 1)
TextLoggingStatusByDB.Put(dbKey, isTextEnabled)
Log("TEXT LOGGING STATUS BY DB: " & TextLoggingStatusByDB)
' Note: Global re-evaluation of IsAnySQLiteLoggingEnabled is done at the end of AppStart.
Catch
' 6. Error Handling: If a file is invalid (e.g., bad credentials, malformed URL),
' the server should log the error but continue trying with the next file.
@@ -323,7 +308,7 @@ Sub AppStart (Args() As String)
End If
Next
End If
' Final log of all databases the server is managing.
Dim sbListaDeCP_Log As StringBuilder
sbListaDeCP_Log.Initialize
@@ -336,10 +321,10 @@ Sub AppStart (Args() As String)
Log($"Main.AppStart: Configured and ready databases: [${sbListaDeCP_Log.ToString}]"$)
' <<<< Initialization block for the log cleanup Timer >>>>
' UNCONDITIONAL Initialization of the Timer (Ensures the object exists and prevents IllegalStateException)
timerLogs.Initialize("TimerLogs", 1800000) ' 30 minutes = 1800 * 1000 = 1800000 ms
' CONDITIONAL CONTROL BASED ON GRANULAR STATUS
IsAnySQLiteLoggingEnabled = False
For Each dbStatus As Boolean In SQLiteLoggingStatusByDB.Values
@@ -348,7 +333,7 @@ Sub AppStart (Args() As String)
Exit ' If one is active, it's enough to turn on the Timer
End If
Next
If IsAnySQLiteLoggingEnabled Then
timerLogs.Enabled = True
If logger Then Log("Main.AppStart: Log cleanup timer ACTIVATED (at least one DB requires logs).")
@@ -385,140 +370,75 @@ End Sub
' --- Subroutine to initialize the local user database (SQLite) ---
Sub InitializeSQLiteDatabase
Dim dbFileName As String = "users.db"
' Check if the database file already exists
If File.Exists(File.DirApp, dbFileName) = False Then
' --- Create a new database ---
Log("Creating new user database: " & dbFileName)
SQL1.InitializeSQLite(File.DirApp, dbFileName, True)
' Create 'users' table
Dim createUserTable As String = "CREATE TABLE users (username TEXT PRIMARY KEY, password_hash TEXT NOT NULL)"
SQL1.ExecNonQuery(createUserTable)
Private dbFileName As String = "users.db"
Private dbDirName As String = GlobalParameters.WorkingDirectory
' --- Configuration for SQL_Logs based on LOG_DB_MODE ---
Private logDirName As String = dbDirName
Private logFileName As String = dbFileName
Private isInMemoryMode As Boolean = (LOG_DB_MODE = "MEMORY")
If isInMemoryMode Then
' For in-memory databases, use the special filename ":memory:" and empty directory.
logDirName = ""
logFileName = ":memory:"
If logger Then Log("NOTICE: SQL_Logs initialized as IN-MEMORY database (data is non-persistent).")
Else
If logger Then Log($"NOTICE: SQL_Logs initialized as DISK database: ${dbFileName}"$)
End If
' Create 'query_logs' table
If logger Then Log("Creating 'query_logs' table with performance columns.")
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)
' Initialize SQL_Auth (always points to the disk file for user persistence).
SQL_Auth.InitializeSQLite(dbDirName, dbFileName, True)
' Initialize SQL_Logs (points to disk file or :memory:)
SQL_Logs.InitializeSQLite(logDirName, logFileName, True)
' Set PRAGMA for better performance (Write-Ahead Logging)
SQL1.ExecNonQuery("PRAGMA journal_mode=WAL;")
SQL1.ExecNonQuery("PRAGMA synchronous=NORMAL;")
' Check if schema creation/migration is necessary.
' This is true if the disk file is brand new OR if we are running in memory mode.
Private isNewDbFile As Boolean = File.Exists(dbDirName, dbFileName) = False
' Insert default user
Dim defaultUser As String = "admin"
Dim defaultPass As String = "admin"
Dim hashedPass As String = bc.hashpw(defaultPass, bc.gensalt)
SQL1.ExecNonQuery2("INSERT INTO users (username, password_hash) VALUES (?, ?)", Array As Object(defaultUser, hashedPass))
Log($"Default user created -> user: ${defaultUser}, pass: ${defaultPass}"$)
' Create 'errores' (errors) table
Log("Creating 'errores' table for event logging.")
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("Creating performance indexes on log tables.")
' Index on timestamp for fast cleanup (DELETE/ORDER BY) in query_logs
SQL1.ExecNonQuery("CREATE INDEX idx_query_timestamp ON query_logs(timestamp)")
' Index on duration_ms for the 'slowqueries' query (ORDER BY)
SQL1.ExecNonQuery("CREATE INDEX idx_query_duration ON query_logs(duration_ms)")
' Index on timestamp for fast cleanup of the errors table
SQL1.ExecNonQuery("CREATE INDEX idx_error_timestamp ON errores(timestamp)")
If isNewDbFile Or isInMemoryMode Then
If logger Then Log("Schema creation required (New DB file or In-Memory mode).")
' 1. TABLE CREATION (Done via SQL_Logs instance, as it handles the schema)
SQL_Logs.ExecNonQuery("CREATE TABLE users (username TEXT PRIMARY KEY, password_hash TEXT, last_login_timestamp INTEGER, is_admin INTEGER DEFAULT 0)")
SQL_Logs.ExecNonQuery("CREATE TABLE query_logs (query_name TEXT, duration_ms INTEGER, timestamp INTEGER, db_key TEXT, client_ip TEXT, busy_connections INTEGER, handler_active_requests INTEGER, timestamp_text_local TEXT)")
SQL_Logs.ExecNonQuery("CREATE TABLE errores (timestamp INTEGER, type TEXT, source TEXT, message TEXT, db_key TEXT, command_name TEXT, client_ip TEXT)")
' 2. INDEX CREATION (Done via SQL_Logs instance)
SQL_Logs.ExecNonQuery("CREATE INDEX idx_query_timestamp ON query_logs(timestamp)")
SQL_Logs.ExecNonQuery("CREATE INDEX idx_query_duration ON query_logs(duration_ms)")
SQL_Logs.ExecNonQuery("CREATE INDEX idx_query_dbkey ON query_logs(db_key)") ' --- NEW INDEX: CRITICAL FOR MULTI-DB REPORTS
SQL_Logs.ExecNonQuery("CREATE INDEX idx_error_timestamp ON errores(timestamp)")
' 3. PRAGMAS (Applied to both to ensure consistency in WAL mode)
SQL_Logs.ExecNonQuery("PRAGMA journal_mode=WAL")
SQL_Logs.ExecNonQuery("PRAGMA synchronous=NORMAL")
SQL_Auth.ExecNonQuery("PRAGMA journal_mode=WAL")
SQL_Auth.ExecNonQuery("PRAGMA synchronous=NORMAL")
Else
' --- Load existing database ---
SQL1.InitializeSQLite(File.DirApp, dbFileName, True)
Log("User database loaded.")
' Ensure WAL mode is set on existing DBs
SQL1.ExecNonQuery("PRAGMA journal_mode=WAL;")
SQL1.ExecNonQuery("PRAGMA synchronous=NORMAL;")
' Load existing database (DISK Mode)
If logger Then Log("Existing users.db found. Applying PRAGMAS and checking migrations.")
' >>> START: Migration logic (ALTER TABLE) if the DB already existed <<<
If logger Then Log("Verifying and migrating 'query_logs' table if necessary.")
' Check if 'query_logs' table exists
If SQL1.ExecQuerySingleResult("SELECT name FROM sqlite_master WHERE type='table' AND name='query_logs'") = Null Then
If logger Then Log("'query_logs' table not found, creating it with performance columns.")
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)
Else
' If the query_logs table already exists, check and add missing columns
Dim columnExists As Boolean
Dim rs As ResultSet
' --- VERIFY AND ADD busy_connections ---
columnExists = False
rs = SQL1.ExecQuery("PRAGMA table_info(query_logs)")
Do While rs.NextRow
If rs.GetString("name").EqualsIgnoreCase("busy_connections") Then
columnExists = True
Exit ' Column already exists, exit loop
End If
Loop
rs.Close
If columnExists = False Then
If logger Then Log("Adding column 'busy_connections' to query_logs.")
SQL1.ExecNonQuery("ALTER TABLE query_logs ADD COLUMN busy_connections INTEGER DEFAULT 0")
End If
' --- VERIFY AND ADD handler_active_requests ---
columnExists = False
rs = SQL1.ExecQuery("PRAGMA table_info(query_logs)")
Do While rs.NextRow
If rs.GetString("name").EqualsIgnoreCase("handler_active_requests") Then
columnExists = True
Exit ' Column already exists, exit loop
End If
Loop
rs.Close
If columnExists = False Then
If logger Then Log("Adding column 'handler_active_requests' to query_logs.")
SQL1.ExecNonQuery("ALTER TABLE query_logs ADD COLUMN handler_active_requests INTEGER DEFAULT 0")
End If
' --- VERIFY AND ADD timestamp_text_local ---
columnExists = False
rs = SQL1.ExecQuery("PRAGMA table_info(query_logs)")
Do While rs.NextRow
If rs.GetString("name").EqualsIgnoreCase("timestamp_text_local") Then
columnExists = True
Exit ' Column already exists, exit loop
End If
Loop
rs.Close
If columnExists = False Then
If logger Then Log("Adding column 'timestamp_text_local' to query_logs.")
' Use 'TEXT' to store the formatted date/time string.
SQL1.ExecNonQuery("ALTER TABLE query_logs ADD COLUMN timestamp_text_local TEXT")
End If
' >>> START: Migration logic for 'errores' if DB already existed <<<
If logger Then Log("Verifying and migrating 'errores' table if necessary.")
If SQL1.ExecQuerySingleResult("SELECT name FROM sqlite_master WHERE type='table' AND name='errores'") = Null Then
If logger Then Log("'errores' table not found, creating it.")
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)
Else
If logger Then Log("'errores' table already exists.")
End If
' >>> END: Migration logic for 'errores' <<<
' Ensure PRAGMAS are set on both connections
SQL_Logs.ExecNonQuery("PRAGMA journal_mode=WAL")
SQL_Logs.ExecNonQuery("PRAGMA synchronous=NORMAL")
SQL_Auth.ExecNonQuery("PRAGMA journal_mode=WAL")
SQL_Auth.ExecNonQuery("PRAGMA synchronous=NORMAL")
' >>> Migration Logic (INDEX VERIFICATION) <<<
' Migration check must run on SQL_Logs
' --- VERIFY AND ADD idx_query_dbkey INDEX ---
If SQL_Logs.ExecQuerySingleResult($"SELECT name FROM sqlite_master WHERE type='index' AND name='idx_query_dbkey'"$) = Null Then
If logger Then Log("Adding index 'idx_query_dbkey' to query_logs.")
SQL_Logs.ExecNonQuery("CREATE INDEX idx_query_dbkey ON query_logs(db_key)")
End If
' >>> END: Migration logic (ALTER TABLE) <<<
' (Migration logic for other assumed columns/tables should use SQL_Logs)
End If
End Sub
@@ -606,61 +526,55 @@ Public Sub WriteQueryLogsBatch
logsToWrite.Initialize ' 1. Initialize the local list (CRITICAL)
' === STEP 1: Atomic Cache Swap (Protected by ReentrantLock) ===
LogCacheLock.RunMethod("lock", Null)
If QueryLogCache.Size = 0 Then
' Cache is empty, release lock and return
LogCacheLock.RunMethod("unlock", Null)
Return
End If
' *** CRITICAL FIX: Copy content (AddAll) instead of reference. ***
logsToWrite.AddAll(QueryLogCache)
Dim batchSize As Int = logsToWrite.Size
' Clear the global cache. logsToWrite now holds the copy of the items.
QueryLogCache.Initialize
LogCacheLock.RunMethod("unlock", Null)
' Check if text logging is enabled for any of these logs
If logsToWrite.Size > 0 Then
' Call the text archiving sub on a separate worker thread.
' This is NON-BLOCKING for the current thread, which will proceed to the SQLite transaction.
CallSubDelayed2(Me, "ArchiveQueryLogsToDailyFile", logsToWrite)
End If
' === STEP 2: Transactional Write to SQLite ===
' === STEP 2: Transactional Write to SQLite ===
Try
' 1. Begin the transaction: Everything that follows is a single disk operation.
SQL1.BeginTransaction
' 1. Begin the transaction on the dedicated LOGS instance.
SQL_Logs.BeginTransaction
For Each logEntry As Map In logsToWrite
' Insert the log entry
SQL1.ExecNonQuery2("INSERT INTO query_logs (query_name, duration_ms, timestamp, db_key, client_ip, busy_connections, handler_active_requests, timestamp_text_local) VALUES (?, ?, ?, ?, ?, ?, ?, ?)", _
SQL_Logs.ExecNonQuery2("INSERT INTO query_logs (query_name, duration_ms, timestamp, db_key, client_ip, busy_connections, handler_active_requests, timestamp_text_local) VALUES (?, ?, ?, ?, ?, ?, ?, ?)", _
Array As Object(logEntry.Get("query_name"), logEntry.Get("duration_ms"), logEntry.Get("timestamp"), logEntry.Get("db_key"), _
logEntry.Get("client_ip"), logEntry.Get("busy_connections"), logEntry.Get("handler_active_requests"), _
logEntry.Get("timestamp_text_local")))
logEntry.Get("client_ip"), logEntry.Get("busy_connections"), logEntry.Get("handler_active_requests"), _
logEntry.Get("timestamp_text_local")))
Next
' 2. Finalize the transaction: Efficient write to disk.
SQL1.TransactionSuccessful
SQL_Logs.TransactionSuccessful
If logger Then Log($"[LOG BATCH] Batch of ${batchSize} performance logs written successfully."$)
Catch
' If it fails, undo all logs in this batch and log the failure.
SQL1.Rollback
SQL_Logs.Rollback
Dim ErrorMsg As String = "CRITICAL ERROR: Failed to write performance log batch to SQLite: " & LastException.Message
Log(ErrorMsg)
' Use LogServerError so the failure is recorded in the 'errores' table (if logging is enabled)
LogServerError("ERROR", "Main.WriteQueryLogsBatch", ErrorMsg, Null, "log_batch_write_performance", Null)
End Try
End Sub
' --- Event subroutine for the 'timerLogs' Timer. ---
@@ -674,7 +588,7 @@ Sub TimerLogs_Tick
WriteErrorLogsBatch
' 3. Clean up and VACUUM (this sub also checks IsAnySQLiteLoggingEnabled)
borraArribaDe30000Logs
borraArribaDe10000Logs
Catch
Dim ErrorMsg As String = "ERROR in TimerLogs_Tick while trying to clear logs: " & LastException.Message
@@ -685,114 +599,115 @@ End Sub
' Writes the cached error logs to the SQLite DB in a single transaction
Public Sub WriteErrorLogsBatch
' List to store error logs copied from the cache
Dim logsToWrite As List
logsToWrite.Initialize
' === STEP 1: Atomic Cache Swap (Protected by ReentrantLock) ===
' Bloqueamos el LogCacheLock para garantizar la atomicidad de la copia y limpieza.
' Lock LogCacheLock to guarantee atomicity of copy and cleanup.
LogCacheLock.RunMethod("lock", Null)
If ErrorLogCache.Size = 0 Then
' La caché está vacía, liberamos el lock inmediatamente y salimos.
' Cache is empty, release the lock immediately and exit.
LogCacheLock.RunMethod("unlock", Null)
Return
End If
' *** Copiar el contenido de la caché global de forma atómica. ***
' *** Atomically copy global cache content. ***
logsToWrite.AddAll(ErrorLogCache)
' Usar el tamaño de la lista copiada para el procesamiento.
Dim batchSize As Int = logsToWrite.Size
' Log(logsToWrite)
' Limpiar la caché global. logsToWrite es ahora una lista independiente y poblada.
' Clean the global cache. logsToWrite is now an independent and populated list.
ErrorLogCache.Initialize
LogCacheLock.RunMethod("unlock", Null) ' Release the lock.
LogCacheLock.RunMethod("unlock", Null) ' Liberar el lock.
If logger Then Log($"[LOG BATCH] Starting transactional write of ${batchSize} ERROR logs to SQLite. Logs copied: ${batchSize}"$)
' === La corrección de Lógica ocurre aquí: La llamada a ArchiveErrorLogsToDailyFile
' y el proceso transaccional ocurren AHORA, después de asegurar que logsToWrite
' tiene contenido y que el lock fue liberado. ===
' 1. (Opcional, si el logging de texto CSV está habilitado)
' === STEP 1: Archive to daily CSV file (if enabled) ===
If batchSize > 0 Then
' Delegar a una nueva subrutina para manejar la I/O de disco CSV (CallSubDelayed2)
' Delegate to a new subroutine to handle CSV disk I/O (CallSubDelayed2)
CallSubDelayed2(Me, "ArchiveErrorLogsToDailyFile", logsToWrite)
End If
' === STEP 2: Escritura Transaccional a SQLite (Usa logsToWrite) ===
' === STEP 2: Transactional Write to SQLite (Uses logsToWrite) ===
If batchSize = 0 Then
' Este caso no debería ocurrir con la lógica anterior, pero es un chequeo de seguridad.
Log("WARNING: Failed to copy list. logsToWrite is empty. Aborting write.")
Return
End If
Try
' 1. Iniciar la transacción.
SQL1.BeginTransaction
' 1. Start transaction on the dedicated LOGS instance.
SQL_Logs.BeginTransaction
For Each logEntry As Map In logsToWrite
' Insertar la entrada de log
SQL1.ExecNonQuery2("INSERT INTO errores (timestamp, type, source, message, db_key, command_name, client_ip) VALUES (?, ?, ?, ?, ?, ?, ?)", _
Array As Object(logEntry.Get("timestamp"), logEntry.Get("type"), logEntry.Get("source"), logEntry.Get("message"), _
logEntry.Get("db_key"), logEntry.Get("command_name"), logEntry.Get("client_ip")))
' Insert the log entry
SQL_Logs.ExecNonQuery2("INSERT INTO errores (timestamp, type, source, message, db_key, command_name, client_ip) VALUES (?, ?, ?, ?, ?, ?, ?)", _
Array As Object(logEntry.Get("timestamp"), logEntry.Get("type"), logEntry.Get("source"), logEntry.Get("message"), _
logEntry.Get("db_key"), logEntry.Get("command_name"), logEntry.Get("client_ip")))
Next
' 2. Confirmar la transacción.
SQL1.TransactionSuccessful
' 2. Commit the transaction.
SQL_Logs.TransactionSuccessful
If logger Then Log($"[LOG BATCH] Batch of ${logsToWrite.Size} ERROR logs written successfully."$)
Catch
' 3. Rollback si falla.
SQL1.Rollback
' 3. Rollback if failed.
SQL_Logs.Rollback
Dim ErrorMsg As String = "CRITICAL ERROR: Failed to write ERROR log batch to SQLite: " & LastException.Message
Log(ErrorMsg)
End Try
End Sub
' Deletes the oldest records from 'query_logs' table and runs VACUUM.
Sub borraArribaDe30000Logs 'ignore
If IsAnySQLiteLoggingEnabled Then ' Only run if at least one DB requires logs.
' 1. Cleanup of Performance Logs (query_logs)
If logger Then Log("Trimming 'query_logs' table, limit of 30,000 records.")
Dim fechaCorte As Long ' (cutoff date/timestamp)
' First, try to find the timestamp of the 30,001st record.
Try ' OFFSET 30000 skips the 30,000 most recent.
fechaCorte = SQL1.ExecQuerySingleResult($"SELECT timestamp FROM query_logs ORDER BY timestamp DESC LIMIT 1 OFFSET 30000"$)
Catch ' If the table has fewer than 30,000 records, the result is NULL or throws an exception.
fechaCorte = 0 ' Force to 0 so it doesn't delete anything.
End Try
' If a cutoff time was found (i.e., there are more than 30,000 records)...
If fechaCorte > 0 Then ' Execute the simple DELETE, which is very fast using the idx_query_timestamp index.
SQL1.ExecNonQuery2("DELETE FROM query_logs WHERE timestamp < ?", Array As Object(fechaCorte))
End If
Sub borraArribaDe10000Logs 'ignore
Private Const LOG_LIMIT_PERFORMANCE As Int = 10000 ' New limit for performance logs
Private Const LOG_LIMIT_ERRORS As Int = 10000 ' Limit for error logs (retained 15,000)
If IsAnySQLiteLoggingEnabled Then ' Only run if at least one DB requires logs.
' 1. Cleanup of Performance Logs (query_logs)
If logger Then Log($"Trimming 'query_logs' table, limit of ${LOG_LIMIT_PERFORMANCE} records."$)
Dim fechaCorte As Long ' (cutoff date/timestamp)
' Find the timestamp of the (LOG_LIMIT_PERFORMANCE + 1)st record using SQL_Logs
Try ' OFFSET skips the most recent records.
fechaCorte = SQL_Logs.ExecQuerySingleResult($"SELECT timestamp FROM query_logs ORDER BY timestamp DESC LIMIT 1 OFFSET ${LOG_LIMIT_PERFORMANCE}"$)
Catch ' If the table has fewer records than the limit.
fechaCorte = 0
End Try
If fechaCorte > 0 Then ' Execute DELETE on SQL_Logs
SQL_Logs.ExecNonQuery2("DELETE FROM query_logs WHERE timestamp < ?", Array As Object(fechaCorte))
End If
' 2. Cleanup of Error Logs (errores)
Dim fechaCorteError As Long
Try ' OFFSET 15000 skips the 15,000 most recent.
fechaCorteError = SQL1.ExecQuerySingleResult($"SELECT timestamp FROM errores ORDER BY timestamp DESC LIMIT 1 OFFSET 15000"$)
Catch ' If the table has fewer than 15,000 records, result is NULL.
Try ' OFFSET LOG_LIMIT_ERRORS skips the most recent.
fechaCorteError = SQL_Logs.ExecQuerySingleResult($"SELECT timestamp FROM errores ORDER BY timestamp DESC LIMIT 1 OFFSET ${LOG_LIMIT_ERRORS}"$)
Catch ' If the table has fewer than 15,000 records.
fechaCorteError = 0
End Try
' If a cutoff time was found...
If fechaCorteError > 0 Then
SQL1.ExecNonQuery2("DELETE FROM errores WHERE timestamp < ?", Array As Object(fechaCorteError))
SQL_Logs.ExecNonQuery2("DELETE FROM errores WHERE timestamp < ?", Array As Object(fechaCorteError))
End If
' 3. Control and Conditional Execution of VACUUM
TimerTickCount = TimerTickCount + 1
If TimerTickCount >= VACUUM_CYCLES Then
If logger Then Log("EXECUTING VACUUM (24-hour cycle completed).")
SQL1.ExecNonQuery("vacuum;") ' Execute VACUUM.
SQL_Logs.ExecNonQuery("vacuum;") ' Execute VACUUM on SQL_Logs.
TimerTickCount = 0 ' Reset the counter.
Else
' Show how many cycles are left, only if logger is active.
If logger Then Log($"VACUUM skipped. ${VACUUM_CYCLES - TimerTickCount} cycles remaining until daily execution."$)
End If
Else
' If IsAnySQLiteLoggingEnabled is False, the Timer should not be active.
If logger Then Log("NOTICE: Log cleanup task skipped. Global SQLite logging is disabled.")
End If
End Sub

View File

@@ -8,7 +8,6 @@ ModuleBookmarks14=
ModuleBookmarks15=
ModuleBookmarks16=
ModuleBookmarks17=
ModuleBookmarks18=
ModuleBookmarks2=
ModuleBookmarks3=
ModuleBookmarks4=
@@ -27,7 +26,6 @@ ModuleBreakpoints14=
ModuleBreakpoints15=
ModuleBreakpoints16=
ModuleBreakpoints17=
ModuleBreakpoints18=
ModuleBreakpoints2=
ModuleBreakpoints3=
ModuleBreakpoints4=
@@ -42,11 +40,10 @@ ModuleClosedNodes10=
ModuleClosedNodes11=
ModuleClosedNodes12=
ModuleClosedNodes13=
ModuleClosedNodes14=
ModuleClosedNodes15=
ModuleClosedNodes16=5,6
ModuleClosedNodes17=2,3
ModuleClosedNodes18=
ModuleClosedNodes14=2,3,4,6,7
ModuleClosedNodes15=5,6
ModuleClosedNodes16=2,3
ModuleClosedNodes17=
ModuleClosedNodes2=
ModuleClosedNodes3=
ModuleClosedNodes4=
@@ -55,6 +52,6 @@ ModuleClosedNodes6=
ModuleClosedNodes7=
ModuleClosedNodes8=
ModuleClosedNodes9=
NavigationStack=DBHandlerB4X,Handle,103,6,DBHandlerB4X,CleanupAndLog,221,0,Main,ArchiveQueryLogsToDailyFile,839,0,RDCConnector,Class_Globals,6,0,DBHandlerJSON,Handle,219,0,Main,AppStart,255,6,Main,LogQueryPerformance,510,0,Main,WriteErrorLogsBatch,654,6,Main,LogServerError,539,6,Main,ArchiveErrorLogsToDailyFile,869,6
NavigationStack=Main,WriteErrorLogsBatch,587,0,DoLoginHandler,Handle,32,0,ChangePassHandler,Handle,9,0,Main,WriteQueryLogsBatch,461,0,RDCConnector,Class_Globals,4,0,Main,Process_Globals,60,0,Main,InitializeSQLiteDatabase,370,0,Main,borraArribaDe10000Logs,612,0,Main,TimerLogs_Tick,532,0,Manager,Handle,79,0,Cambios,Process_Globals,19,0
SelectedBuild=0
VisibleModules=4,5,15,1,12,9,3,13
VisibleModules=3,4,14,1,11,8,12,5,2