- VERSION 5.09.14

```
feat: Implement hot-swap for DB config reload and JSON POST support

**Cambios Principales:**

1. **Hot-Swap para recarga de configuraciones de DB sin reiniciar servidor**
2. **Migración a ReentrantLock para sincronización por incompatibilidad con Sync**
3. **Soporte para peticiones POST con Content-Type: application/json**
4. **Mejoras en inicialización del pool de conexiones y soporte multi-DB**

**Problemas Resueltos:**

- Falta de "Hot-Swap" en `reload`: El comando no permitía recarga dinámica de configuraciones sin reinicio
- Ausencia de mecanismo de cierre de pools en RDCConnector para liberación ordenada de conexiones
- Incompatibilidad con `Sync` en entorno B4X
- Procesamiento incorrecto de peticiones POST con Content-Type: application/json
- Inicialización incorrecta de pools C3P0 con TotalConnections: 0
- Configuración inconsistente de parámetros críticos de C3P0
- jdbcUrl truncada/vacía en logs por shadowing de variables

**Cambios Implementados:**

**Manager.bas:**
- Reemplazo completo de lógica para comando "reload"
- Creación de nuevos conectores antes de reemplazar los antiguos
- Sincronización con ReentrantLock para acceso thread-safe
- Patrón seguro de bloqueo sin `Finally` usando bandera booleana
- Cierre explícito de oldConnectors después del reemplazo
- Validación de inicialización y control de errores robusto
- Registro detallado en log HTML del proceso

**RDCConnector.bas:**
- Implementación de método `Public Sub Close()` para liberar pools C3P0
- Corrección de shadowing de variable `config` en LoadConfigMap
- Reordenamiento de Initialize
- Configuración completa de C3P0 antes de adquirir conexiones
- Forzar reportes de errores con acquireRetryAttempts y breakAfterAcquireFailure
- Activación forzada del pool con conexión temporal

**Main.bas:**
- Declaración de `MainConnectorsLock As JavaObject` (ReentrantLock)
- Inicialización del lock en AppStart
- Declaración separada de conectores (con1, con2, con3, con4)

**DBHandlerJSON.bas:**
- Detección de peticiones POST con Content-Type: application/json
- Lectura de JSON desde InputStream en lugar de parámetro URL
- Cierre explícito del InputStream para liberación de recursos
- Corrección de nombres de variables para evitar conflictos
- Mensajes de error mejorados para ambos métodos (legacy y nuevo)

**Beneficios:**
- Recarga en caliente de configuraciones DB sin interrupción de servicio
- Mayor disponibilidad y mantenibilidad del servidor
- Prevención de fugas de recursos con cierre ordenado de pools
- Compatibilidad con estándares APIs web (POST application/json)
- Inicialización robusta y confiable de pools de conexiones
- Mejor reporting de errores y diagnóstico de problemas
- Soporte multi-DB más estable y confiable
```
This commit is contained in:
2025-09-15 11:44:16 -06:00
parent 674eb2c81b
commit e04cdded47
11 changed files with 815 additions and 343 deletions

View File

@@ -30,40 +30,54 @@ Library6=jshell
Library7=json
Library8=jsql
Library9=bcrypt
Module1=ChangePassHandler
Module10=ping
Module11=RDCConnector
Module12=TestHandler
Module2=DBHandlerB4X
Module3=DBHandlerJSON
Module4=DoLoginHandler
Module5=faviconHandler
Module6=GlobalParameters
Module7=LoginHandler
Module8=LogoutHandler
Module9=Manager
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=12
NumberOfModules=13
Version=10.3
@EndOfDesignText@
'Non-UI application (console / server application)
#Region Project Attributes
#CommandLineArgs:
#MergeLibraries: True
' VERSION 5.09.08
'###########################################################################################################
'###################### 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
'###########################################################################################################
#Region Project Attributes
#CommandLineArgs:
#MergeLibraries: True
' VERSION 5.09.014
'###########################################################################################################
'###################### 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
'- VERSION 5.09.08
'- Se agregó que se puedan configurar en el config.properties los siguientes parametros:
'
' - setInitialPoolSize = 3
' - setMinPoolSize = 2
' - setMaxPoolSize = 5
'
'- Se agregaron en duro a RDConnector los siguientes parametros:
'
' - setMaxIdleTime <-- Tiempo máximo de inactividad de la conexión.
' - setMaxConnectionAge <-- Tiempo de vida máximo de una conexión.
' - setCheckoutTimeout <-- Tiempo máximo de espera por una conexión.
'change based on the jdbc jar file
'#AdditionalJar: mysql-connector-java-5.1.27-bin
'#AdditionalJar: postgresql-42.7.0
@@ -72,104 +86,164 @@ Version=10.3
#AdditionalJar: sqlite-jdbc-3.7.2
Sub Process_Globals
Public srvr As Server
Public const VERSION As Float = 2.23
Type DBCommand (Name As String, Parameters() As Object)
Type DBResult (Tag As Object, Columns As Map, Rows As List)
Dim listaDeCP As List
Dim cpFiles As List
Public Connectors, commandsMap As Map
Public SQL1 As SQL ' Objeto SQL para la base de datos de usuarios
Private bc As BCrypt
' --- Variables globales accesibles desde cualquier parte del proyecto ---
Public srvr As Server ' El objeto principal del servidor HTTP de B4J.
Public const VERSION As Float = 2.23 ' La versión actual de este servidor jRDC modificado.
' Tipos personalizados 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.
Public listaDeCP As List ' Contiene una lista de los identificadores de bases de datos configuradas (ej. "DB1", "DB2").
Private cpFiles As List ' Una lista temporal para almacenar los nombres de archivos encontrados en el directorio.
' Mapas globales para gestionar los conectores de base de datos y los comandos SQL.
Public Connectors, commandsMap As Map ' Connectors: Almacena las instancias de RDCConnector por DB.
' commandsMap: Almacena los comandos SQL cargados para cada DB.
Public SQL1 As SQL ' Objeto SQL para interactuar con la base de datos de usuarios (SQLite).
Private bc As BCrypt ' Objeto para realizar operaciones de hashing de contraseñas de forma segura (para autenticación).
Public MainConnectorsLock As JavaObject ' Objeto de bloqueo para proteger Main.Connectors
End Sub
Sub AppStart (Args() As String)
' --- INICIO DE CAMBIOS ---
' Inicializamos la base de datos. Se creará si no existe.
' --- Subrutina principal que se ejecuta al iniciar la aplicación ---
' 1. Inicializa la base de datos local de usuarios (SQLite).
' Esta base de datos se crea automáticamente si no existe y contiene los usuarios para el panel de administración.
InitializeSQLiteDatabase
' --- FIN DE CAMBIOS ---
listaDeCP.Initialize
srvr.Initialize("")
Dim con As RDCConnector
Connectors = srvr.CreateThreadSafeMap
commandsMap.Initialize
con.Initialize("DB1") 'Inicializamos el default de config.properties
Connectors.Put("DB1", con)
srvr.Port = con.serverPort
listaDeCP.Add("DB1")
' 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.
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).
' 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.
' <<<< NUEVA INICIALIZACIÓN: Creamos una instancia de ReentrantLock para proteger Main.Connectors >>>>
MainConnectorsLock.InitializeNewInstance("java.util.concurrent.locks.ReentrantLock", Null)
' <<<< HASTA AQUÍ LA NUEVA INICIALIZACIÓN >>>>
' === 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
Log(cpFiles)
For i = 0 To cpFiles.Size - 1
If cpFiles.Get(i) = "config.DB2.properties" Then ' Si existe el archivo DB2, lo usamos.
Dim con As RDCConnector
con.Initialize("DB2")
Connectors.Put("DB2", con)
listaDeCP.Add("DB2")
End If
If cpFiles.Get(i) = "config.DB3.properties" Then ' Si existe el archivo DB3, lo usamos.
Dim con As RDCConnector
con.Initialize("DB3")
Connectors.Put("DB3", con)
listaDeCP.Add("DB3")
End If
If cpFiles.Get(i) = "config.DB4.properties" Then ' Si existe el archivo DB4, lo usamos.
con.Initialize("DB4")
Connectors.Put("DB4", con)
listaDeCP.Add("DB4")
End If
Next
End If
srvr.AddHandler("/ping", "ping", True) ' Agrega un manejador a la ruta "/test", asignando las solicitudes a la clase TestHandler, el último parámetro indica si el manejador debe ejecutar en un nuevo hilo (False en este caso)
srvr.AddHandler("/test", "TestHandler", True) ' Agrega un manejador a la ruta "/test", asignando las solicitudes a la clase TestHandler, el último parámetro indica si el manejador debe ejecutar en un nuevo hilo (False en este caso)
' --- INICIO DE CAMBIOS ---
' 1. Rutas para el sistema de Login
srvr.AddHandler("/login", "LoginHandler", True) ' Sirve la página de login
srvr.AddHandler("/dologin", "DoLoginHandler", True) ' Procesa el intento de login
srvr.AddHandler("/logout", "LogoutHandler", True) ' Cierra la sesión
srvr.AddHandler("/changepass", "ChangePassHandler", True)
' 2. El handler del manager se queda igual, pero ahora estará protegido
srvr.AddHandler("/manager", "Manager", True)
' --- FIN DE CAMBIOS ---
srvr.AddHandler("/DBJ", "DBHandlerJSON", True)
srvr.AddHandler("/dbrquery", "DBHandlerJSON", True)
srvr.AddHandler("/favicon.ico", "faviconHandler", True)
' srvr.AddHandler("/*", "DB1Handler", False) ' Si no se especifica una base de datos, entonces asignamos la solicitud a la DB1.
srvr.AddHandler("/*", "DBHandlerB4X", True)
srvr.Start
Log("===========================================================")
Log($"-=== jRDC is running on port: ${srvr.port} (version = $1.2{VERSION}) ===-"$)
Log("===========================================================")
StartMessageLoop
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", True) ' Endpoint simple para verificar si el servidor está activo.
srvr.AddHandler("/test", "TestHandler", True) ' Endpoint para pruebas de conexión y estado del servidor.
srvr.AddHandler("/login", "LoginHandler", True) ' Muestra la página HTML de login.
srvr.AddHandler("/dologin", "DoLoginHandler", True) ' Procesa el intento de inicio de sesión.
srvr.AddHandler("/logout", "LogoutHandler", True) ' Cierra la sesión del usuario.
srvr.AddHandler("/changepass", "ChangePassHandler", True) ' Permite a los usuarios cambiar su contraseña.
srvr.AddHandler("/manager", "Manager", True) ' Panel de administración del servidor (requiere autenticación).
srvr.AddHandler("/DBJ", "DBHandlerJSON", True) ' Handler para clientes web (ej. JavaScript, Node.js) que usan JSON.
srvr.AddHandler("/dbrquery", "DBHandlerJSON", True) ' Un alias para el handler JSON, por si se usa en clientes específicos.
srvr.AddHandler("/favicon.ico", "faviconHandler", True) ' Sirve el icono de la página (favicon).
srvr.AddHandler("/*", "DBHandlerB4X", True) ' 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
' Nueva subrutina para crear y configurar la base de datos de usuarios
' --- 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.
Sub InitializeSQLiteDatabase
Dim dbFileName As String = "users.db"
' Si la base de datos no existe en la carpeta del .jar, la creamos
If File.Exists(File.DirApp, dbFileName) = False Then
Log("Creando nueva base de datos de usuarios: " & dbFileName)
' Inicializamos la conexión
SQL1.InitializeSQLite(File.DirApp, dbFileName, True)
' Creamos la tabla de usuarios
Dim createUserTable As String = "CREATE TABLE users (username TEXT PRIMARY KEY, password_hash TEXT NOT NULL)"
SQL1.ExecNonQuery(createUserTable)
' Creamos un usuario por defecto para el primer inicio
Dim defaultUser As String = "admin"
Dim defaultPass As String = "12345"
Dim hashedPass As String = bc.hashpw(defaultPass, bc.gensalt) ' bc.HashPassword(defaultPass)
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 ya existe, solo la abrimos
SQL1.InitializeSQLite(File.DirApp, dbFileName, True)
Log("Base de datos de usuarios cargada.")
End If
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'.
Dim createUserTable As String = "CREATE TABLE users (username TEXT PRIMARY KEY, password_hash TEXT NOT NULL)"
SQL1.ExecNonQuery(createUserTable)
' 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.")
End If
End Sub
' --- FIN DE CAMBIOS ---