AppType=StandardJava Build1=Default,b4j.JRDCMulti File1=config.DB2.properties File10=stop.bat File2=config.DB3.properties File3=config.DB4.properties File4=config.properties File5=login.html File6=reiniciaProcesoBow.bat File7=reiniciaProcesoPM2.bat File8=start.bat File9=start2.bat FileGroup1=Default Group FileGroup10=Default Group FileGroup2=Default Group FileGroup3=Default Group FileGroup4=Default Group FileGroup5=Default Group FileGroup6=Default Group FileGroup7=Default Group FileGroup8=Default Group FileGroup9=Default Group Group=Default Group Library1=byteconverter Library2=javaobject Library3=jcore Library4=jrandomaccessfile Library5=jserver Library6=jshell Library7=json Library8=jsql Library9=bcrypt Module1=Cambios Module10=Manager Module11=ParameterValidationUtils Module12=ping Module13=RDCConnector Module14=TestHandler Module2=ChangePassHandler Module3=DBHandlerB4X Module4=DBHandlerJSON Module5=DoLoginHandler Module6=faviconHandler Module7=GlobalParameters Module8=LoginHandler Module9=LogoutHandler NumberOfFiles=10 NumberOfLibraries=9 NumberOfModules=14 Version=10.3 @EndOfDesignText@ 'Non-UI application (console / server application) #Region Project Attributes #CommandLineArgs: #MergeLibraries: True ' VERSION 5.09.16 '########################################################################################################### '###################### PULL ############################################################# 'Ctrl + click ide://run?file=%WINDIR%\System32\cmd.exe&Args=/c&Args=git&Args=pull '########################################################################################################### '###################### PUSH ############################################################# 'Ctrl + click ide://run?file=%WINDIR%\System32\WindowsPowerShell\v1.0\powershell.exe&Args=github&Args=..\..\ '########################################################################################################### '###################### PUSH TORTOISE GIT ######################################################### 'Ctrl + click ide://run?file=%WINDIR%\System32\WindowsPowerShell\v1.0\powershell.exe&Args=TortoiseGitProc&Args=/command:commit&Args=/path:"../"&Args=/closeonend:2 '########################################################################################################### #End Region 'change based on the jdbc jar file '#AdditionalJar: mysql-connector-java-5.1.27-bin '#AdditionalJar: postgresql-42.7.0 #AdditionalJar: ojdbc11 ' Librería para manejar la base de datos SQLite #AdditionalJar: sqlite-jdbc-3.7.2 Sub Process_Globals ' --- Variables globales accesibles desde cualquier parte del proyecto --- ' Objeto principal del servidor HTTP de B4J. Public srvr As Server ' La versión actual de este servidor jRDC modificado. Public const VERSION As Float = 2.23 ' Tipos personalizados (clases) para la serialización y deserialización de datos Type DBCommand (Name As String, Parameters() As Object) Type DBResult (Tag As Object, Columns As Map, Rows As List) ' Contiene una lista de los identificadores de bases de datos configuradas (ej. "DB1", "DB2"). Public listaDeCP As List ' Una lista temporal para almacenar los nombres de archivos de configuración encontrados. Private cpFiles As List ' Mapas globales para gestionar los conectores de base de datos y los comandos SQL. Public Connectors, commandsMap As Map ' Objeto SQL para interactuar con la base de datos de usuarios y logs (SQLite). Public SQL1 As SQL ' Objeto para realizar operaciones de hashing de contraseñas de forma segura. Private bc As BCrypt ' Objeto de bloqueo (ReentrantLock) para proteger Main.Connectors durante Hot-Swap. Public MainConnectorsLock As JavaObject ' Timer para ejecutar tareas periódicas, como la limpieza de logs. Public timerLogs As Timer ' NUEVAS VARIABLES para control granular de logs ' Mapa para almacenar el estado de logging (True/False) por cada DBKey (DB1, DB2, etc.). Public SQLiteLoggingStatusByDB As Map ' Bandera global que indica si AL MENOS una base de datos tiene los logs habilitados. Public IsAnySQLiteLoggingEnabled As Boolean ' Tipo para encapsular el resultado de la validación de parámetros. Type ParameterValidationResult ( _ Success As Boolean, _ ErrorMessage As String, _ ParamsToExecute As List _ ' La lista de parámetros final a usar en la ejecución SQL ) End Sub Sub AppStart (Args() As String) ' --- Subrutina principal que se ejecuta al iniciar la aplicación --- bc.Initialize("BC") ' 1. Inicializa la base de datos local de usuarios (SQLite) y la tabla de logs. InitializeSQLiteDatabase ' 2. Inicializa los mapas globales definidos en GlobalParameters.bas. GlobalParameters.mpLogs.Initialize GlobalParameters.mpTotalRequests.Initialize GlobalParameters.mpTotalConnections.Initialize GlobalParameters.mpBlockConnection.Initialize ' Aseguramos que el mapa de conteo de peticiones activas sea thread-safe. GlobalParameters.ActiveRequestsCountByDB = srvr.CreateThreadSafeMap ' 3. Inicializa las estructuras principales del servidor HTTP. listaDeCP.Initialize srvr.Initialize("") Connectors = srvr.CreateThreadSafeMap commandsMap.Initialize ' NUEVO: Inicializar el mapa de estado de logs granular SQLiteLoggingStatusByDB.Initialize ' Creamos una instancia de ReentrantLock para proteger Main.Connectors. MainConnectorsLock.InitializeNewInstance("java.util.concurrent.locks.ReentrantLock", Null) ' === 4. INICIALIZACIÓN DEL CONECTOR PARA LA BASE DE DATOS PRINCIPAL (DB1) === Try Dim con1 As RDCConnector con1.Initialize("DB1") Connectors.Put("DB1", con1) srvr.Port = con1.serverPort listaDeCP.Add("DB1") Log($"Main.AppStart: Conector 'DB1' inicializado exitosamente en puerto: ${srvr.Port}"$) ' Lógica de Logs para DB1 (Fuente principal de configuración) Dim enableLogsSetting As Int = con1.config.GetDefault("enableSQLiteLogs", 1).As(Int) Dim isEnabled As Boolean = (enableLogsSetting = 1) SQLiteLoggingStatusByDB.Put("DB1", isEnabled) ' Guardar el estado Catch Dim ErrorMsg As String = $"Main.AppStart: ERROR CRÍTICO al inicializar conector 'DB1': ${LastException.Message}"$ Log(ErrorMsg) LogServerError("ERROR", "Main.AppStart", ErrorMsg, "DB1", Null, Null) ExitApplication End Try ' === 5. DETECCIÓN E INICIALIZACIÓN DE BASES DE DATOS ADICIONALES (DB2, DB3, DB4) === cpFiles = File.ListFiles("./") If cpFiles.Size > 0 Then For i = 0 To cpFiles.Size - 1 ' Procesa 'config.DB2.properties' If cpFiles.Get(i) = "config.DB2.properties" Then Try Dim con2 As RDCConnector con2.Initialize("DB2") Connectors.Put("DB2", con2) listaDeCP.Add("DB2") Log("Main.AppStart: Conector 'DB2' inicializado exitosamente.") ' Lógica de Logs para DB2 Dim enableLogsSetting As Int = con2.config.GetDefault("enableSQLiteLogs", 0).As(Int) Dim isEnabled As Boolean = (enableLogsSetting = 1) SQLiteLoggingStatusByDB.Put("DB2", isEnabled) ' Guardar el estado Catch Dim ErrorMsg As String = $"Main.AppStart: ERROR al inicializar conector 'DB2': ${LastException.Message}"$ Log(ErrorMsg) LogServerError("ERROR", "Main.AppStart", ErrorMsg, "DB2", Null, Null) End Try End If ' Procesa 'config.DB3.properties' If cpFiles.Get(i) = "config.DB3.properties" Then Try Dim con3 As RDCConnector con3.Initialize("DB3") Connectors.Put("DB3", con3) listaDeCP.Add("DB3") Log("Main.AppStart: Conector 'DB3' inicializado exitosamente.") ' Lógica de Logs para DB3 Dim enableLogsSetting As Int = con3.config.GetDefault("enableSQLiteLogs", 0).As(Int) Dim isEnabled As Boolean = (enableLogsSetting = 1) SQLiteLoggingStatusByDB.Put("DB3", isEnabled) ' Guardar el estado Catch Dim ErrorMsg As String = $"Main.AppStart: ERROR al inicializar conector 'DB3': ${LastException.Message}"$ Log(ErrorMsg) LogServerError("ERROR", "Main.AppStart", ErrorMsg, "DB3", Null, Null) End Try End If ' Procesa 'config.DB4.properties' If cpFiles.Get(i) = "config.DB4.properties" Then Try Dim con4 As RDCConnector con4.Initialize("DB4") Connectors.Put("DB4", con4) listaDeCP.Add("DB4") Log("Main.AppStart: Conector 'DB4' inicializado exitosamente.") ' Lógica de Logs para DB4 Dim enableLogsSetting As Int = con4.config.GetDefault("enableSQLiteLogs", 0).As(Int) Dim isEnabled As Boolean = (enableLogsSetting = 1) SQLiteLoggingStatusByDB.Put("DB4", isEnabled) ' Guardar el estado Catch Dim ErrorMsg As String = $"Main.AppStart: ERROR al inicializar conector 'DB4': ${LastException.Message}"$ Log(ErrorMsg) LogServerError("ERROR", "Main.AppStart", ErrorMsg, "DB4", Null, Null) End Try End If Next Else Log("Main.AppStart: No se encontraron archivos de configuración adicionales (config.DBx.properties).") End If ' Log final de las bases de datos que el servidor está gestionando. Dim sbListaDeCP_Log As StringBuilder sbListaDeCP_Log.Initialize For Each item As String In listaDeCP sbListaDeCP_Log.Append(item).Append(", ") Next If sbListaDeCP_Log.Length > 0 Then sbListaDeCP_Log.Remove(sbListaDeCP_Log.Length - 2, sbListaDeCP_Log.Length) End If Log($"Main.AppStart: Bases de datos configuradas y listas: [${sbListaDeCP_Log.ToString}]"$) ' <<<< Bloque de inicialización del Timer para la limpieza de logs >>>> ' Inicialización INCONDICIONAL del Timer (Garantiza que el objeto exista y prevenga el IllegalStateException) timerLogs.Initialize("TimerLogs", 600000) ' 10 minutos = 600 * 1000 = 600000 ms ' CONTROL CONDICIONAL BASADO EN EL ESTADO GRANULAR IsAnySQLiteLoggingEnabled = False For Each dbStatus As Boolean In SQLiteLoggingStatusByDB.Values If dbStatus Then IsAnySQLiteLoggingEnabled = True Exit ' Si uno está activo, es suficiente para encender el Timer End If Next If IsAnySQLiteLoggingEnabled Then timerLogs.Enabled = True Log("Main.AppStart: Timer de limpieza de logs ACTIVADO (al menos una DB requiere logs).") Else timerLogs.Enabled = False Log("Main.AppStart: Timer de limpieza de logs DESHABILITADO (ninguna DB requiere logs).") End If ' <<<< Fin del bloque del Timer >>>> ' === 6. REGISTRO DE HANDLERS HTTP PARA EL SERVIDOR === srvr.AddHandler("/ping", "ping", False) srvr.AddHandler("/test", "TestHandler", False) srvr.AddHandler("/login", "LoginHandler", False) srvr.AddHandler("/dologin", "DoLoginHandler", False) srvr.AddHandler("/logout", "LogoutHandler", False) srvr.AddHandler("/changepass", "ChangePassHandler", False) srvr.AddHandler("/manager", "Manager", False) srvr.AddHandler("/DBJ", "DBHandlerJSON", False) srvr.AddHandler("/dbrquery", "DBHandlerJSON", False) srvr.AddHandler("/favicon.ico", "faviconHandler", False) srvr.AddHandler("/*", "DBHandlerB4X", False) ' 7. Inicia el servidor HTTP. srvr.Start Log("===========================================================") Log($"-=== jRDC está funcionando en el puerto: ${srvr.Port} (versión = $1.2{VERSION}) ===-"$) Log("===========================================================") ' 8. Inicia el bucle de mensajes de B4J. StartMessageLoop End Sub ' --- Subrutina para inicializar la base de datos de usuarios local (SQLite) --- Sub InitializeSQLiteDatabase Dim dbFileName As String = "users.db" If File.Exists(File.DirApp, dbFileName) = False Then Log("Creando nueva base de datos de usuarios: " & dbFileName) SQL1.InitializeSQLite(File.DirApp, dbFileName, True) ' Crear tabla 'users' Dim createUserTable As String = "CREATE TABLE users (username TEXT PRIMARY KEY, password_hash TEXT NOT NULL)" SQL1.ExecNonQuery(createUserTable) ' Crear tabla 'query_logs' Log("Creando tabla 'query_logs' con columnas de rendimiento.") Dim createQueryLogsTable As String = "CREATE TABLE query_logs (id INTEGER PRIMARY KEY AUTOINCREMENT, query_name TEXT, duration_ms INTEGER, timestamp INTEGER, db_key TEXT, client_ip TEXT, busy_connections INTEGER, handler_active_requests INTEGER)" SQL1.ExecNonQuery(createQueryLogsTable) ' Insertar usuario por defecto Dim defaultUser As String = "admin" Dim defaultPass As String = "12345" Dim hashedPass As String = bc.hashpw(defaultPass, bc.gensalt) SQL1.ExecNonQuery2("INSERT INTO users (username, password_hash) VALUES (?, ?)", Array As Object(defaultUser, hashedPass)) Log($"Usuario por defecto creado -> user: ${defaultUser}, pass: ${defaultPass}"$) ' Crear tabla 'errores' Log("Creando tabla 'errores' para registrar eventos.") Dim createErrorsTable As String = "CREATE TABLE errores (id INTEGER PRIMARY KEY AUTOINCREMENT, timestamp INTEGER, type TEXT, source TEXT, message TEXT, db_key TEXT, command_name TEXT, client_ip TEXT)" SQL1.ExecNonQuery(createErrorsTable) Else SQL1.InitializeSQLite(File.DirApp, dbFileName, True) Log("Base de datos de usuarios cargada.") ' >>> INICIO: Lógica de migración (ALTER TABLE) si la DB ya existía <<< Log("Verificando y migrando tabla 'query_logs' si es necesario.") If SQL1.ExecQuerySingleResult("SELECT name FROM sqlite_master WHERE type='table' AND name='query_logs'") = Null Then Log("Tabla 'query_logs' no encontrada, creándola con columnas de rendimiento.") 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 ' Si la tabla query_logs ya existe, entonces verificamos y añadimos las columnas faltantes (busy_connections, handler_active_requests). Dim columnExists As Boolean Dim rs As ResultSet ' --- VERIFICAR Y AÑADIR 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 ' La columna ya existe, salimos del bucle. End If Loop rs.Close If columnExists = False Then Log("Añadiendo columna 'busy_connections' a query_logs.") SQL1.ExecNonQuery("ALTER TABLE query_logs ADD COLUMN busy_connections INTEGER DEFAULT 0") End If ' --- VERIFICAR Y AÑADIR 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 ' La columna ya existe, salimos del bucle. End If Loop rs.Close If columnExists = False Then Log("Añadiendo columna 'handler_active_requests' a query_logs.") SQL1.ExecNonQuery("ALTER TABLE query_logs ADD COLUMN handler_active_requests INTEGER DEFAULT 0") End If ' >>> INICIO: Lógica de migración para 'errores' si la DB ya existía <<< Log("Verificando y migrando tabla 'errores' si es necesario.") If SQL1.ExecQuerySingleResult("SELECT name FROM sqlite_master WHERE type='table' AND name='errores'") = Null Then Log("Tabla 'errores' no encontrada, creándola.") Dim createErrorsTable As String = "CREATE TABLE errores (id INTEGER PRIMARY KEY AUTOINCREMENT, timestamp INTEGER, type TEXT, source TEXT, message TEXT, db_key TEXT, command_name TEXT, client_ip TEXT)" SQL1.ExecNonQuery(createErrorsTable) Else Log("Tabla 'errores' ya existe.") End If ' >>> FIN: Lógica de migración para 'errores' <<< End If ' >>> FIN: Lógica de migración (ALTER TABLE) <<< End If End Sub ' --- Subrutina para registrar las métricas de rendimiento de las queries en la tabla 'query_logs'. --- ' ¡MODIFICADA PARA USAR FILTRADO GRANULAR POR DBKEY! Public Sub LogQueryPerformance(QueryName As String, DurationMs As Long, DbKey As String, ClientIp As String, HandlerActiveRequests As Int, PoolBusyConnections As Int) ' Obtener el estado de logging para esta DBKey. Usar False si la DBKey no existe en el mapa. Dim isEnabled As Boolean = SQLiteLoggingStatusByDB.GetDefault(DbKey, False) If isEnabled Then Try SQL1.ExecNonQuery2("INSERT INTO query_logs (query_name, duration_ms, timestamp, db_key, client_ip, busy_connections, handler_active_requests) VALUES (?, ?, ?, ?, ?, ?, ?)", _ Array As Object(QueryName, DurationMs, DateTime.Now, DbKey, ClientIp, PoolBusyConnections, HandlerActiveRequests)) Catch Log("Error al guardar log de query en SQLite (Main.LogQueryPerformance): " & LastException.Message) End Try End If End Sub ' --- Subrutina para registrar errores y advertencias en la tabla 'errores'. --- ' ¡MODIFICADA PARA USAR FILTRADO GRANULAR POR DBKEY! Public Sub LogServerError(Type0 As String, Source As String, Message As String, DBKey As String, CommandName As String, ClientIp As String) ' Obtener el estado de logging para esta DBKey. Usar False si la DBKey es Null o no está en el mapa. Dim isEnabled As Boolean = SQLiteLoggingStatusByDB.GetDefault(DBKey, False) If isEnabled Then Try SQL1.ExecNonQuery2("INSERT INTO errores (timestamp, type, source, message, db_key, command_name, client_ip) VALUES (?, ?, ?, ?, ?, ?, ?)", _ Array As Object(DateTime.Now, Type0, Source, Message, DBKey, CommandName, ClientIp)) Catch Log("ERROR CRÍTICO: Fallo al guardar el log de error/advertencia en SQLite (Main.LogServerError): " & LastException.Message) End Try End If End Sub ' --- Subrutina de evento para el Timer 'timerLogs'. --- ' El estado 'Enabled' del Timer ya está controlado por IsAnySQLiteLoggingEnabled en AppStart y Manager. Sub TimerLogs_Tick Try borraArribaDe15000Logs Catch Dim ErrorMsg As String = "ERROR en TimerLogs_Tick al intentar borrar logs: " & LastException.Message Log(ErrorMsg) LogServerError("ERROR", "Main.TimerLogs_Tick", ErrorMsg, Null, "log_cleanup", Null) End Try End Sub ' --- Borra los registros más antiguos de la tabla 'query_logs' y hace VACUUM. --- ' ¡MODIFICADA PARA USAR FILTRADO GLOBAL! Sub borraArribaDe15000Logs 'ignore If IsAnySQLiteLoggingEnabled Then ' Solo ejecutar si al menos una DB requiere logs. Log("Recortando la tabla de 'query_logs', límite de 15,000 registros.") SQL1.ExecNonQuery("DELETE FROM query_logs WHERE timestamp NOT in (SELECT timestamp FROM query_logs ORDER BY timestamp desc LIMIT 15000 )") SQL1.ExecNonQuery("vacuum;") Else ' Si IsAnySQLiteLoggingEnabled es False, el Timer no debería estar activo. Log("AVISO: Tarea de limpieza de logs omitida. El logging global de SQLite está deshabilitado.") End If End Sub