- 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

141
Cambios.bas Normal file
View File

@@ -0,0 +1,141 @@
B4J=true
Group=Default Group
ModulesStructureVersion=1
Type=StaticCode
Version=10.3
@EndOfDesignText@
' ########################################
' ##### HISTORIAL DE CAMBIOS #####
' ########################################
Sub Process_Globals
'- VERSION X.XX.XX (cabios a implementar)
'- Agregar que se puedan usar cualquier cantidad de archivos config.properties
'- Agregar que se pueda recargar solo un archivo de configuracion o todos a la vez.
'- Agregar que el "Test" del manager revise (con el query de Jorge) cuantas conexiones hay actualmente activas,
' o si no en el test, un nuevo handler, talvez "Conexiones".
'- Agregar una forma de probar con carga el servidor
'- Agregar la opcion de "Queries lentos"
'- VERSION 5.09.13.3
'- Implementación de "Hot-Swap" para recarga de configuraciones de DB sin reiniciar el servidor.
'- Migración a ReentrantLock para sincronización debido a incompatibilidad con 'Sync'.
'- **Problemas Resueltos:**
'- 1. **Falta de "Hot-Swap" en `reload`:** El comando `reload` en `Manager.bas` no permitía la recarga dinámica de las configuraciones de la base de datos (config.properties) sin necesidad de reiniciar el servidor. La implementación anterior simplemente re-inicializaba las instancias existentes de `RDCConnector` in-situ, sin liberar los recursos de los pools de conexión anteriores, lo cual era ineficiente y propenso a errores.
'- 2. **Ausencia de un mecanismo de cierre de pools:** No existía un método `Close` en `RDCConnector.bas` que permitiera cerrar ordenadamente los `ConnectionPool` (C3P0) y liberar las conexiones a la base de datos, lo que era crítico para un "hot-swap" limpio .
'- 3. **Incompatibilidad con `Sync`:** La palabra clave `Sync` de B4X no era reconocida por el entorno de desarrollo del usuario, impidiendo su uso para la sincronización de hilos necesaria en el "hot-swap".
'- 4. **Ausencia de `Finally` en B4X:** La palabra clave `Finally` (común en otros lenguajes como Java para asegurar la liberación de recursos) no está disponible directamente en B4X, lo cual planteó un desafío para garantizar la liberación del `ReentrantLock` de forma segura.
'- **Cambios Implementados:**
'- **En `Main.bas`:**
'- * **Declaración de `MainConnectorsLock`:** Se añadió `Public MainConnectorsLock As JavaObject` en `Sub Process_Globals` para declarar una instancia de `java.util.concurrent.locks.ReentrantLock`, que servirá como objeto de bloqueo global para proteger el mapa `Main.Connectors`.
'- * **Inicialización de `MainConnectorsLock`:** Se inicializó `MainConnectorsLock.InitializeNewInstance("java.util.concurrent.locks.ReentrantLock", Null)` en `Sub AppStart`, asegurando que el objeto de bloqueo esté listo al inicio del servidor.
'- **En `RDCConnector.bas`:**
'- * **Método `Public Sub Close`:** Se añadió esta subrutina al final del módulo. Utiliza `JavaObject` para invocar `joPool.RunMethod("close", Null)` sobre la instancia subyacente de C3P0, permitiendo un cierre ordenado y la liberación de todas las conexiones del pool .
'- **En `Manager.bas`:**
'- * **Reemplazo completo de la lógica `If Command = "reload" Then`:**
'- * **Creación de `newConnectors`:** Se crea un mapa temporal (`Dim newConnectors As Map`) para inicializar las **nuevas instancias** de `RDCConnector` con la configuración fresca de los archivos `.properties` .
'- * **Preservación de `oldConnectors`:** Se almacena una referencia al mapa `Main.Connectors` actual en un nuevo mapa (`Dim oldConnectors As Map`) para tener acceso a los conectores antiguos que necesitan ser cerrados .
'- * **Sincronización con `ReentrantLock`:** Para proteger la manipulación del mapa `Main.Connectors` (que es compartido por múltiples hilos), se utilizan `Main.MainConnectorsLock.RunMethod("lock", Null)` y `Main.MainConnectorsLock.RunMethod("unlock", Null)`. Esto asegura que el reemplazo del mapa sea atómico, es decir, que solo un hilo pueda acceder a `Main.Connectors` durante la lectura y la escritura .
'- * **Manejo de Bloqueo Seguro sin `Finally`:** Dado que `Finally` no está disponible en B4X, se implementó un patrón con una bandera booleana (`lockAcquired`) dentro de un bloque `Try...Catch` para garantizar que `unlock()` siempre se ejecute si `lock()` fue exitoso, previniendo interbloqueos .
'- * **Cierre explícito de `oldConnectors`:** Después de que los `newConnectors` reemplazan a los `oldConnectors`, se itera sobre el mapa `oldConnectors` y se llama a `oldRDC.Close` para cada conector, liberando sus recursos de base de datos de manera limpia .
'- * **Validación de inicialización y control de errores:** Se agregó lógica para verificar el éxito de la inicialización de los nuevos conectores y abortar el "hot-swap" si ocurre un error crítico, manteniendo los conectores antiguos activos para evitar una interrupción del servicio .
'- * **Registro detallado:** Se mejoró la salida del log HTML del `Manager` para mostrar el proceso de recarga, las estadísticas de los pools recién inicializados y el cierre de los antiguos, incluyendo JSON detallado de las métricas de C3P0 .
'- • Beneficio: Estos cambios dotan al servidor jRDC2-Multi de una capacidad crítica para actualizar sus configuraciones de conexión a bases de datos en caliente, sin necesidad de reiniciar el servicio. Esto mejora la disponibilidad, simplifica el mantenimiento y previene fugas de recursos al asegurar el cierre ordenado de los pools de conexión antiguos.
'- VERSION 5.09.13.2
'- Módulo: DBHandlerJSON.bas
'- Descripción de Cambios: Manejo de Peticiones POST con Content-Type: application/json
'- • Problema Identificado: La implementación anterior de DBHandlerJSON procesaba las peticiones POST esperando que el payload JSON se encontrara en el parámetro j de la URL (req.GetParameter("j")). Esto impedía la correcta lectura de peticiones POST que utilizaban Content-Type: application/json, donde el JSON se envía directamente en el cuerpo de la petición (InputStream). Como resultado, los clientes recibían un error indicando la ausencia del parámetro j .
'- • Solución Implementada:
'- 1. Se modificó la lógica en el método Handle para detectar explícitamente las peticiones POST con Content-Type igual a application/json.
'- 2. En estos casos, el payload JSON ahora se lee directamente del InputStream de la petición (req.InputStream).
'- 3. Se utilizó Bit.InputStreamToBytes(Is0) para leer el cuerpo completo de la petición a un Array de bytes, seguido de BytesToString para convertirlo en la cadena JSON.
'- 4. Se añadió el cierre explícito del InputStream (Is0.Close) para asegurar la liberación de recursos .
'- 5. Se corrigió el nombre de la variable Is a Is0 para evitar un conflicto con la palabra reservada Is de B4X .
'- 6. Se actualizó el mensaje de error para aclarar que el JSON puede faltar tanto en el parámetro j como en el cuerpo de la petición.
'- • Beneficio: Esta corrección asegura que el DBHandlerJSON sea compatible con el "Método Recomendado" de POST con application/json, mejorando la robustez y la adherencia a los estándares de las APIs web, Sin comprometer la retrocompatibilidad con el "Método Legacy" (GET con parámetro j).
'- VERSION 5.09.13
' feat: Mejora la inicialización del pool de conexiones y el soporte multi-DB.
'
' - Este commit aborda y resuelve varios problemas críticos relacionados con la inicialización
' del pool de conexiones (C3P0) para múltiples bases de datos y la depuración de logs
' en el servidor jRDC2-Multi.
'
' **Problemas Resueltos:**
'
' 1. **Inicialización de `TotalConnections: 0` en todos los pools:** Anteriormente, el Log mostraba 0 conexiones inicializadas para todas las bases de datos (DB1, DB2, DB3, DB4) durante `AppStart`, a pesar de que los `handlers` de `DBHandlerB4X` y `DBHandlerJSON` podían conectarse más tarde bajo demanda. Esto indicaba un fallo silencioso en la creación de conexiones iniciales por parte de C3P0.
' 2. **Configuración inconsistente de C3P0:** Parámetros críticos de C3P0 como `acquireRetryAttempts` y `breakAfterAcquireFailure` no se aplicaban correctamente al inicio, manteniendo los valores por defecto que ocultaban errores de conexión.
' 3. **`jdbcUrl` truncado/vacío:** Se observó que la `jdbcUrl` aparecía truncada o vacía en algunos logs de C3P0, indicando un problema en la carga de la configuración.
'
' **Cambios Implementados:**
'
' **En `Main.bas`:**
'
' * **Declaración de conectores:** Se aseguró la declaración de variables `Dim conX As RDCConnector` separadas para cada conector (con1, con2, con3, con4) para evitar conflictos de variables y asegurar la inicialización correcta.
' **En `RDCConnector.bas`:**
'
' * **Corrección de *shadowing* de `config`:** Se modificó `LoadConfigMap(DB)` para asignar directamente a la variable de clase `config` (eliminando `Dim` local), resolviendo el problema de la `jdbcUrl` truncada y asegurando que cada `RDCConnector` use su configuración específica de manera persistente.
' * **Reordenamiento y robustecimiento de `Initialize`:**
' * **Carga de `config`:** Se asegura que `config` se cargue completamente en la variable de clase antes de cualquier operación del pool.
' * **Configuración de C3P0:** Todas las propiedades del pool (incluyendo `setInitialPoolSize`, `setMinPoolSize`, `setMaxPoolSize`, `setMaxIdleTime`, etc. ahora se aplican mediante `jo.RunMethod` *inmediatamente después* de `pool.Initialize` y *antes* de que el pool intente adquirir conexiones.
' * **Forzar reportes de errores:** Se añadieron las líneas `jo.RunMethod("setAcquireRetryAttempts", Array As Object(1))` y `jo.RunMethod("setBreakAfterAcquireFailure", Array As Object(True))`. Estas son cruciales para forzar a C3P0 a lanzar una `SQLException` explícita si falla al crear las conexiones iniciales, en lugar de fallar silenciosamente.
' * **Activación forzada del pool:** Se implementó `Dim tempCon As SQL = pool.GetConnection` seguido de `tempCon.Close` dentro de un bloque `Try...Catch`. Esto obliga al pool a establecer las conexiones iniciales (`InitialPoolSize`) con la configuración ya aplicada, permitiendo la captura de errores reales si la conexión falla.
'- 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.
'
'- Se agregó en el config.properties, al final del "JdbcUrl" este parametro, que le indica al servidor de Oracle
' el nombre del cliente que se está conectando "?v$session.program=jRDC_Multi"
'- VERSION 5.09.08
'- Se cambio el codigo para que en lugar de esperar un mapa con los parametros del query y nombres de los parametros (par1, par2, etc) para definir el ordenamiento, ahora se espera una lista [1,"2",3], y el orden de los parametros se toma directamente del orden en el que se mandan, de la misma forma que en B4A.
'- VERSION 5.09.04
'- Se cambio el nombre del handler de B4X a DBHandlerB4X.
'- Se quitaron los handlers que ya no servian.
'- VERSION 5.09.01
'- Se corrigieron errores en "Manager".
'- Se cambiaron nombres de handlers.
'- Se corrigio un error en la ruta de "www/login.html".
'- VERSION 5.08.31
'- Se corrigio que no avisaba cuando el query no requeria parametros y si se enviaban (en el JSONHandler)
'- VERSION 5.08.30
'- Se cambiaron los 4 handlers de B4A a uno solo que toma el DB de la ruta automáticamente.
'- Se agregaron validaciones del numero de parametros y si el query no los requiere o se dan de mas o de menos, manda un error especificando eso, ya no se reciben errores directos de la base de datos, esto fue tanto para B4A como para JSON.
'- Se modificó el Readme.md para incluir todos estos cambios.
'- VERSION 5.08.25
'- Se modificaron los archivos de reinicio de los servicios (servidor y Bow) y se cambio el menu del "manager" para que a seccion de "reload" incluya la liga a reinciar Bow.
'- VERSION 5.08.02
'- Se hizo un cambio para tratar de que las conexiones se "identifiquen" con Oracle y Jorge pueda saber que conexiones/recursos estamos ocupando
'- VERSION 4.11.14
'- Se agregó el parametro "setMaxPoolSize=5" para que solo genere 5 conexiones a la base de datos, antes generaba 15.
'- Se quitaron lineas previamente comentadas.
'- VERSION 4.11.09
'- Commit inicial on Nov 9, 2024
End Sub

View File

@@ -120,6 +120,15 @@ Sub Handle(req As ServletRequest, resp As ServletResponse)
If con <> Null And con.IsInitialized Then con.Close If con <> Null And con.IsInitialized Then con.Close
' Registra en el log el comando ejecutado, cuánto tiempo tardó y la IP del cliente. ' Registra en el log el comando ejecutado, cuánto tiempo tardó y la IP del cliente.
Log($"Command: ${q}, took: ${DateTime.Now - start}ms, client=${req.RemoteAddress}"$) Log($"Command: ${q}, took: ${DateTime.Now - start}ms, client=${req.RemoteAddress}"$)
' *** NUEVO: Insertar el log en la base de datos SQLite ***
Dim duration As Long = DateTime.Now - start
Try
Main.SQL1.ExecNonQuery2("INSERT INTO query_logs (query_name, duration_ms, timestamp, db_key, client_ip) VALUES (?, ?, ?, ?, ?)", _
Array As Object(q, duration, DateTime.Now, dbKey, req.RemoteAddress))
Catch
Log("Error al guardar log de query en SQLite (DBHandlerB4X): " & LastException.Message)
End Try
End Sub End Sub
' Ejecuta una consulta única usando el protocolo V2 (B4XSerializator). ' Ejecuta una consulta única usando el protocolo V2 (B4XSerializator).

View File

@@ -12,14 +12,13 @@ Sub Class_Globals
End Sub End Sub
' Subrutina de inicialización de la clase. Se llama cuando se crea un objeto de esta clase. ' Subrutina de inicialización de la clase. Se llama cuando se crea un objeto de esta clase.
' En este caso, no se necesita ninguna inicialización específica.
Public Sub Initialize Public Sub Initialize
End Sub End Sub
' Este es el método principal que maneja las peticiones HTTP entrantes (req) y prepara la respuesta (resp).
' Este es el método principal que maneja las peticiones HTTP entrantes (req) y prepara la respuesta (resp). ' Este es el método principal que maneja las peticiones HTTP entrantes (req) y prepara la respuesta (resp).
Sub Handle(req As ServletRequest, resp As ServletResponse) Sub Handle(req As ServletRequest, resp As ServletResponse)
Log("============== DB1JsonHandler ==============")
' --- Headers CORS (Cross-Origin Resource Sharing) --- ' --- Headers CORS (Cross-Origin Resource Sharing) ---
' Estos encabezados son necesarios para permitir que un cliente web (ej. una página con JavaScript) ' Estos encabezados son necesarios para permitir que un cliente web (ej. una página con JavaScript)
' que se encuentra en un dominio diferente pueda hacer peticiones a este servidor. ' que se encuentra en un dominio diferente pueda hacer peticiones a este servidor.
@@ -34,37 +33,54 @@ Sub Handle(req As ServletRequest, resp As ServletResponse)
' Establece "DB1" como el nombre de la base de datos por defecto. ' Establece "DB1" como el nombre de la base de datos por defecto.
Dim DB As String = "DB1" Dim DB As String = "DB1"
' Obtiene el objeto conector para la base de datos por defecto desde el objeto Main.
' Obtiene el objeto conector para la base de datos por defecto o especificada.
Connector = Main.Connectors.Get(DB) Connector = Main.Connectors.Get(DB)
' Declara una variable para la conexión SQL. ' Declara una variable para la conexión SQL.
Dim con As SQL Dim con As SQL
' Inicia un bloque Try...Catch para manejar posibles errores durante la ejecución. ' Inicia un bloque Try...Catch para manejar posibles errores durante la ejecución.
Try Try
' Obtiene el valor del parámetro 'j' de la petición. Se espera que contenga una cadena JSON. Dim jsonString As String
Dim jsonString As String = req.GetParameter("j")
' Verifica si el parámetro 'j' es nulo o está vacío. ' *** INICIO DE LA LÓGICA CORREGIDA PARA LEER JSON DEL CUERPO (POST) O DE PARÁMETROS (GET/POST Form-urlencoded) ***
If req.Method = "POST" And req.ContentType.Contains("application/json") Then
' Para peticiones POST con Content-Type: application/json, el JSON viene en el cuerpo (InputStream).
Dim Is0 As InputStream = req.InputStream ' ¡CORREGIDO: Usamos Is0 en lugar de Is para evitar conflicto con palabra reservada!
' Usamos Bit.InputStreamToBytes de la librería jcore para leer el stream completo a un array de bytes.
Dim bytes() As Byte = Bit.InputStreamToBytes(Is0)
jsonString = BytesToString(bytes, 0, bytes.Length, "UTF8") ' Convertimos los bytes a String UTF8.
Is0.Close ' ¡Es CRÍTICO cerrar el InputStream para liberar los recursos del sistema!
Else
' Para peticiones GET o POST con Content-Type como 'application/x-www-form-urlencoded',
' el JSON se espera en el parámetro 'j' de la URL.
jsonString = req.GetParameter("j")
End If
' *** FIN DE LA LÓGICA CORREGIDA PARA LEER JSON ***
' Verifica si la cadena JSON obtenida es nula o está vacía.
If jsonString = Null Or jsonString = "" Then If jsonString = Null Or jsonString = "" Then
' Si falta el parámetro, envía una respuesta de error 400 (Bad Request) y termina la ejecución. ' Si falta el JSON, envía una respuesta de error 400 (Bad Request) y termina la ejecución.
SendErrorResponse(resp, 400, "Falta el parametro 'j' en el URL") SendErrorResponse(resp, 400, "Falta el parámetro 'j' en el URL o el cuerpo JSON en la petición.")
Return Return
End If End If
' Crea un objeto JSONParser para analizar la cadena JSON. ' Crea un objeto JSONParser para analizar la cadena JSON.
Dim parser As JSONParser Dim parser As JSONParser
parser.Initialize(jsonString) parser.Initialize(jsonString)
' Convierte la cadena JSON en un objeto Map, que es como un diccionario (clave-valor). ' Convierte la cadena JSON en un objeto Map, que es como un diccionario (clave-valor).
Dim RootMap As Map = parser.NextObject Dim RootMap As Map = parser.NextObject
' Extrae los datos necesarios del JSON. ' Extrae los datos necesarios del JSON.
Dim execType As String = RootMap.GetDefault("exec", "") ' Tipo de ejecución: "executeQuery" o "executeCommand". Dim execType As String = RootMap.GetDefault("exec", "") ' Tipo de ejecución: "executeQuery" (SELECT) o "executeCommand" (INSERT/UPDATE/DELETE).
Dim queryName As String = RootMap.Get("query") ' Nombre del comando SQL (definido en config.properties). Dim queryName As String = RootMap.Get("query") ' Nombre del comando SQL (definido en config.properties).
' Se obtiene "params" como una Lista en lugar de un Mapa. ' Se obtiene "params" como una Lista en lugar de un Mapa, para soportar el ordenamiento de B4A.
Dim paramsList As List = RootMap.Get("params") Dim paramsList As List = RootMap.Get("params")
' Si la lista de parámetros es nula (no se proporcionó en el JSON), ' Si la lista de parámetros es nula (no se proporcionó en el JSON), la inicializamos como una lista vacía.
' la inicializamos como una lista vacía para evitar errores más adelante.
If paramsList = Null Or paramsList.IsInitialized = False Then If paramsList = Null Or paramsList.IsInitialized = False Then
paramsList.Initialize paramsList.Initialize
End If End If
@@ -74,101 +90,66 @@ Sub Handle(req As ServletRequest, resp As ServletResponse)
' Valida que el nombre de la base de datos (DB) exista en la lista de conexiones configuradas en Main. ' Valida que el nombre de la base de datos (DB) exista en la lista de conexiones configuradas en Main.
If Main.listaDeCP.IndexOf(DB) = -1 Then If Main.listaDeCP.IndexOf(DB) = -1 Then
SendErrorResponse(resp, 400, "Parametro 'DB' invalido. El nombre '" & DB & "' no es válido.") SendErrorResponse(resp, 400, "Parámetro 'DB' inválido. El nombre '" & DB & "' no es válido.")
' Se añade Return para detener la ejecución si la BD no es válida.
Return Return
End If End If
' Obtiene una conexión a la base de datos del pool de conexiones. ' Obtiene una conexión a la base de datos del pool de conexiones para la DB seleccionada.
con = Connector.GetConnection(DB) con = Connector.GetConnection(DB)
' Obtiene la cadena SQL del archivo de configuración usando el nombre de la consulta (queryName). ' Obtiene la cadena SQL del archivo de configuración usando el nombre de la consulta (queryName).
Dim sqlCommand As String = Connector.GetCommand(DB, queryName) Dim sqlCommand As String = Connector.GetCommand(DB, queryName)
' <<< INICIO VALIDACIÓN: VERIFICAR SI EL COMANDO EXISTE >>> ' <<< INICIO VALIDACIÓN: VERIFICAR SI EL COMANDO EXISTE >>>
' Comprueba si el comando SQL (query) especificado en el JSON fue encontrado en el archivo de configuración.
If sqlCommand = Null Or sqlCommand = "null" Or sqlCommand.Trim = "" Then If sqlCommand = Null Or sqlCommand = "null" Or sqlCommand.Trim = "" Then
' Si no se encontró el comando, crea un mensaje de error claro.
Dim errorMessage As String = $"El comando '${queryName}' no fue encontrado en el config.properties de '${DB}'."$ Dim errorMessage As String = $"El comando '${queryName}' no fue encontrado en el config.properties de '${DB}'."$
' Registra el error en el log del servidor para depuración.
Log(errorMessage) Log(errorMessage)
' Envía una respuesta de error 400 (Bad Request) al cliente en formato JSON.
SendErrorResponse(resp, 400, errorMessage) SendErrorResponse(resp, 400, errorMessage)
' Cierra la conexión a la base de datos antes de salir para evitar fugas de conexión.
If con <> Null And con.IsInitialized Then con.Close If con <> Null And con.IsInitialized Then con.Close
' Detiene la ejecución del método Handle para esta petición.
Return Return
End If End If
' <<< FIN VALIDACIÓN >>> ' <<< FIN VALIDACIÓN >>>
' Comprueba el tipo de ejecución solicitado ("executeQuery" o "executeCommand"). ' Comprueba el tipo de ejecución solicitado ("executeQuery" o "executeCommand").
If execType.ToLowerCase = "executequery" Then If execType.ToLowerCase = "executequery" Then
' Declara una variable para almacenar el resultado de la consulta.
Dim rs As ResultSet Dim rs As ResultSet
' Si el comando SQL contiene placeholders ('?'), significa que espera parámetros.
' Se usa 'paramsList' directamente en lugar de 'orderedParams'.
If sqlCommand.Contains("?") Or paramsList.Size > 0 Then If sqlCommand.Contains("?") Or paramsList.Size > 0 Then
' ================================================================= ' =================================================================
' === VALIDACIÓN DE CONTEO DE PARÁMETROS ========================== ' === VALIDACIÓN DE CONTEO DE PARÁMETROS ==========================
' ================================================================= ' =================================================================
' Calcula cuántos parámetros espera la consulta contando el número de '?'.
Dim expectedParams As Int = sqlCommand.Length - sqlCommand.Replace("?", "").Length Dim expectedParams As Int = sqlCommand.Length - sqlCommand.Replace("?", "").Length
' Obtiene cuántos parámetros se recibieron de la lista.
Dim receivedParams As Int = paramsList.Size Dim receivedParams As Int = paramsList.Size
Log($"expectedParams: ${expectedParams}, receivedParams: ${receivedParams}"$) Log($"expectedParams: ${expectedParams}, receivedParams: ${receivedParams}"$)
If expectedParams <> receivedParams Then If expectedParams <> receivedParams Then
' Si no coinciden, envía un error 400 detallado. SendErrorResponse(resp, 400, $"Número de parámetros equivocado para '${queryName}'. Se esperaban ${expectedParams} y se recibieron ${receivedParams}."$)
SendErrorResponse(resp, 400, $"Número de parametros equivocado para '${queryName}'. Se esperaban ${expectedParams} y se recibieron ${receivedParams}."$)
' Cierra la conexión antes de salir para evitar fugas.
If con <> Null And con.IsInitialized Then con.Close If con <> Null And con.IsInitialized Then con.Close
' Detiene la ejecución para evitar un error en la base de datos.
Return Return
End If End If
' ================================================================= ' =================================================================
' Ejecuta la consulta pasando el comando SQL y la lista de parámetros.
rs = con.ExecQuery2(sqlCommand, paramsList) rs = con.ExecQuery2(sqlCommand, paramsList)
Else Else
' Si no hay '?', ejecuta la consulta directamente sin parámetros.
rs = con.ExecQuery(sqlCommand) rs = con.ExecQuery(sqlCommand)
End If End If
' --- Procesamiento de resultados --- ' --- Procesamiento de resultados ---
' Prepara una lista para almacenar todas las filas del resultado.
Dim ResultList As List Dim ResultList As List
ResultList.Initialize ResultList.Initialize
' Usa un objeto JavaObject para acceder a los metadatos del resultado (info de columnas).
Dim jrs As JavaObject = rs Dim jrs As JavaObject = rs
Dim rsmd As JavaObject = jrs.RunMethod("getMetaData", Null) Dim rsmd As JavaObject = jrs.RunMethod("getMetaData", Null)
' Obtiene el número de columnas en el resultado.
Dim cols As Int = rsmd.RunMethod("getColumnCount", Null) Dim cols As Int = rsmd.RunMethod("getColumnCount", Null)
' Itera sobre cada fila del resultado (ResultSet).
Do While rs.NextRow Do While rs.NextRow
' Crea un mapa para almacenar los datos de la fila actual (columna -> valor).
Dim RowMap As Map Dim RowMap As Map
RowMap.Initialize RowMap.Initialize
' Itera sobre cada columna de la fila.
For i = 1 To cols For i = 1 To cols
' Obtiene el nombre de la columna.
Dim ColumnName As String = rsmd.RunMethod("getColumnName", Array(i)) Dim ColumnName As String = rsmd.RunMethod("getColumnName", Array(i))
' Obtiene el valor de la columna.
Dim value As Object = jrs.RunMethod("getObject", Array(i)) Dim value As Object = jrs.RunMethod("getObject", Array(i))
' Añade la pareja (nombre_columna, valor) al mapa de la fila.
RowMap.Put(ColumnName, value) RowMap.Put(ColumnName, value)
Next Next
' Añade el mapa de la fila a la lista de resultados.
ResultList.Add(RowMap) ResultList.Add(RowMap)
Loop Loop
' Cierra el ResultSet para liberar recursos de la base de datos.
rs.Close rs.Close
' Envía una respuesta de éxito con la lista de resultados en formato JSON.
SendSuccessResponse(resp, CreateMap("result": ResultList)) SendSuccessResponse(resp, CreateMap("result": ResultList))
Else If execType.ToLowerCase = "executecommand" Then Else If execType.ToLowerCase = "executecommand" Then
' Si es un comando (INSERT, UPDATE, DELETE), también valida los parámetros.
If sqlCommand.Contains("?") Then If sqlCommand.Contains("?") Then
' ================================================================= ' =================================================================
' === VALIDACIÓN DE CONTEO DE PARÁMETROS (para Comandos) ========== ' === VALIDACIÓN DE CONTEO DE PARÁMETROS (para Comandos) ==========
@@ -176,38 +157,23 @@ Sub Handle(req As ServletRequest, resp As ServletResponse)
Dim expectedParams As Int = sqlCommand.Length - sqlCommand.Replace("?", "").Length Dim expectedParams As Int = sqlCommand.Length - sqlCommand.Replace("?", "").Length
Dim receivedParams As Int = paramsList.Size Dim receivedParams As Int = paramsList.Size
If expectedParams <> receivedParams Then If expectedParams <> receivedParams Then
SendErrorResponse(resp, 400, $"Número de parametros equivocado para '${queryName}'. Se esperaban ${expectedParams} y se recibieron ${receivedParams}."$) SendErrorResponse(resp, 400, $"Número de parámetros equivocado para '${queryName}'. Se esperaban ${expectedParams} y se recibieron ${receivedParams}."$)
' Cierra la conexión antes de salir.
If con <> Null And con.IsInitialized Then con.Close If con <> Null And con.IsInitialized Then con.Close
' Detiene la ejecución.
Return Return
End If End If
' ================================================================= ' =================================================================
End If End If
' Ejecuta el comando que no devuelve resultados (NonQuery) con sus parámetros.
con.ExecNonQuery2(sqlCommand, paramsList) con.ExecNonQuery2(sqlCommand, paramsList)
' Envía una respuesta de éxito con un mensaje de confirmación.
SendSuccessResponse(resp, CreateMap("message": "Command executed successfully")) SendSuccessResponse(resp, CreateMap("message": "Command executed successfully"))
Else Else
' Si el valor de 'exec' no es ni "executeQuery" ni "executeCommand", envía un error. SendErrorResponse(resp, 400, "Parámetro 'exec' inválido. '" & execType & "' no es un valor permitido.")
SendErrorResponse(resp, 400, "Parametro 'exec' inválido. '" & execType & "' no es un valor permitido.")
End If End If
Catch Catch
' Si ocurre cualquier error inesperado en el bloque Try...
' Registra la excepción completa en el log del servidor para diagnóstico.
Log(LastException) Log(LastException)
' Envía una respuesta de error 500 (Internal Server Error) con el mensaje de la excepción.
SendErrorResponse(resp, 500, LastException.Message) SendErrorResponse(resp, 500, LastException.Message)
End Try End Try
' Este bloque se ejecuta siempre al final, haya habido error o no, *excepto si se usó Return antes*.
' Comprueba si el objeto de conexión fue inicializado y sigue abierto.
If con <> Null And con.IsInitialized Then If con <> Null And con.IsInitialized Then
' Cierra la conexión para devolverla al pool y que pueda ser reutilizada.
' Esto es fundamental para no agotar las conexiones a la base de datos.
con.Close con.Close
End If End If
End Sub End Sub

View File

@@ -11,7 +11,13 @@ DriverClass=oracle.jdbc.driver.OracleDriver
#GOHAN ---> server #GOHAN ---> server
#JdbcUrl=jdbc:oracle:thin:@//10.0.0.205:1521/DBKMT #JdbcUrl=jdbc:oracle:thin:@//10.0.0.205:1521/DBKMT
#JdbcUrl=jdbc:oracle:thin:@//10.0.0.236:1521/DBKMT #JdbcUrl=jdbc:oracle:thin:@//10.0.0.236:1521/DBKMT
JdbcUrl=jdbc:oracle:thin:@//192.168.101.13:1521/DBKMT JdbcUrl=jdbc:oracle:thin:@//192.168.101.13:1521/DBKMT?v$session.program=jRDC_Multi
# Configuración del pool de conexiones para DB2
InitialPoolSize=3
MinPoolSize=2
MaxPoolSize=10
AcquireIncrement=5
# SVR-KEYMON-PRODUCCION--> Usuario # SVR-KEYMON-PRODUCCION--> Usuario
User=SALMA User=SALMA
@@ -44,11 +50,13 @@ Debug=true
sql.traeConexion=select 'DB2' as conexion from dual sql.traeConexion=select 'DB2' as conexion from dual
sql.select_soporte=select * from GUNA.soporte sql.select_soporte=select * from GUNA.soporte
sql.select_conexion=SELECT 'OK' AS VALOR FROM DUAL sql.select_conexion=SELECT 'OK' AS VALOR FROM DUAL
sql.traeConexion4=SELECT (?) AS p1, (?) AS p2, (?) AS p3 FROM DUAL
sql.select_version=select cat_ve_version from cat_version sql.select_version=select cat_ve_version from cat_version
sql.select_version_GV2=select cat_ve_version from GUNA.cat_version sql.select_version_GV2=select cat_ve_version from GUNA.cat_version
sql.selectAlmacen=select * from cat_almacen where cat_al_id = ? sql.selectAlmacen=select * from cat_almacen where cat_al_id = ?
sql.sv=select * from cat_rutas where CAT_RU_RUTA = ? sql.sv=select * from cat_rutas where CAT_RU_RUTA = ?
sql.verify_device=select * from kelloggs.GUIDs where almacen = ? and ruta = ?
sql.registarMovil=insert into kelloggs.GUIDs (almacen, ruta, guid, estatus) values (?, ?, ?, 'ok')
sql.update_usuario_guna_nobajas=UPDATE GUNA.CAT_LOGINS SET CAT_LO_ESTATUS = 'Activo',CAT_LO_CONECTADO ='0' WHERE CAT_LO_ESTATUS != 'Baja' and CAT_LO_USUARIO = (?) sql.update_usuario_guna_nobajas=UPDATE GUNA.CAT_LOGINS SET CAT_LO_ESTATUS = 'Activo',CAT_LO_CONECTADO ='0' WHERE CAT_LO_ESTATUS != 'Baja' and CAT_LO_USUARIO = (?)
sql.proc_usuario=BEGIN EXECUTE IMMEDIATE ('DECLARE Cursor_SYS Sys_Refcursor; BEGIN SP_ACTIVAR_USUARIO( '''||(?)||''',Cursor_SYS); end;'); END; sql.proc_usuario=BEGIN EXECUTE IMMEDIATE ('DECLARE Cursor_SYS Sys_Refcursor; BEGIN SP_ACTIVAR_USUARIO( '''||(?)||''',Cursor_SYS); end;'); END;

View File

@@ -11,8 +11,13 @@ DriverClass=oracle.jdbc.driver.OracleDriver
#GOHAN ---> server #GOHAN ---> server
#JdbcUrl=jdbc:oracle:thin:@//10.0.0.205:1521/DBKMT #JdbcUrl=jdbc:oracle:thin:@//10.0.0.205:1521/DBKMT
#JdbcUrl=jdbc:oracle:thin:@//10.0.0.236:1521/DBKMT #JdbcUrl=jdbc:oracle:thin:@//10.0.0.236:1521/DBKMT
JdbcUrl=jdbc:oracle:thin:@//192.168.101.12:1521/DBKMT JdbcUrl=jdbc:oracle:thin:@//192.168.101.12:1521/DBKMT?v$session.program=jRDC_Multi
# Configuración del pool de conexiones para DB2
InitialPoolSize=3
MinPoolSize=2
MaxPoolSize=10
AcquireIncrement=5
# SVR-KEYMON-PRODUCCION--> Usuario # SVR-KEYMON-PRODUCCION--> Usuario
#User=GUNA #User=GUNA

View File

@@ -11,7 +11,13 @@ DriverClass=oracle.jdbc.driver.OracleDriver
#GOHAN ---> server #GOHAN ---> server
#JdbcUrl=jdbc:oracle:thin:@//10.0.0.205:1521/DBKMT #JdbcUrl=jdbc:oracle:thin:@//10.0.0.205:1521/DBKMT
#JdbcUrl=jdbc:oracle:thin:@//10.0.0.236:1521/DBKMT #JdbcUrl=jdbc:oracle:thin:@//10.0.0.236:1521/DBKMT
JdbcUrl=jdbc:oracle:thin:@//192.168.101.13:1521/DBKMT JdbcUrl=jdbc:oracle:thin:@//192.168.101.13:1521/DBKMT?v$session.program=jRDC_Multi
# Configuración del pool de conexiones para DB2
InitialPoolSize=3
MinPoolSize=2
MaxPoolSize=10
AcquireIncrement=5
# SVR-KEYMON-PRODUCCION--> Usuario # SVR-KEYMON-PRODUCCION--> Usuario
User=SALMA User=SALMA

View File

@@ -11,8 +11,13 @@ DriverClass=oracle.jdbc.driver.OracleDriver
#GOHAN ---> server #GOHAN ---> server
#JdbcUrl=jdbc:oracle:thin:@//10.0.0.205:1521/DBKMT #JdbcUrl=jdbc:oracle:thin:@//10.0.0.205:1521/DBKMT
#JdbcUrl=jdbc:oracle:thin:@//10.0.0.236:1521/DBKMT #JdbcUrl=jdbc:oracle:thin:@//10.0.0.236:1521/DBKMT
JdbcUrl=jdbc:oracle:thin:@//192.168.101.10:1521/DBKMT?oracle.jdbc.defaultClientIdentifier=jRDC_Multi JdbcUrl=jdbc:oracle:thin:@//192.168.101.10:1521/DBKMT?v$session.program=jRDC_Multi
# Configuración del pool de conexiones para DB1
InitialPoolSize=3
MinPoolSize=2
MaxPoolSize=10
AcquireIncrement=5
# SVR-KEYMON-PRODUCCION--> Usuario # SVR-KEYMON-PRODUCCION--> Usuario
User=GUNA User=GUNA
@@ -46,6 +51,8 @@ sql.select_revisaClienteCredito_GUNA2=select (select count(CAT_CL_CODIGO) from G
sql.traeConexion=select 'DB1' as conexion from dual sql.traeConexion=select 'DB1' as conexion from dual
sql.traeConexion2=select 'DB1' as conexion from dual sql.traeConexion2=select 'DB1' as conexion from dual
sql.traeConexion3=select '1' as par1, 2 as par2 from dual
sql.traeConexion4=SELECT (?) AS p1, (?) AS p2, (?) AS p3 FROM DUAL
sql.select_soporte=select * from GUNA.soporte sql.select_soporte=select * from GUNA.soporte
sql.select_conexion=SELECT 'OK' AS VALOR FROM DUAL sql.select_conexion=SELECT 'OK' AS VALOR FROM DUAL
sql.selectAlmacen=select cat_al_id, cat_al_desc, cat_al_archftp from cat_almacen where cat_al_id = ? sql.selectAlmacen=select cat_al_id, cat_al_desc, cat_al_archftp from cat_almacen where cat_al_id = ?

View File

@@ -28,21 +28,21 @@ Sub Handle(req As ServletRequest, resp As ServletResponse)
' --- MANEJO ESPECIAL PARA SNAPSHOT --- ' --- MANEJO ESPECIAL PARA SNAPSHOT ---
' El comando "snapshot" no devuelve HTML, sino una imagen. Lo manejamos por separado al principio. ' El comando "snapshot" no devuelve HTML, sino una imagen. Lo manejamos por separado al principio.
If Command = "snapshot" Then If Command = "snapshot" Then
Try ' Try
resp.ContentType = "image/png" ' resp.ContentType = "image/png"
Dim robot, toolkit As JavaObject ' Dim robot, toolkit As JavaObject
robot.InitializeNewInstance("java.awt.Robot", Null) ' robot.InitializeNewInstance("java.awt.Robot", Null)
toolkit.InitializeStatic("java.awt.Toolkit") ' toolkit.InitializeStatic("java.awt.Toolkit")
Dim screenRect As JavaObject ' Dim screenRect As JavaObject
screenRect.InitializeNewInstance("java.awt.Rectangle", Array As Object( _ ' screenRect.InitializeNewInstance("java.awt.Rectangle", Array As Object( _
toolkit.RunMethodJO("getDefaultToolkit", Null).RunMethod("getScreenSize", Null))) ' toolkit.RunMethodJO("getDefaultToolkit", Null).RunMethod("getScreenSize", Null)))
Dim image As JavaObject = robot.RunMethod("createScreenCapture", Array As Object(screenRect)) ' Dim image As JavaObject = robot.RunMethod("createScreenCapture", Array As Object(screenRect))
Dim ImageIO As JavaObject ' Dim ImageIO As JavaObject
ImageIO.InitializeStatic("javax.imageio.ImageIO").RunMethod("write", Array As Object(image, "png", resp.OutputStream)) ' ImageIO.InitializeStatic("javax.imageio.ImageIO").RunMethod("write", Array As Object(image, "png", resp.OutputStream))
Catch ' Catch
resp.SendError(500, LastException.Message) ' resp.SendError(500, LastException.Message)
End Try ' End Try
Return ' Detenemos la ejecución aquí para no enviar más HTML. ' Return ' Detenemos la ejecución aquí para no enviar más HTML.
End If End If
' --- FIN DE MANEJO ESPECIAL --- ' --- FIN DE MANEJO ESPECIAL ---
@@ -67,7 +67,8 @@ Sub Handle(req As ServletRequest, resp As ServletResponse)
' --- Cabecera, Botón y Formulario Oculto (igual que antes) --- ' --- Cabecera, Botón y Formulario Oculto (igual que antes) ---
sb.Append("<h1>Panel de Administración jRDC</h1>") sb.Append("<h1>Panel de Administración jRDC</h1>")
sb.Append($"Bienvenido, <b>${req.GetSession.GetAttribute("username")}</b><br>"$) sb.Append($"Bienvenido, <b>${req.GetSession.GetAttribute("username")}</b><br>"$)
sb.Append("<p class='nav'><a href='/manager?command=test'>Test</a> | <a href='/manager?command=ping'>Ping</a> | <a href='/manager?command=reload'>Reload</a> | <a href='/manager?command=rpm2'>Reiniciar (pm2)</a> | <a href='/manager?command=reviveBow'>Revive Bow</a></p><hr>") ' sb.Append("<p class='nav'><a href='/manager?command=test'>Test</a> | <a href='/manager?command=ping'>Ping</a> | <a href='/manager?command=reload'>Reload</a> | <a href='/manager?command=rpm2'>Reiniciar (pm2)</a> | <a href='/manager?command=reviveBow'>Revive Bow</a></p><hr>")
sb.Append("<p class='nav'><a href='/manager?command=test'>Test</a> | <a href='/manager?command=ping'>Ping</a> | <a href='/manager?command=reload'>Reload</a> | <a href='/manager?command=slowqueries'>Queries Lentos</a> | <a href='/manager?command=totalcon'>Estadísticas Pool</a> | <a href='/manager?command=rpm2'>Reiniciar (pm2)</a> | <a href='/manager?command=reviveBow'>Revive Bow</a></p><hr>")
' sb.Append("<button onclick='toggleForm()'>Cambiar Contraseña</button>") ' sb.Append("<button onclick='toggleForm()'>Cambiar Contraseña</button>")
sb.Append("<div id='changePassForm' style='display:none;'>") sb.Append("<div id='changePassForm' style='display:none;'>")
sb.Append("<h2>Cambiar Contraseña</h2><form action='/changepass' method='post'>") sb.Append("<h2>Cambiar Contraseña</h2><form action='/changepass' method='post'>")
@@ -84,16 +85,113 @@ Sub Handle(req As ServletRequest, resp As ServletResponse)
' ### INICIO DE TU LÓGICA DE COMANDOS INTEGRADA ### ' ### INICIO DE TU LÓGICA DE COMANDOS INTEGRADA ###
' ========================================================================= ' =========================================================================
If Command = "reload" Then If Command = "reload" Then
Private estaDB As String = "" ' Usamos un StringBuilder temporal para acumular los logs de la recarga antes de añadirlos al StringBuilder principal.
For i = 0 To Main.listaDeCP.Size - 1 Dim sbTemp As StringBuilder
Main.Connectors.Get(Main.listaDeCP.get(i)).As(RDCConnector).Initialize(Main.listaDeCP.get(i)) sbTemp.Initialize
If Main.listaDeCP.get(i) <> "DB1" Then estaDB = "." & Main.listaDeCP.get(i) Else estaDB = "" sbTemp.Append($"Iniciando recarga de configuración (Hot-Swap) ($DateTime{DateTime.Now})"$).Append("<br>" & CRLF)
sb.Append($"Recargando config${estaDB}.properties ($DateTime{DateTime.Now})<br/>"$)
sb.Append($"Queries en config.properties: <b>${Main.Connectors.Get(Main.listaDeCP.get(i)).As(RDCConnector).commands.Size}</b><br/>"$) ' 1. Crear un nuevo mapa temporal para almacenar los conectores recién inicializados.
sb.Append($"<b>JdbcUrl:</b> ${Main.Connectors.Get(Main.listaDeCP.get(i)).As(RDCConnector).config.Get("JdbcUrl")}</b><br/>"$) Dim newConnectors As Map
sb.Append($"<b>User:</b> ${Main.Connectors.Get(Main.listaDeCP.get(i)).As(RDCConnector).config.Get("User")}</b><br/>"$) newConnectors.Initialize
sb.Append($"<b>ServerPort:</b> ${Main.srvr.Port}</b><br/><br/>"$)
' Guardamos una referencia al mapa de conectores actualmente activos.
Dim oldConnectors As Map
Dim reloadSuccessful As Boolean = True
' *** INICIO DEL BLOQUE CRÍTICO 1: Obtener oldConnectors con ReentrantLock ***
Dim lock1Acquired As Boolean = False ' Bandera para saber si el bloqueo fue adquirido.
Try
Main.MainConnectorsLock.RunMethod("lock", Null) ' Adquirimos el bloqueo.
lock1Acquired = True ' Marcamos que el bloqueo fue adquirido.
oldConnectors = Main.Connectors ' Obtenemos la referencia al mapa actual de conectores.
Catch
sbTemp.Append($" -> ERROR CRÍTICO: No se pudo adquirir el bloqueo para leer conectores antiguos: ${LastException.Message}"$).Append("<br>" & CRLF)
reloadSuccessful = False ' Si falla aquí, la recarga no puede continuar.
End Try
If lock1Acquired Then
Main.MainConnectorsLock.RunMethod("unlock", Null) ' Liberamos el bloqueo si fue adquirido.
End If
' *** FIN DEL BLOQUE CRÍTICO 1 ***
If Not(reloadSuccessful) Then ' Si el primer bloqueo falló o la asignación, salimos temprano.
sb.Append(sbTemp.ToString) ' Añadimos los logs acumulados al StringBuilder principal.
sb.Append($"¡ERROR: La recarga de configuración falló en la fase de bloqueo inicial! Los conectores antiguos siguen activos."$).Append("<br>" & CRLF)
Return ' Salir del Handle ya que ocurrió un error crítico irrecuperable.
End If
' 2. Iterar sobre las bases de datos configuradas y crear *nuevas* instancias de RDCConnector.
For Each dbKey As String In Main.listaDeCP
Try
Dim newRDC As RDCConnector
newRDC.Initialize(dbKey) ' Inicializa la nueva instancia con la configuración fresca.
newConnectors.Put(dbKey, newRDC)
Dim newPoolStats As Map = newRDC.GetPoolStats
sbTemp.Append($" -> ${dbKey}: Nuevo conector inicializado. Conexiones: ${newPoolStats.Get("TotalConnections")}"$).Append("<br>" & CRLF)
Catch
sbTemp.Append($" -> ERROR CRÍTICO al inicializar nuevo conector para ${dbKey}: ${LastException.Message}"$).Append("<br>" & CRLF)
reloadSuccessful = False
Exit ' Si uno falla, abortamos la recarga completa para evitar un estado inconsistente.
End Try
Next Next
sb.Append(sbTemp.ToString) ' Añadimos los logs acumulados de la inicialización al StringBuilder principal.
If reloadSuccessful Then
' 3. Si todos los nuevos conectores se inicializaron con éxito,
' realizamos el "cambio de cartel" (hot-swap) de forma atómica.
' *** INICIO DEL BLOQUE CRÍTICO 2: Reemplazar Main.Connectors con ReentrantLock ***
Dim lock2Acquired As Boolean = False ' Bandera para saber si el bloqueo fue adquirido.
Try
Main.MainConnectorsLock.RunMethod("lock", Null) ' Adquirimos el bloqueo.
lock2Acquired = True ' Marcamos que el bloqueo fue adquirido.
Main.Connectors = newConnectors ' Reemplazamos el mapa de conectores completo por el nuevo.
Catch
sb.Append($" -> ERROR CRÍTICO: No se pudo adquirir el bloqueo para reemplazar conectores: ${LastException.Message}"$).Append("<br>" & CRLF)
reloadSuccessful = False ' Si falla aquí, la recarga no se completó con éxito.
End Try
If lock2Acquired Then
Main.MainConnectorsLock.RunMethod("unlock", Null) ' Liberamos el bloqueo si fue adquirido.
End If
' *** FIN DEL BLOQUE CRÍTICO 2 ***
If reloadSuccessful Then ' Si el segundo bloqueo y swap fue exitoso
sb.Append($"¡Recarga de configuración completada con éxito (Hot-Swap)!"$).Append("<br>" & CRLF)
sb.Append($"Nuevos conectores activos. Verificando estado final..."$).Append("<br>" & CRLF)
' Mostrar el estado de los *nuevos* conectores después del swap.
If Main.Connectors.IsInitialized Then
Dim liveStats As Map
liveStats.Initialize
For Each dbKey As String In Main.Connectors.Keys
Dim currentConnector As RDCConnector = Main.Connectors.Get(dbKey).As(RDCConnector)
liveStats.Put(dbKey, currentConnector.GetPoolStats) ' Obtiene las estadísticas en tiempo real.
Next
j.Initialize(liveStats)
sb.Append($"Estado actual de los pools: ${j.ToString}"$).Append(CRLF) ' No <br> para JSON puro
End If
' 4. Cerrar explícitamente los pools de conexión de las instancias antiguas.
If oldConnectors.IsInitialized Then
sb.Append("Cerrando conectores antiguos...").Append("<br>" & CRLF)
For Each dbKey As String In oldConnectors.Keys
Dim oldRDC As RDCConnector = oldConnectors.Get(dbKey).As(RDCConnector)
If oldRDC <> Null And oldRDC.IsInitialized Then
oldRDC.Close ' Llama al método Close que hemos añadido al RDCConnector.
sb.Append($" -> Pool antiguo de ${dbKey} cerrado."$).Append("<br>" & CRLF)
End If
Next
End If
Else
sb.Append($"¡ERROR: La recarga de configuración falló en la fase de reemplazo de conectores! Los conectores antiguos siguen activos."$).Append("<br>" & CRLF)
End If
Else
sb.Append($"¡ERROR: La recarga de configuración falló durante la inicialización de nuevos conectores! Los conectores antiguos siguen activos."$).Append("<br>" & CRLF)
' Si la recarga falló, los conectores antiguos (oldConnectors) se mantienen activos
' y siguen sirviendo para evitar un paro del servicio.
End If
Else If Command = "test" Then Else If Command = "test" Then
Try Try
Dim con As SQL = Main.Connectors.Get("DB1").As(RDCConnector).GetConnection("") Dim con As SQL = Main.Connectors.Get("DB1").As(RDCConnector).GetConnection("")
@@ -174,13 +272,49 @@ Sub Handle(req As ServletRequest, resp As ServletResponse)
' j.Initialize(Global.mpBlockConnection) ' j.Initialize(Global.mpBlockConnection)
sb.Append(j.ToString) sb.Append(j.ToString)
End If End If
Else If Command = "totalcon" Then
If GlobalParameters.mpTotalConnections.IsInitialized Then
j.Initialize(GlobalParameters.mpTotalConnections)
sb.Append(j.ToString)
End If
Else If Command = "ping" Then Else If Command = "ping" Then
sb.Append($"Pong ($DateTime{DateTime.Now})"$) sb.Append($"Pong ($DateTime{DateTime.Now})"$)
Else If Command = "totalcon" Then ' <<< Modificado: Ahora usa GetPoolStats para cada pool
' Verificamos que el mapa global de conexiones esté inicializado.
' Aunque no lo poblamos directamente, es un buen chequeo de estado.
If GlobalParameters.mpTotalConnections.IsInitialized Then
sb.Append("<h2>Estadísticas del Pool de Conexiones por DB:</h2>")
' Creamos un mapa LOCAL para almacenar las estadísticas de TODOS los pools de conexiones.
Dim allPoolStats As Map
allPoolStats.Initialize
' Iteramos sobre cada clave de base de datos que tenemos configurada (DB1, DB2, etc.).
For Each dbKey As String In Main.listaDeCP
' Obtenemos el conector RDC para la base de datos actual.
Dim connector As RDCConnector = Main.Connectors.Get(dbKey).As(RDCConnector)
' Si el conector no está inicializado (lo cual no debería ocurrir si Main.AppStart funcionó),
' registramos un error y pasamos al siguiente.
If connector.IsInitialized = False Then
Log($"Manager: ADVERTENCIA: El conector para ${dbKey} no está inicializado."$)
Dim errorMap As Map = CreateMap("Error": "Conector no inicializado o no cargado correctamente")
allPoolStats.Put(dbKey, errorMap)
Continue ' Salta a la siguiente iteración del bucle.
End If
' Llamamos al método GetPoolStats del conector para obtener las métricas de su pool.
Dim poolStats As Map = connector.GetPoolStats
' Añadimos las estadísticas de este pool (poolStats) al mapa general (allPoolStats),
' usando la clave de la base de datos (dbKey) como su identificador.
allPoolStats.Put(dbKey, poolStats)
Next
' Inicializamos el generador JSON con el mapa 'allPoolStats' (que ahora sí debería contener datos).
' (La variable 'j' ya está declarada en Class_Globals de Manager.bas, no la declares de nuevo aquí).
j.Initialize(allPoolStats)
' Añadimos la representación JSON de las estadísticas al StringBuilder para la respuesta HTML.
sb.Append(j.ToString)
Else
sb.Append("El mapa de conexiones GlobalParameters.mpTotalConnections no está inicializado.")
End If
End If End If
' ========================================================================= ' =========================================================================
' ### FIN DE TU LÓGICA DE COMANDOS ### ' ### FIN DE TU LÓGICA DE COMANDOS ###

View File

@@ -4,53 +4,77 @@ ModulesStructureVersion=1
Type=Class Type=Class
Version=4.19 Version=4.19
@EndOfDesignText@ @EndOfDesignText@
'Class module ' Módulo de clase: RDCConnector
' Esta clase gestiona el pool de conexiones a una base de datos específica.
' Cada instancia de RDCConnector maneja la conexión y los comandos para una base de datos.
Sub Class_Globals Sub Class_Globals
Private pool As ConnectionPool Private pool As ConnectionPool ' Objeto principal para gestionar el pool de conexiones de la base de datos (usa C3P0 internamente).
Private DebugQueries As Boolean Private DebugQueries As Boolean ' Bandera para activar/desactivar el modo de depuración de queries.
Dim commands As Map Dim commands As Map ' Almacena los comandos SQL específicos de esta base de datos, cargados de su archivo de configuración.
Public serverPort As Int Public serverPort As Int ' El puerto que el servidor HTTP usará, obtenido del archivo de configuración principal (config.properties).
Public usePool As Boolean = True Public usePool As Boolean = True ' Indica si se debe usar el pool de conexiones (siempre True en este diseño).
Dim config As Map Dim config As Map ' Almacena la configuración completa (JdbcUrl, User, Password, etc.) cargada de su respectivo archivo .properties.
End Sub End Sub
'Initializes the object. You can add parameters to this method if needed. ' Subrutina de inicialización para el conector de una base de datos específica.
' Se llama una vez por cada base de datos (DB1, DB2, DB3, DB4) al iniciar el servidor.
' DB: El identificador único de la base de datos (ej. "DB1", "DB2").
Public Sub Initialize(DB As String) Public Sub Initialize(DB As String)
' Log("RDCConnector Initialize") ' Si el identificador es "DB1", se usa una cadena vacía para que File.ReadMap cargue "config.properties" (el archivo por defecto).
If DB.EqualsIgnoreCase("DB1") Then DB = "" 'Esto para el config.properties por default If DB.EqualsIgnoreCase("DB1") Then DB = ""
Dim config As Map = LoadConfigMap(DB)
Log($"Inicializamos ${DB}, usuario: ${config.Get("User")}"$)
pool.Initialize(config.Get("DriverClass"), config.Get("JdbcUrl"), config.Get("User"), config.Get("Password"))
Dim jo As JavaObject = pool
' Leer valores del config.properties o usar valores por defecto ' PASO 1: Cargar la configuración desde el archivo .properties correspondiente.
' Si el parámetro no se encuentra en el archivo .properties, se usará el segundo valor (el por defecto). ' Es CRUCIAL que se asigne a la variable de CLASE 'config' (sin 'Dim' local)
Dim initialPoolSize As Int = config.GetDefault("InitialPoolSize", 3) ' Por defecto 3 ' para que la configuración cargada del archivo sea persistente para esta instancia del conector.
Dim minPoolSize As Int = config.GetDefault("MinPoolSize", 2) ' Por defecto 3 config = LoadConfigMap(DB)
Dim maxPoolSize As Int = config.GetDefault("MaxPoolSize", 5) ' Por defecto 5
jo.RunMethod("setInitialPoolSize", Array(initialPoolSize)) ' Sets the inital pool size to 2 ' Bloque Try-Catch para la inicialización y configuración del pool.
jo.RunMethod("setMinPoolSize", Array(minPoolSize)) ' Sets the min pool size to 2 ' Esto capturará cualquier error crítico que impida la conexión inicial a la base de datos.
jo.RunMethod("setMaxPoolSize", Array(maxPoolSize)) ' Max number of concurrent connections Try
' PASO 2: Inicializar el objeto B4X ConnectionPool.
' Esto crea la instancia subyacente de com.mchange.v2.c3p0.ComboPooledDataSource (la librería C3P0).
' En este punto, C3P0 solo se inicializa como objeto. Aún no intenta hacer conexiones activas.
' Se le pasan los parámetros básicos para que C3P0 pueda construirse.
pool.Initialize(config.Get("DriverClass"), config.Get("JdbcUrl"), config.Get("User"), config.Get("Password"))
' Define el tiempo máximo de inactividad en SEGUNDOS. Dim jo As JavaObject = pool ' Obtener la referencia JavaObject para acceder a métodos de configuración avanzados de C3P0.
' Una conexión que permanezca en el pool sin ser usada por más de 300 segundos (5 minutos)
' será cerrada para liberar recursos, siempre y cuando no se viole el tamaño mínimo del pool (minPoolSize).
jo.RunMethod("setMaxIdleTime", Array As Object(300))
' Define la "edad" o tiempo de vida máximo de una conexión en SEGUNDOS. ' PASO 3: Aplicar *todas* las propiedades de configuración de C3P0 INMEDIATAMENTE.
' Después de 900 segundos (15 minutos) desde su creación, la conexión será marcada para ser ' Esto debe ocurrir *después* de 'pool.Initialize' pero *antes* de que C3P0 intente realmente adquirir conexiones.
' eliminada y reemplazada por una nueva la próxima vez que regrese al pool. ' Esto asegura que las configuraciones sean efectivas desde el primer intento de conexión.
' Esto previene problemas con conexiones "viciadas" y mantiene el pool saludable.
jo.RunMethod("setMaxConnectionAge", Array As Object(900))
' Define el tiempo máximo de espera por una conexión en MILISEGUNDOS. ' Lectura de los valores desde el archivo de configuración, con valores por defecto si no se encuentran.
' Si todas las conexiones del pool están ocupadas, una nueva petición esperará hasta Dim initialPoolSize As Int = config.GetDefault("InitialPoolSize", 3)
' 60000 milisegundos (1 minuto). Si ninguna conexión se libera en ese lapso, la petición Dim minPoolSize As Int = config.GetDefault("MinPoolSize", 2)
' fallará con un error. Esto evita que la aplicación se congele bajo carga pesada. Dim maxPoolSize As Int = config.GetDefault("MaxPoolSize", 5)
jo.RunMethod("setCheckoutTimeout", Array As Object(60000)) Dim acquireIncrement As Int = config.GetDefault("AcquireIncrement", 5)
' com.mchange.v2.c3p0.ComboPooledDataSource [ ' Configuración de los parámetros del pool de conexiones C3P0:
jo.RunMethod("setInitialPoolSize", Array(initialPoolSize)) ' Define el número de conexiones que se intentarán crear al iniciar el pool.
jo.RunMethod("setMinPoolSize", Array(minPoolSize)) ' Fija el número mínimo de conexiones que el pool mantendrá abiertas.
jo.RunMethod("setMaxPoolSize", Array(maxPoolSize)) ' Define el número máximo de conexiones simultáneas.
jo.RunMethod("setAcquireIncrement", Array(acquireIncrement)) ' Cuántas conexiones nuevas se añaden en lote si el pool se queda sin disponibles.
jo.RunMethod("setMaxIdleTime", Array As Object(config.GetDefault("MaxIdleTime", 300))) ' Tiempo máximo de inactividad de una conexión antes de cerrarse (segundos).
jo.RunMethod("setMaxConnectionAge", Array As Object(config.GetDefault("MaxConnectionAge", 900))) ' Tiempo máximo de vida de una conexión (segundos).
jo.RunMethod("setCheckoutTimeout", Array As Object(config.GetDefault("CheckoutTimeout", 60000))) ' Tiempo máximo de espera por una conexión del pool (milisegundos).
' LÍNEAS CRÍTICAS PARA FORZAR UN COMPORTAMIENTO NO SILENCIOSO DE C3P0:
' Por defecto, C3P0 puede reintentar muchas veces y no lanzar una excepción si las conexiones iniciales fallan.
' Estas líneas fuerzan a C3P0 a ser estricto y reportar errores de inmediato.
jo.RunMethod("setAcquireRetryAttempts", Array As Object(1)) ' Limita los intentos iniciales de adquisición a 1.
jo.RunMethod("setBreakAfterAcquireFailure", Array As Object(True)) ' ¡Forza a C3P0 a lanzar una excepción si falla al adquirir conexiones!
' PASO 4: Forzar la creación de conexiones iniciales y verificar el estado.
' Este paso es VITAL. Obliga a C3P0 a intentar establecer las conexiones iniciales (InitialPoolSize)
' *con la configuración ya establecida*. Si hay un problema de conectividad real, la excepción
' se capturará aquí y se reportará.
Dim tempCon As SQL = pool.GetConnection ' Adquiere una conexión para forzar al pool a inicializarse.
If tempCon.IsInitialized Then
tempCon.Close ' Devolvemos la conexión inmediatamente al pool para que esté disponible.
End If
' com.mchange.v2.c3p0.ComboPooledDataSource [
' acquireIncrement -> 3, ' acquireIncrement -> 3,
' acquireRetryAttempts -> 30, ' acquireRetryAttempts -> 30,
' acquireRetryDelay -> 1000, ' acquireRetryDelay -> 1000,
@@ -93,63 +117,158 @@ Public Sub Initialize(DB As String)
' unreturnedConnectionTimeout -> 0, ' unreturnedConnectionTimeout -> 0,
' userOverrides -> {}, ' userOverrides -> {},
' usesTraditionalReflectiveProxies -> False ' usesTraditionalReflectiveProxies -> False
' ] ' ]
'
Catch
' Si ocurre un error durante la inicialización del pool o al forzar la conexión,
' este Log es CRÍTICO para el diagnóstico, especialmente en un entorno de producción.
Log($"RDCConnector.Initialize para ${DB}: ERROR CRÍTICO al inicializar/forzar conexión: ${LastException.Message}"$)
End Try
' Dim jo2 As JavaObject = pool ' Configuración de depuración de queries. Se activa automáticamente si el proyecto se ejecuta en modo DEBUG.
' Log(jo2.GetField("END_TO_END_CLIENTID_INDEX")) #If DEBUG
' DebugQueries = True
#Else
DebugQueries = False
#End If
' jo.RunMethod("setPreferredTestQuery", Array("BEGIN DBMS_SESSION.SET_IDENTIFIER('whatever'); END;")) ' Se obtiene el puerto del servidor HTTP desde la configuración de esta base de datos.
' jo.RunMethod("setPreferredTestQuery", Array("alter session set current_schema=MYSCHEMA")) ' Nota: En el diseño actual, el puerto principal lo define DB1 (config.properties).
' jo2.RunMethod("setClientIdentifier",Array( "MAX")) ' Tiempo máximo de inactividad antes de cerrar una conexión
#if DEBUG
DebugQueries = True
#else
DebugQueries = False
#end if
serverPort = config.Get("ServerPort") serverPort = config.Get("ServerPort")
' Asegura que el identificador DB no sea una cadena vacía para la carga de comandos.
If DB = "" Then DB = "DB1" If DB = "" Then DB = "DB1"
' Carga los comandos SQL predefinidos de esta base de datos en el mapa global 'commandsMap'.
LoadSQLCommands(config, DB) LoadSQLCommands(config, DB)
End Sub End Sub
' Carga el mapa de configuración (JdbcUrl, User, Password, etc.) desde el archivo .properties correspondiente.
' DB: El identificador de la base de datos (ej. "DB1", "DB2").
' Retorna un Mapa con la configuración leída.
Private Sub LoadConfigMap(DB As String) As Map Private Sub LoadConfigMap(DB As String) As Map
Private DBX As String = "" Private DBX As String = ""
If DB <> "" Then DBX = "." & DB If DB <> "" Then DBX = "." & DB ' Construye el sufijo del nombre de archivo (ej. ".DB2").
Log("===========================================") Log($"Leemos el config${DBX}.properties"$) ' Mantenemos este log para confirmación de carga.
Log($"Leemos el config${DBX}.properties"$)
Return File.ReadMap("./", "config" & DBX & ".properties") Return File.ReadMap("./", "config" & DBX & ".properties")
End Sub End Sub
' Obtiene la sentencia SQL completa para un comando dado desde el mapa de comandos cargado.
' DB: El identificador de la base de datos.
' Key: El nombre del comando SQL (ej. "select_user").
' Retorna la sentencia SQL como String.
Public Sub GetCommand(DB As String, Key As String) As String Public Sub GetCommand(DB As String, Key As String) As String
Log("==== GetCommand ====") commands = Main.commandsMap.get(DB).As(Map) ' Obtiene los comandos de la DB específica del mapa global.
' Log("|" & DB & "|" & Key & "|")
commands = Main.commandsMap.get(DB).As(Map)
If commands.ContainsKey("sql." & Key) = False Then If commands.ContainsKey("sql." & Key) = False Then
Log("*** Command not found: " & Key) Log("*** Command not found: " & Key) ' Este log es importante mantenerlo si un comando no se encuentra.
End If End If
' Log(commands.ContainsKey("sql." & Key)) Return commands.Get("sql." & Key) ' Retorna la sentencia SQL.
Log("========= Traemos """ & Key & """ ==========")
Log(">>>>>> " & commands.Get("sql." & Key) & " <<<<<<")
Return commands.Get("sql." & Key)
End Sub End Sub
' Obtiene una conexión SQL funcional del pool de conexiones para la base de datos especificada.
' DB: El identificador de la base de datos.
' Retorna un objeto SQL (la conexión a la base de datos).
Public Sub GetConnection(DB As String) As SQL Public Sub GetConnection(DB As String) As SQL
Log("==== GetConnection ==== ") If DB.EqualsIgnoreCase("DB1") Then DB = ""
If DB.EqualsIgnoreCase("DB1") Then DB = "" 'Esto para el config.properties or default ' En modo de depuración, recarga los comandos SQL en cada petición.
' Esto permite modificar queries en config.properties sin reiniciar el servidor.
If DebugQueries Then LoadSQLCommands(LoadConfigMap(DB), DB) If DebugQueries Then LoadSQLCommands(LoadConfigMap(DB), DB)
Return pool.GetConnection Return pool.GetConnection ' Retorna una conexión del pool.
End Sub End Sub
' Carga todos los comandos SQL del mapa de configuración en el mapa global 'commandsMap'.
' config2: El mapa de configuración de la DB actual.
' DB: El identificador de la base de datos.
Private Sub LoadSQLCommands(config2 As Map, DB As String) Private Sub LoadSQLCommands(config2 As Map, DB As String)
Log("==== LoadSQLCommands ==== ")
Log($"Cargamos los comandos desde el config.${DB}.properties"$)
Dim newCommands As Map Dim newCommands As Map
newCommands.Initialize newCommands.Initialize
For Each k As String In config2.Keys For Each k As String In config2.Keys
If k.StartsWith("sql.") Then If k.StartsWith("sql.") Then ' Busca claves que comiencen con "sql." (ej. "sql.select_user").
newCommands.Put(k, config2.Get(k)) newCommands.Put(k, config2.Get(k)) ' Añade el comando al mapa.
End If End If
Next Next
commands = newCommands commands = newCommands ' Actualiza el mapa de comandos de esta instancia de RDCConnector.
' Log($"Inicializado: ${DB} "$ & Main.commandsMap.IsInitialized) Main.commandsMap.Put(DB, commands) ' Almacena el mapa de comandos en el mapa global 'commandsMap' de Main.
Main.commandsMap.Put(DB, commands) End Sub
' Nuevo: Obtiene estadísticas detalladas del pool de conexiones.
Public Sub GetPoolStats As Map
Dim stats As Map
stats.Initialize
' Log("--- RDCConnector.GetPoolStats llamado ---") ' Log de inicio
If pool.IsInitialized Then
' Log("RDCConnector.GetPoolStats: Pool está inicializado. Intentando obtener métricas.")
Dim jo As JavaObject = pool ' Convertimos el objeto pool a JavaObject para acceder a sus métodos.
Try
' --- Métricas en tiempo real del pool ---
Dim totalConn As Object = jo.RunMethod("getNumConnectionsAllUsers", Null)
stats.Put("TotalConnections", totalConn)
' Log($"RDCConnector.GetPoolStats: TotalConnections = ${totalConn}"$)
Dim busyConn As Object = jo.RunMethod("getNumBusyConnectionsAllUsers", Null)
stats.Put("BusyConnections", busyConn)
' Log($"RDCConnector.GetPoolStats: BusyConnections = ${busyConn}"$)
Dim idleConn As Object = jo.RunMethod("getNumIdleConnectionsAllUsers", Null)
stats.Put("IdleConnections", idleConn)
' Log($"RDCConnector.GetPoolStats: IdleConnections = ${idleConn}"$)
' --- Valores de configuración del pool (para referencia) ---
Dim initialSize As Object = jo.RunMethod("getInitialPoolSize", Null)
stats.Put("InitialPoolSize", initialSize)
' Log($"RDCConnector.GetPoolStats: InitialPoolSize = ${initialSize}"$)
Dim minSize As Object = jo.RunMethod("getMinPoolSize", Null)
stats.Put("MinPoolSize", minSize)
' Log($"RDCConnector.GetPoolStats: MinPoolSize = ${minSize}"$)
Dim maxSize As Object = jo.RunMethod("getMaxPoolSize", Null)
stats.Put("MaxPoolSize", maxSize)
' Log($"RDCConnector.GetPoolStats: MaxPoolSize = ${maxSize}"$)
Dim acquireInc As Object = jo.RunMethod("getAcquireIncrement", Null)
stats.Put("AcquireIncrement", acquireInc)
' Log($"RDCConnector.GetPoolStats: AcquireIncrement = ${acquireInc}"$)
Dim maxIdle As Object = jo.RunMethod("getMaxIdleTime", Null)
stats.Put("MaxIdleTime", maxIdle)
' Log($"RDCConnector.GetPoolStats: MaxIdleTime = ${maxIdle}"$)
Dim maxAge As Object = jo.RunMethod("getMaxConnectionAge", Null)
stats.Put("MaxConnectionAge", maxAge)
' Log($"RDCConnector.GetPoolStats: MaxConnectionAge = ${maxAge}"$)
Dim checkoutTime As Object = jo.RunMethod("getCheckoutTimeout", Null)
stats.Put("CheckoutTimeout", checkoutTime)
' Log($"RDCConnector.GetPoolStats: CheckoutTimeout = ${checkoutTime}"$)
Catch
Log("RDCConnector.GetPoolStats: ERROR CRÍTICO al obtener estadísticas del pool: " & LastException.Message)
stats.Put("Error", LastException.Message)
End Try
Else
Log("RDCConnector.GetPoolStats: ADVERTENCIA: Pool NO está inicializado. Retornando mapa con error.")
stats.Put("Error", "Pool de conexiones no inicializado para esta DB.")
End If
' *** CORRECCIÓN: Usamos JSONGenerator para serializar el mapa a String para el Log ***
Dim tempJsonGen As JSONGenerator ' Declaramos un JSONGenerator temporal
tempJsonGen.Initialize(stats) ' Lo inicializamos con el mapa 'stats'
' Log("--- RDCConnector.GetPoolStats finalizado. Retornando stats: " & tempJsonGen.ToString & " ---") ' Log de fin con JSON
Return stats
End Sub
' *** NUEVA SUBRUTINA: Cierra el pool de conexiones de forma ordenada usando JavaObject ***
' Este método es crucial para liberar los recursos de la base de datos
' cuando un conector RDC ya no es necesario o va a ser reemplazado.
Public Sub Close
If pool <> Null And pool.IsInitialized Then
Log($"RDCConnector: Cerrando pool de conexiones."$)
' Convertimos el objeto pool de B4X a un JavaObject para poder llamar a su método 'close()'
' que no está expuesto directamente en la envoltura de B4X.
Dim joPool As JavaObject = pool
joPool.RunMethod("close", Null) ' Llamamos al método 'close()' del objeto Java subyacente de C3P0.
End If
End Sub End Sub

View File

@@ -30,40 +30,54 @@ Library6=jshell
Library7=json Library7=json
Library8=jsql Library8=jsql
Library9=bcrypt Library9=bcrypt
Module1=ChangePassHandler Module1=Cambios
Module10=ping Module10=Manager
Module11=RDCConnector Module11=ping
Module12=TestHandler Module12=RDCConnector
Module2=DBHandlerB4X Module13=TestHandler
Module3=DBHandlerJSON Module2=ChangePassHandler
Module4=DoLoginHandler Module3=DBHandlerB4X
Module5=faviconHandler Module4=DBHandlerJSON
Module6=GlobalParameters Module5=DoLoginHandler
Module7=LoginHandler Module6=faviconHandler
Module8=LogoutHandler Module7=GlobalParameters
Module9=Manager Module8=LoginHandler
Module9=LogoutHandler
NumberOfFiles=10 NumberOfFiles=10
NumberOfLibraries=9 NumberOfLibraries=9
NumberOfModules=12 NumberOfModules=13
Version=10.3 Version=10.3
@EndOfDesignText@ @EndOfDesignText@
'Non-UI application (console / server application) 'Non-UI application (console / server application)
#Region Project Attributes
#CommandLineArgs: #Region Project Attributes
#MergeLibraries: True #CommandLineArgs:
' VERSION 5.09.08 #MergeLibraries: True
'########################################################################################################### ' VERSION 5.09.014
'###################### PULL ############################################################# '###########################################################################################################
'Ctrl + click ide://run?file=%WINDIR%\System32\cmd.exe&Args=/c&Args=git&Args=pull '###################### 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 #############################################################
'########################################################################################################### '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 '###################### 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 #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 'change based on the jdbc jar file
'#AdditionalJar: mysql-connector-java-5.1.27-bin '#AdditionalJar: mysql-connector-java-5.1.27-bin
'#AdditionalJar: postgresql-42.7.0 '#AdditionalJar: postgresql-42.7.0
@@ -72,104 +86,164 @@ Version=10.3
#AdditionalJar: sqlite-jdbc-3.7.2 #AdditionalJar: sqlite-jdbc-3.7.2
Sub Process_Globals Sub Process_Globals
Public srvr As Server ' --- Variables globales accesibles desde cualquier parte del proyecto ---
Public const VERSION As Float = 2.23 Public srvr As Server ' El objeto principal del servidor HTTP de B4J.
Type DBCommand (Name As String, Parameters() As Object) Public const VERSION As Float = 2.23 ' La versión actual de este servidor jRDC modificado.
Type DBResult (Tag As Object, Columns As Map, Rows As List)
Dim listaDeCP As List ' Tipos personalizados para la serialización y deserialización de datos
Dim cpFiles As List ' entre el cliente B4X (DBRequestManager) y el servidor jRDC2.
Public Connectors, commandsMap As Map Type DBCommand (Name As String, Parameters() As Object) ' Define un comando SQL.
Public SQL1 As SQL ' Objeto SQL para la base de datos de usuarios Type DBResult (Tag As Object, Columns As Map, Rows As List) ' Define la estructura de un resultado de consulta.
Private bc As BCrypt
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 End Sub
Sub AppStart (Args() As String) Sub AppStart (Args() As String)
' --- INICIO DE CAMBIOS --- ' --- Subrutina principal que se ejecuta al iniciar la aplicación ---
' Inicializamos la base de datos. Se creará si no existe.
' 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 InitializeSQLiteDatabase
' --- FIN DE CAMBIOS ---
listaDeCP.Initialize ' 2. Inicializa los mapas globales definidos en GlobalParameters.bas.
srvr.Initialize("") ' Estos mapas se usan para monitorear el servidor y gestionar configuraciones dinámicas.
Dim con As RDCConnector GlobalParameters.mpLogs.Initialize ' Mapa para almacenar logs de actividad.
Connectors = srvr.CreateThreadSafeMap GlobalParameters.mpTotalRequests.Initialize ' Mapa para contar peticiones por endpoint/DB.
commandsMap.Initialize GlobalParameters.mpTotalConnections.Initialize ' Mapa para almacenar el estado de los pools de conexión por DB.
con.Initialize("DB1") 'Inicializamos el default de config.properties GlobalParameters.mpBlockConnection.Initialize ' Mapa para gestionar IPs bloqueadas (si la funcionalidad está activa).
Connectors.Put("DB1", con)
srvr.Port = con.serverPort ' 3. Inicializa las estructuras principales del servidor HTTP.
listaDeCP.Add("DB1") 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("./") cpFiles = File.ListFiles("./")
If cpFiles.Size > 0 Then If cpFiles.Size > 0 Then
Log(cpFiles) For i = 0 To cpFiles.Size - 1
For i = 0 To cpFiles.Size - 1 ' Procesa 'config.DB2.properties'
If cpFiles.Get(i) = "config.DB2.properties" Then ' Si existe el archivo DB2, lo usamos. If cpFiles.Get(i) = "config.DB2.properties" Then
Dim con As RDCConnector Dim con2 As RDCConnector ' Declara una variable específica y única para el conector de DB2.
con.Initialize("DB2") con2.Initialize("DB2") ' Inicializa la instancia del conector para "DB2".
Connectors.Put("DB2", con) Connectors.Put("DB2", con2) ' Asocia "DB2" con su instancia de RDCConnector.
listaDeCP.Add("DB2") listaDeCP.Add("DB2") ' Añade "DB2" a la lista de bases de datos.
End If Log("Main.AppStart: Conector 'DB2' inicializado exitosamente.")
If cpFiles.Get(i) = "config.DB3.properties" Then ' Si existe el archivo DB3, lo usamos. End If
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 --- ' Procesa 'config.DB3.properties'
' 1. Rutas para el sistema de Login If cpFiles.Get(i) = "config.DB3.properties" Then
srvr.AddHandler("/login", "LoginHandler", True) ' Sirve la página de login Dim con3 As RDCConnector ' Declara una variable específica y única para el conector de DB3.
srvr.AddHandler("/dologin", "DoLoginHandler", True) ' Procesa el intento de login con3.Initialize("DB3") ' Inicializa la instancia del conector para "DB3".
srvr.AddHandler("/logout", "LogoutHandler", True) ' Cierra la sesión Connectors.Put("DB3", con3) ' Asocia "DB3" con su instancia de RDCConnector.
srvr.AddHandler("/changepass", "ChangePassHandler", True) listaDeCP.Add("DB3") ' Añade "DB3" a la lista de bases de datos.
' 2. El handler del manager se queda igual, pero ahora estará protegido Log("Main.AppStart: Conector 'DB3' inicializado exitosamente.")
srvr.AddHandler("/manager", "Manager", True) End If
' --- FIN DE CAMBIOS ---
srvr.AddHandler("/DBJ", "DBHandlerJSON", True) ' Procesa 'config.DB4.properties'
srvr.AddHandler("/dbrquery", "DBHandlerJSON", True) If cpFiles.Get(i) = "config.DB4.properties" Then
srvr.AddHandler("/favicon.ico", "faviconHandler", True) Dim con4 As RDCConnector ' Declara una variable específica y única para el conector de DB4.
' srvr.AddHandler("/*", "DB1Handler", False) ' Si no se especifica una base de datos, entonces asignamos la solicitud a la DB1. 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
srvr.AddHandler("/*", "DBHandlerB4X", True) ' 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
srvr.Start
Log("===========================================================")
Log($"-=== jRDC is running on port: ${srvr.port} (version = $1.2{VERSION}) ===-"$)
Log("===========================================================")
StartMessageLoop
End Sub 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 Sub InitializeSQLiteDatabase
Dim dbFileName As String = "users.db" Dim dbFileName As String = "users.db" ' Nombre del archivo de la base de datos SQLite.
' 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 ' Verifica si el archivo de la base de datos ya existe en el directorio de la aplicación.
Dim defaultUser As String = "admin" If File.Exists(File.DirApp, dbFileName) = False Then
Dim defaultPass As String = "12345" Log("Creando nueva base de datos de usuarios: " & dbFileName)
Dim hashedPass As String = bc.hashpw(defaultPass, bc.gensalt) ' bc.HashPassword(defaultPass) ' Inicializa la conexión a la base de datos SQLite, creándola si no existe (último parámetro en True).
SQL1.InitializeSQLite(File.DirApp, dbFileName, True)
SQL1.ExecNonQuery2("INSERT INTO users (username, password_hash) VALUES (?, ?)", Array As Object(defaultUser, hashedPass)) ' Define y ejecuta la sentencia SQL para crear la tabla 'users'.
Log($"Usuario por defecto creado -> user: ${defaultUser}, pass: ${defaultPass}"$) Dim createUserTable As String = "CREATE TABLE users (username TEXT PRIMARY KEY, password_hash TEXT NOT NULL)"
Else SQL1.ExecNonQuery(createUserTable)
' Si ya existe, solo la abrimos
SQL1.InitializeSQLite(File.DirApp, dbFileName, True) ' Crea un usuario por defecto para facilitar el primer acceso al panel de administración.
Log("Base de datos de usuarios cargada.") Dim defaultUser As String = "admin"
End If 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 End Sub
' --- FIN DE CAMBIOS ---

View File

@@ -3,6 +3,7 @@ ModuleBookmarks1=
ModuleBookmarks10= ModuleBookmarks10=
ModuleBookmarks11= ModuleBookmarks11=
ModuleBookmarks12= ModuleBookmarks12=
ModuleBookmarks13=
ModuleBookmarks2= ModuleBookmarks2=
ModuleBookmarks3= ModuleBookmarks3=
ModuleBookmarks4= ModuleBookmarks4=
@@ -16,6 +17,7 @@ ModuleBreakpoints1=
ModuleBreakpoints10= ModuleBreakpoints10=
ModuleBreakpoints11= ModuleBreakpoints11=
ModuleBreakpoints12= ModuleBreakpoints12=
ModuleBreakpoints13=
ModuleBreakpoints2= ModuleBreakpoints2=
ModuleBreakpoints3= ModuleBreakpoints3=
ModuleBreakpoints4= ModuleBreakpoints4=
@@ -29,6 +31,7 @@ ModuleClosedNodes1=
ModuleClosedNodes10= ModuleClosedNodes10=
ModuleClosedNodes11= ModuleClosedNodes11=
ModuleClosedNodes12= ModuleClosedNodes12=
ModuleClosedNodes13=
ModuleClosedNodes2= ModuleClosedNodes2=
ModuleClosedNodes3= ModuleClosedNodes3=
ModuleClosedNodes4= ModuleClosedNodes4=
@@ -37,6 +40,6 @@ ModuleClosedNodes6=
ModuleClosedNodes7= ModuleClosedNodes7=
ModuleClosedNodes8= ModuleClosedNodes8=
ModuleClosedNodes9= ModuleClosedNodes9=
NavigationStack=DBHandlerJSON,SendErrorResponse,239,0,RDCConnector,LoadConfigMap,86,0,RDCConnector,GetCommand,90,0,faviconHandler,Initialize,12,0,faviconHandler,Class_Globals,8,0,Manager,Initialize,9,0,RDCConnector,Class_Globals,9,0,Main,AppStart,49,0,Manager,Handle,23,6,RDCConnector,Initialize,38,0 NavigationStack=RDCConnector,Class_Globals,12,0,Manager,Initialize,8,0,RDCConnector,GetPoolStats,256,0,Main,Process_Globals,58,0,RDCConnector,Close,266,0,Manager,Handle,171,2,Main,AppStart,138,0,DBHandlerJSON,Handle,82,0,RDCConnector,Initialize,70,0,Cambios,Process_Globals,43,6
SelectedBuild=0 SelectedBuild=0
VisibleModules=2,3,11,9,12 VisibleModules=3,4,12,10,13,1