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=ping Module12=RDCConnector Module13=TestHandler Module2=ChangePassHandler Module3=DBHandlerB4X Module4=DBHandlerJSON Module5=DoLoginHandler Module6=faviconHandler Module7=GlobalParameters Module8=LoginHandler Module9=LogoutHandler NumberOfFiles=10 NumberOfLibraries=9 NumberOfModules=13 Version=10.3 @EndOfDesignText@ 'Non-UI application (console / server application) #Region Project Attributes #CommandLineArgs: #MergeLibraries: True ' VERSION 5.09.15 '########################################################################################################### '###################### 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 ' entre el cliente B4X (DBRequestManager) y el servidor jRDC2. Type DBCommand (Name As String, Parameters() As Object) ' Define un comando SQL. Type DBResult (Tag As Object, Columns As Map, Rows As List) ' Define la estructura de un resultado de consulta. ' Contiene una lista de los identificadores de bases de datos configuradas (ej. "DB1", "DB2"). 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. ' Connectors: Almacena las instancias de RDCConnector por cada base de datos (DB1, DB2, etc.). ' commandsMap: Almacena los comandos SQL cargados de los archivos de configuración para cada DB. Public Connectors, commandsMap As Map ' 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 (para autenticación de Manager). Private bc As BCrypt ' Objeto de bloqueo (ReentrantLock) para proteger el mapa Main.Connectors durante operaciones de recarga (Hot-Swap). Public MainConnectorsLock As JavaObject ' Mapa para contar las peticiones activas por cada base de datos. Es thread-safe. Public ActiveRequestsCountByDB As Map ' Timer para ejecutar tareas periódicas, como la limpieza de logs. Public timerLogs As Timer 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. ' Esta base de datos se crea automáticamente si no existe o se migra si es necesario. InitializeSQLiteDatabase ' <<<< Bloque de inicialización del Timer para la limpieza de logs >>>> ' Inicializa y configura el Timer para borrar logs antiguos cada 10 minutos (600,000 milisegundos). timerLogs.Initialize("TimerLogs", 600000) ' 10 minutos = 600 * 1000 = 600000 ms timerLogs.Enabled = True ' Habilita el timer para que empiece a correr. Log("Main.AppStart: Timer de limpieza de 'query_logs' inicializado para ejecutarse cada 10 minutos.") ' <<<< Fin del bloque del Timer >>>> ' 2. Inicializa los mapas globales definidos en GlobalParameters.bas. ' Estos mapas se usan para monitorear el servidor y gestionar configuraciones dinámicas. GlobalParameters.mpLogs.Initialize ' Mapa para almacenar logs de actividad general. GlobalParameters.mpTotalRequests.Initialize ' Mapa para contar peticiones por endpoint/DB. GlobalParameters.mpTotalConnections.Initialize ' Mapa para almacenar el estado de los pools de conexión por DB. GlobalParameters.mpBlockConnection.Initialize ' Mapa para gestionar IPs bloqueadas (si la funcionalidad está activa). ' Aseguramos que el mapa de conteo de peticiones activas sea thread-safe para un manejo concurrente seguro. GlobalParameters.ActiveRequestsCountByDB = srvr.CreateThreadSafeMap ' 3. Inicializa las estructuras principales del servidor HTTP. listaDeCP.Initialize ' Inicializa la lista que contendrá los IDs de las bases de datos. srvr.Initialize("") ' Inicializa el objeto servidor HTTP. Connectors = srvr.CreateThreadSafeMap ' Crea un mapa seguro para almacenar instancias de RDCConnector (un conector por DB). commandsMap.Initialize ' Inicializa el mapa que almacenará los comandos SQL cargados de los archivos de configuración. ' Creamos una instancia de ReentrantLock para proteger Main.Connectors durante operaciones atómicas de Hot-Swap. MainConnectorsLock.InitializeNewInstance("java.util.concurrent.locks.ReentrantLock", Null) ' === 4. INICIALIZACIÓN DEL CONECTOR PARA LA BASE DE DATOS PRINCIPAL (DB1) === ' DB1 siempre usa el archivo 'config.properties' por defecto. Dim con1 As RDCConnector ' Declara una variable específica y única para el conector de DB1. con1.Initialize("DB1") ' Inicializa la instancia del conector para "DB1". Connectors.Put("DB1", con1) ' Asocia el identificador "DB1" con su instancia de RDCConnector. srvr.Port = con1.serverPort ' El puerto del servidor HTTP se obtiene del config.properties de DB1. listaDeCP.Add("DB1") ' Añade "DB1" a la lista de bases de datos gestionadas. Log($"Main.AppStart: Conector 'DB1' inicializado exitosamente en puerto: ${srvr.Port}"$) ' === 5. DETECCIÓN E INICIALIZACIÓN DE BASES DE DATOS ADICIONALES (DB2, DB3, DB4) === ' El servidor busca archivos de configuración adicionales (ej. config.DB2.properties) ' en el mismo directorio donde se ejecuta el JAR. cpFiles = File.ListFiles("./") If cpFiles.Size > 0 Then For i = 0 To cpFiles.Size - 1 ' Procesa 'config.DB2.properties' If cpFiles.Get(i) = "config.DB2.properties" Then Dim con2 As RDCConnector ' Declara una variable específica y única para el conector de DB2. con2.Initialize("DB2") ' Inicializa la instancia del conector para "DB2". Connectors.Put("DB2", con2) ' Asocia "DB2" con su instancia de RDCConnector. listaDeCP.Add("DB2") ' Añade "DB2" a la lista de bases de datos. Log("Main.AppStart: Conector 'DB2' inicializado exitosamente.") End If ' Procesa 'config.DB3.properties' If cpFiles.Get(i) = "config.DB3.properties" Then Dim con3 As RDCConnector ' Declara una variable específica y única para el conector de DB3. con3.Initialize("DB3") ' Inicializa la instancia del conector para "DB3". Connectors.Put("DB3", con3) ' Asocia "DB3" con su instancia de RDCConnector. listaDeCP.Add("DB3") ' Añade "DB3" a la lista de bases de datos. Log("Main.AppStart: Conector 'DB3' inicializado exitosamente.") End If ' Procesa 'config.DB4.properties' If cpFiles.Get(i) = "config.DB4.properties" Then Dim con4 As RDCConnector ' Declara una variable específica y única para el conector de DB4. con4.Initialize("DB4") ' Inicializa la instancia del conector para "DB4". Connectors.Put("DB4", con4) ' Asocia "DB4" con su instancia de RDCConnector. listaDeCP.Add("DB4") ' Añade "DB4" a la lista de bases de datos. Log("Main.AppStart: Conector 'DB4' inicializado exitosamente.") 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) ' Elimina la última ", " End If Log($"Main.AppStart: Bases de datos configuradas y listas: [${sbListaDeCP_Log.ToString}]"$) ' === 6. REGISTRO DE HANDLERS HTTP PARA EL SERVIDOR === ' Asocia rutas URL específicas con clases que manejarán las peticiones correspondientes. ' El último parámetro (True) indica que el handler se ejecutará en un nuevo hilo, ' lo que es recomendable para la mayoría de los casos para evitar bloqueos. srvr.AddHandler("/ping", "ping", False) ' Endpoint simple para verificar si el servidor está activo. srvr.AddHandler("/test", "TestHandler", False) ' Endpoint para pruebas de conexión y estado del servidor. srvr.AddHandler("/login", "LoginHandler", False) ' Muestra la página HTML de login. srvr.AddHandler("/dologin", "DoLoginHandler", False) ' Procesa el intento de inicio de sesión. srvr.AddHandler("/logout", "LogoutHandler", False) ' Cierra la sesión del usuario. srvr.AddHandler("/changepass", "ChangePassHandler", False) ' Permite a los usuarios cambiar su contraseña. srvr.AddHandler("/manager", "Manager", False) ' Panel de administración del servidor (requiere autenticación). srvr.AddHandler("/DBJ", "DBHandlerJSON", False) ' Handler para clientes web (ej. JavaScript, Node.js) que usan JSON. srvr.AddHandler("/dbrquery", "DBHandlerJSON", False) ' Un alias para el handler JSON, por si se usa en clientes específicos. srvr.AddHandler("/favicon.ico", "faviconHandler", False) ' Sirve el icono de la página (favicon). srvr.AddHandler("/*", "DBHandlerB4X", False) ' Handler por defecto para clientes B4X (DBRequestManager), ' procesa peticiones dinámicamente según la URL. ' 7. Inicia el servidor HTTP. 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. Es esencial para que la aplicación ' de servidor continúe ejecutándose y procesando eventos. StartMessageLoop End Sub ' --- Subrutina para inicializar la base de datos de usuarios local (SQLite) --- ' Esta base de datos se utiliza para almacenar credenciales de usuarios que pueden ' acceder al panel de administración del servidor jRDC y los logs de queries. Sub InitializeSQLiteDatabase Dim dbFileName As String = "users.db" ' Nombre del archivo de la base de datos SQLite. ' Verifica si el archivo de la base de datos ya existe en el directorio de la aplicación. If File.Exists(File.DirApp, dbFileName) = False Then Log("Creando nueva base de datos de usuarios: " & dbFileName) ' Inicializa la conexión a la base de datos SQLite, creándola si no existe (último parámetro en True). SQL1.InitializeSQLite(File.DirApp, dbFileName, True) ' Define y ejecuta la sentencia SQL para crear la tabla 'users' para la autenticación del Manager. Dim createUserTable As String = "CREATE TABLE users (username TEXT PRIMARY KEY, password_hash TEXT NOT NULL)" SQL1.ExecNonQuery(createUserTable) ' >>> INICIO: Creación de la tabla query_logs con las nuevas columnas desde CERO <<< Log("Creando tabla 'query_logs' con columnas de rendimiento.") ' Esta tabla almacena métricas detalladas de cada query ejecutada. Dim createQueryLogsTable As String = "CREATE TABLE query_logs (id INTEGER PRIMARY KEY AUTOINCREMENT, query_name TEXT, duration_ms INTEGER, timestamp INTEGER, db_key TEXT, client_ip TEXT, busy_connections INTEGER, handler_active_requests INTEGER)" SQL1.ExecNonQuery(createQueryLogsTable) ' >>> FIN: Creación de la tabla query_logs <<< ' Crea un usuario por defecto para facilitar el primer acceso al panel de administración. Dim defaultUser As String = "admin" Dim defaultPass As String = "12345" ' Genera un hash seguro de la contraseña usando BCrypt, lo cual es crucial para la seguridad. Dim hashedPass As String = bc.hashpw(defaultPass, bc.gensalt) ' Inserta el usuario por defecto en la tabla 'users'. SQL1.ExecNonQuery2("INSERT INTO users (username, password_hash) VALUES (?, ?)", Array As Object(defaultUser, hashedPass)) Log($"Usuario por defecto creado -> user: ${defaultUser}, pass: ${defaultPass}"$) Else ' Si el archivo de la base de datos ya existe, simplemente se abre. 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.") ' Primero, verificar si la tabla query_logs existe. If SQL1.ExecQuerySingleResult("SELECT name FROM sqlite_master WHERE type='table' AND name='query_logs'") = Null Then 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 ' Ejecutamos PRAGMA para obtener la información de la tabla y verificar la existencia de la columna. rs = SQL1.ExecQuery("PRAGMA table_info(query_logs)") 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 ' ¡Importante cerrar el ResultSet para liberar recursos! 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)") ' Ejecutamos PRAGMA nuevamente para esta columna. 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 ' ¡Importante cerrar el ResultSet! 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 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'. Public Sub LogQueryPerformance(QueryName As String, DurationMs As Long, DbKey As String, ClientIp As String, HandlerActiveRequests As Int, PoolBusyConnections As Int) Try ' Los valores de PoolBusyConnections y HandlerActiveRequests ya se reciben directamente del handler, ' eliminando la necesidad de obtenerlos del conector en este punto. ' Insertamos los datos en la tabla query_logs de SQLite. 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 Sub ' Subrutina de evento para el Timer 'timerLogs'. ' Se ejecuta periódicamente (cada 10 minutos) para limpiar la tabla de logs. Sub TimerLogs_Tick Try borraArribaDe15000Logs ' Llama a la función para limpiar los logs. Catch Log("ERROR en TimerLogs_Tick al intentar borrar logs: " & LastException.Message) End Try End Sub ' Borra los registros más antiguos de la tabla 'query_logs', manteniendo solo los 15,000 más recientes. ' Luego, optimiza el espacio de la base de datos SQLite con un 'vacuum'. Sub borraArribaDe15000Logs 'ignore Log("Recortando la tabla de 'query_logs', límite de 15,000 registros.") SQL1.ExecNonQuery("DELETE FROM query_logs WHERE timestamp NOT in (SELECT timestamp FROM query_logs ORDER BY timestamp desc LIMIT 15000 )") SQL1.ExecNonQuery("vacuum;") ' Optimiza el espacio de almacenamiento de la base de datos. End Sub