- 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
' 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}"$)
' *** 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
' Ejecuta una consulta única usando el protocolo V2 (B4XSerializator).

View File

@@ -12,14 +12,13 @@ Sub Class_Globals
End Sub
' 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
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).
Sub Handle(req As ServletRequest, resp As ServletResponse)
Log("============== DB1JsonHandler ==============")
' --- Headers CORS (Cross-Origin Resource Sharing) ---
' 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.
@@ -34,37 +33,54 @@ Sub Handle(req As ServletRequest, resp As ServletResponse)
' Establece "DB1" como el nombre de la base de datos por defecto.
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)
' Declara una variable para la conexión SQL.
Dim con As SQL
' Inicia un bloque Try...Catch para manejar posibles errores durante la ejecución.
Try
' Obtiene el valor del parámetro 'j' de la petición. Se espera que contenga una cadena JSON.
Dim jsonString As String = req.GetParameter("j")
' Verifica si el parámetro 'j' es nulo o está vacío.
Dim jsonString As String
' *** 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
' Si falta el parámetro, envía una respuesta de error 400 (Bad Request) y termina la ejecución.
SendErrorResponse(resp, 400, "Falta el parametro 'j' en el URL")
' Si falta el JSON, envía una respuesta de error 400 (Bad Request) y termina la ejecución.
SendErrorResponse(resp, 400, "Falta el parámetro 'j' en el URL o el cuerpo JSON en la petición.")
Return
End If
' Crea un objeto JSONParser para analizar la cadena JSON.
Dim parser As JSONParser
parser.Initialize(jsonString)
' Convierte la cadena JSON en un objeto Map, que es como un diccionario (clave-valor).
Dim RootMap As Map = parser.NextObject
' Extrae los datos necesarios del JSON.
Dim execType As String = RootMap.GetDefault("exec", "") ' Tipo de ejecución: "executeQuery" o "executeCommand".
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.
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).
' Se obtiene "params" como una Lista en lugar de un Mapa, para soportar el ordenamiento de B4A.
Dim paramsList As List = RootMap.Get("params")
' Si la lista de parámetros es nula (no se proporcionó en el JSON),
' la inicializamos como una lista vacía para evitar errores más adelante.
' Si la lista de parámetros es nula (no se proporcionó en el JSON), la inicializamos como una lista vacía.
If paramsList = Null Or paramsList.IsInitialized = False Then
paramsList.Initialize
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.
If Main.listaDeCP.IndexOf(DB) = -1 Then
SendErrorResponse(resp, 400, "Parametro 'DB' invalido. El nombre '" & DB & "' no es válido.")
' Se añade Return para detener la ejecución si la BD no es válida.
SendErrorResponse(resp, 400, "Parámetro 'DB' inválido. El nombre '" & DB & "' no es válido.")
Return
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)
' Obtiene la cadena SQL del archivo de configuración usando el nombre de la consulta (queryName).
Dim sqlCommand As String = Connector.GetCommand(DB, queryName)
' <<< 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
' 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}'."$
' Registra el error en el log del servidor para depuración.
Log(errorMessage)
' Envía una respuesta de error 400 (Bad Request) al cliente en formato JSON.
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
' Detiene la ejecución del método Handle para esta petición.
Return
End If
' <<< FIN VALIDACIÓN >>>
' Comprueba el tipo de ejecución solicitado ("executeQuery" o "executeCommand").
If execType.ToLowerCase = "executequery" Then
' Declara una variable para almacenar el resultado de la consulta.
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
' =================================================================
' === 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
' Obtiene cuántos parámetros se recibieron de la lista.
Dim receivedParams As Int = paramsList.Size
Log($"expectedParams: ${expectedParams}, receivedParams: ${receivedParams}"$)
If expectedParams <> receivedParams Then
' Si no coinciden, envía un error 400 detallado.
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.
SendErrorResponse(resp, 400, $"Número de parámetros equivocado para '${queryName}'. Se esperaban ${expectedParams} y se recibieron ${receivedParams}."$)
If con <> Null And con.IsInitialized Then con.Close
' Detiene la ejecución para evitar un error en la base de datos.
Return
End If
' =================================================================
' Ejecuta la consulta pasando el comando SQL y la lista de parámetros.
rs = con.ExecQuery2(sqlCommand, paramsList)
Else
' Si no hay '?', ejecuta la consulta directamente sin parámetros.
rs = con.ExecQuery(sqlCommand)
End If
' --- Procesamiento de resultados ---
' Prepara una lista para almacenar todas las filas del resultado.
Dim ResultList As List
ResultList.Initialize
' Usa un objeto JavaObject para acceder a los metadatos del resultado (info de columnas).
Dim jrs As JavaObject = rs
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)
' Itera sobre cada fila del resultado (ResultSet).
Do While rs.NextRow
' Crea un mapa para almacenar los datos de la fila actual (columna -> valor).
Dim RowMap As Map
RowMap.Initialize
' Itera sobre cada columna de la fila.
For i = 1 To cols
' Obtiene el nombre de la columna.
Dim ColumnName As String = rsmd.RunMethod("getColumnName", Array(i))
' Obtiene el valor de la columna.
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)
Next
' Añade el mapa de la fila a la lista de resultados.
ResultList.Add(RowMap)
Loop
' Cierra el ResultSet para liberar recursos de la base de datos.
rs.Close
' Envía una respuesta de éxito con la lista de resultados en formato JSON.
SendSuccessResponse(resp, CreateMap("result": ResultList))
Else If execType.ToLowerCase = "executecommand" Then
' Si es un comando (INSERT, UPDATE, DELETE), también valida los parámetros.
If sqlCommand.Contains("?") Then
' =================================================================
' === 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 receivedParams As Int = paramsList.Size
If expectedParams <> receivedParams Then
SendErrorResponse(resp, 400, $"Número de parametros equivocado para '${queryName}'. Se esperaban ${expectedParams} y se recibieron ${receivedParams}."$)
' Cierra la conexión antes de salir.
SendErrorResponse(resp, 400, $"Número de parámetros equivocado para '${queryName}'. Se esperaban ${expectedParams} y se recibieron ${receivedParams}."$)
If con <> Null And con.IsInitialized Then con.Close
' Detiene la ejecución.
Return
End If
' =================================================================
End If
' Ejecuta el comando que no devuelve resultados (NonQuery) con sus parámetros.
con.ExecNonQuery2(sqlCommand, paramsList)
' Envía una respuesta de éxito con un mensaje de confirmación.
SendSuccessResponse(resp, CreateMap("message": "Command executed successfully"))
Else
' Si el valor de 'exec' no es ni "executeQuery" ni "executeCommand", envía un error.
SendErrorResponse(resp, 400, "Parametro 'exec' inválido. '" & execType & "' no es un valor permitido.")
SendErrorResponse(resp, 400, "Parámetro 'exec' inválido. '" & execType & "' no es un valor permitido.")
End If
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)
' Envía una respuesta de error 500 (Internal Server Error) con el mensaje de la excepción.
SendErrorResponse(resp, 500, LastException.Message)
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
' 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
End If
End Sub

View File

@@ -11,7 +11,13 @@ DriverClass=oracle.jdbc.driver.OracleDriver
#GOHAN ---> server
#JdbcUrl=jdbc:oracle:thin:@//10.0.0.205: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
User=SALMA
@@ -44,11 +50,13 @@ Debug=true
sql.traeConexion=select 'DB2' as conexion from dual
sql.select_soporte=select * from GUNA.soporte
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_GV2=select cat_ve_version from GUNA.cat_version
sql.selectAlmacen=select * from cat_almacen where cat_al_id = ?
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.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
#JdbcUrl=jdbc:oracle:thin:@//10.0.0.205: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
#User=GUNA

View File

@@ -11,7 +11,13 @@ DriverClass=oracle.jdbc.driver.OracleDriver
#GOHAN ---> server
#JdbcUrl=jdbc:oracle:thin:@//10.0.0.205: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
User=SALMA

View File

@@ -11,8 +11,13 @@ DriverClass=oracle.jdbc.driver.OracleDriver
#GOHAN ---> server
#JdbcUrl=jdbc:oracle:thin:@//10.0.0.205: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
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.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_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 = ?

View File

@@ -28,21 +28,21 @@ Sub Handle(req As ServletRequest, resp As ServletResponse)
' --- MANEJO ESPECIAL PARA SNAPSHOT ---
' El comando "snapshot" no devuelve HTML, sino una imagen. Lo manejamos por separado al principio.
If Command = "snapshot" Then
Try
resp.ContentType = "image/png"
Dim robot, toolkit As JavaObject
robot.InitializeNewInstance("java.awt.Robot", Null)
toolkit.InitializeStatic("java.awt.Toolkit")
Dim screenRect As JavaObject
screenRect.InitializeNewInstance("java.awt.Rectangle", Array As Object( _
toolkit.RunMethodJO("getDefaultToolkit", Null).RunMethod("getScreenSize", Null)))
Dim image As JavaObject = robot.RunMethod("createScreenCapture", Array As Object(screenRect))
Dim ImageIO As JavaObject
ImageIO.InitializeStatic("javax.imageio.ImageIO").RunMethod("write", Array As Object(image, "png", resp.OutputStream))
Catch
resp.SendError(500, LastException.Message)
End Try
Return ' Detenemos la ejecución aquí para no enviar más HTML.
' Try
' resp.ContentType = "image/png"
' Dim robot, toolkit As JavaObject
' robot.InitializeNewInstance("java.awt.Robot", Null)
' toolkit.InitializeStatic("java.awt.Toolkit")
' Dim screenRect As JavaObject
' screenRect.InitializeNewInstance("java.awt.Rectangle", Array As Object( _
' toolkit.RunMethodJO("getDefaultToolkit", Null).RunMethod("getScreenSize", Null)))
' Dim image As JavaObject = robot.RunMethod("createScreenCapture", Array As Object(screenRect))
' Dim ImageIO As JavaObject
' ImageIO.InitializeStatic("javax.imageio.ImageIO").RunMethod("write", Array As Object(image, "png", resp.OutputStream))
' Catch
' resp.SendError(500, LastException.Message)
' End Try
' Return ' Detenemos la ejecución aquí para no enviar más HTML.
End If
' --- 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) ---
sb.Append("<h1>Panel de Administración jRDC</h1>")
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("<div id='changePassForm' style='display:none;'>")
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 ###
' =========================================================================
If Command = "reload" Then
Private estaDB As String = ""
For i = 0 To Main.listaDeCP.Size - 1
Main.Connectors.Get(Main.listaDeCP.get(i)).As(RDCConnector).Initialize(Main.listaDeCP.get(i))
If Main.listaDeCP.get(i) <> "DB1" Then estaDB = "." & Main.listaDeCP.get(i) Else estaDB = ""
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/>"$)
sb.Append($"<b>JdbcUrl:</b> ${Main.Connectors.Get(Main.listaDeCP.get(i)).As(RDCConnector).config.Get("JdbcUrl")}</b><br/>"$)
sb.Append($"<b>User:</b> ${Main.Connectors.Get(Main.listaDeCP.get(i)).As(RDCConnector).config.Get("User")}</b><br/>"$)
sb.Append($"<b>ServerPort:</b> ${Main.srvr.Port}</b><br/><br/>"$)
' Usamos un StringBuilder temporal para acumular los logs de la recarga antes de añadirlos al StringBuilder principal.
Dim sbTemp As StringBuilder
sbTemp.Initialize
sbTemp.Append($"Iniciando recarga de configuración (Hot-Swap) ($DateTime{DateTime.Now})"$).Append("<br>" & CRLF)
' 1. Crear un nuevo mapa temporal para almacenar los conectores recién inicializados.
Dim newConnectors As Map
newConnectors.Initialize
' 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
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
Try
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)
sb.Append(j.ToString)
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
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
' =========================================================================
' ### FIN DE TU LÓGICA DE COMANDOS ###

View File

@@ -4,152 +4,271 @@ ModulesStructureVersion=1
Type=Class
Version=4.19
@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
Private pool As ConnectionPool
Private DebugQueries As Boolean
Dim commands As Map
Public serverPort As Int
Public usePool As Boolean = True
Dim config As Map
Private pool As ConnectionPool ' Objeto principal para gestionar el pool de conexiones de la base de datos (usa C3P0 internamente).
Private DebugQueries As Boolean ' Bandera para activar/desactivar el modo de depuración de queries.
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 ' El puerto que el servidor HTTP usará, obtenido del archivo de configuración principal (config.properties).
Public usePool As Boolean = True ' Indica si se debe usar el pool de conexiones (siempre True en este diseño).
Dim config As Map ' Almacena la configuración completa (JdbcUrl, User, Password, etc.) cargada de su respectivo archivo .properties.
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)
' Log("RDCConnector Initialize")
If DB.EqualsIgnoreCase("DB1") Then DB = "" 'Esto para el config.properties por default
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
' 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 = ""
' PASO 1: Cargar la configuración desde el archivo .properties correspondiente.
' Es CRUCIAL que se asigne a la variable de CLASE 'config' (sin 'Dim' local)
' para que la configuración cargada del archivo sea persistente para esta instancia del conector.
config = LoadConfigMap(DB)
' Bloque Try-Catch para la inicialización y configuración del pool.
' Esto capturará cualquier error crítico que impida la conexión inicial a la base de datos.
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"))
Dim jo As JavaObject = pool ' Obtener la referencia JavaObject para acceder a métodos de configuración avanzados de C3P0.
' PASO 3: Aplicar *todas* las propiedades de configuración de C3P0 INMEDIATAMENTE.
' Esto debe ocurrir *después* de 'pool.Initialize' pero *antes* de que C3P0 intente realmente adquirir conexiones.
' Esto asegura que las configuraciones sean efectivas desde el primer intento de conexión.
' Lectura de los valores desde el archivo de configuración, con valores por defecto si no se encuentran.
Dim initialPoolSize As Int = config.GetDefault("InitialPoolSize", 3)
Dim minPoolSize As Int = config.GetDefault("MinPoolSize", 2)
Dim maxPoolSize As Int = config.GetDefault("MaxPoolSize", 5)
Dim acquireIncrement As Int = config.GetDefault("AcquireIncrement", 5)
' Leer valores del config.properties o usar valores por defecto
' Si el parámetro no se encuentra en el archivo .properties, se usará el segundo valor (el por defecto).
Dim initialPoolSize As Int = config.GetDefault("InitialPoolSize", 3) ' Por defecto 3
Dim minPoolSize As Int = config.GetDefault("MinPoolSize", 2) ' Por defecto 3
Dim maxPoolSize As Int = config.GetDefault("MaxPoolSize", 5) ' Por defecto 5
jo.RunMethod("setInitialPoolSize", Array(initialPoolSize)) ' Sets the inital pool size to 2
jo.RunMethod("setMinPoolSize", Array(minPoolSize)) ' Sets the min pool size to 2
jo.RunMethod("setMaxPoolSize", Array(maxPoolSize)) ' Max number of concurrent connections
' Define el tiempo máximo de inactividad en SEGUNDOS.
' 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.
' Después de 900 segundos (15 minutos) desde su creación, la conexión será marcada para ser
' eliminada y reemplazada por una nueva la próxima vez que regrese al pool.
' 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.
' Si todas las conexiones del pool están ocupadas, una nueva petición esperará hasta
' 60000 milisegundos (1 minuto). Si ninguna conexión se libera en ese lapso, la petición
' fallará con un error. Esto evita que la aplicación se congele bajo carga pesada.
jo.RunMethod("setCheckoutTimeout", Array As Object(60000))
' com.mchange.v2.c3p0.ComboPooledDataSource [
' acquireIncrement -> 3,
' acquireRetryAttempts -> 30,
' acquireRetryDelay -> 1000,
' autoCommitOnClose -> False,
' automaticTestTable -> Null,
' breakAfterAcquireFailure -> False,
' checkoutTimeout -> 20000,
' connectionCustomizerClassName -> Null,
' connectionTesterClassName -> com.mchange.v2.c3p0.impl.DefaultConnectionTester,
' contextClassLoaderSource -> caller,
' dataSourceName -> 2rvxvdb7cyxd8zlw6dyb|63021689,
' debugUnreturnedConnectionStackTraces -> False,
' description -> Null,
' driverClass -> oracle.jdbc.driver.OracleDriver,
' extensions -> {},
' factoryClassLocation -> Null,
' forceIgnoreUnresolvedTransactions -> False,
' forceSynchronousCheckins -> False,
' forceUseNamedDriverClass -> False,
' identityToken -> 2rvxvdb7cyxd8zlw6dyb|63021689,
' idleConnectionTestPeriod -> 600,
' initialPoolSize -> 3,
' jdbcUrl -> jdbc:oracle:thin:@//10.0.0.110:1521/DBKMT,
' maxAdministrativeTaskTime -> 0,
' maxConnectionAge -> 0,
' maxIdleTime -> 1800,
' maxIdleTimeExcessConnections -> 0,
' maxPoolSize -> 5,
' maxStatements -> 150,
' maxStatementsPerConnection -> 0,
' minPoolSize -> 3,
' numHelperThreads -> 3,
' preferredTestQuery -> DBMS_SESSION.SET_IDENTIFIER('whatever'),
' privilegeSpawnedThreads -> False,
' properties -> {password=******, user=******},
' propertyCycle -> 0,
' statementCacheNumDeferredCloseThreads -> 0,
' testConnectionOnCheckin -> False,
' testConnectionOnCheckout -> True,
' unreturnedConnectionTimeout -> 0,
' userOverrides -> {},
' 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,
' acquireRetryAttempts -> 30,
' acquireRetryDelay -> 1000,
' autoCommitOnClose -> False,
' automaticTestTable -> Null,
' breakAfterAcquireFailure -> False,
' checkoutTimeout -> 20000,
' connectionCustomizerClassName -> Null,
' connectionTesterClassName -> com.mchange.v2.c3p0.impl.DefaultConnectionTester,
' contextClassLoaderSource -> caller,
' dataSourceName -> 2rvxvdb7cyxd8zlw6dyb|63021689,
' debugUnreturnedConnectionStackTraces -> False,
' description -> Null,
' driverClass -> oracle.jdbc.driver.OracleDriver,
' extensions -> {},
' factoryClassLocation -> Null,
' forceIgnoreUnresolvedTransactions -> False,
' forceSynchronousCheckins -> False,
' forceUseNamedDriverClass -> False,
' identityToken -> 2rvxvdb7cyxd8zlw6dyb|63021689,
' idleConnectionTestPeriod -> 600,
' initialPoolSize -> 3,
' jdbcUrl -> jdbc:oracle:thin:@//10.0.0.110:1521/DBKMT,
' maxAdministrativeTaskTime -> 0,
' maxConnectionAge -> 0,
' maxIdleTime -> 1800,
' maxIdleTimeExcessConnections -> 0,
' maxPoolSize -> 5,
' maxStatements -> 150,
' maxStatementsPerConnection -> 0,
' minPoolSize -> 3,
' numHelperThreads -> 3,
' preferredTestQuery -> DBMS_SESSION.SET_IDENTIFIER('whatever'),
' privilegeSpawnedThreads -> False,
' properties -> {password=******, user=******},
' propertyCycle -> 0,
' statementCacheNumDeferredCloseThreads -> 0,
' testConnectionOnCheckin -> False,
' testConnectionOnCheckout -> True,
' unreturnedConnectionTimeout -> 0,
' userOverrides -> {},
' 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
' Configuración de depuración de queries. Se activa automáticamente si el proyecto se ejecuta en modo DEBUG.
#If DEBUG
' DebugQueries = True
#Else
DebugQueries = False
#End If
' Dim jo2 As JavaObject = pool
' Log(jo2.GetField("END_TO_END_CLIENTID_INDEX"))
' jo.RunMethod("setPreferredTestQuery", Array("BEGIN DBMS_SESSION.SET_IDENTIFIER('whatever'); END;"))
' jo.RunMethod("setPreferredTestQuery", Array("alter session set current_schema=MYSCHEMA"))
' 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
' Se obtiene el puerto del servidor HTTP desde la configuración de esta base de datos.
' Nota: En el diseño actual, el puerto principal lo define DB1 (config.properties).
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"
' Carga los comandos SQL predefinidos de esta base de datos en el mapa global 'commandsMap'.
LoadSQLCommands(config, DB)
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 DBX As String = ""
If DB <> "" Then DBX = "." & DB
Log("===========================================")
Log($"Leemos el config${DBX}.properties"$)
If DB <> "" Then DBX = "." & DB ' Construye el sufijo del nombre de archivo (ej. ".DB2").
Log($"Leemos el config${DBX}.properties"$) ' Mantenemos este log para confirmación de carga.
Return File.ReadMap("./", "config" & DBX & ".properties")
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
Log("==== GetCommand ====")
' Log("|" & DB & "|" & Key & "|")
commands = Main.commandsMap.get(DB).As(Map)
commands = Main.commandsMap.get(DB).As(Map) ' Obtiene los comandos de la DB específica del mapa global.
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
' Log(commands.ContainsKey("sql." & Key))
Log("========= Traemos """ & Key & """ ==========")
Log(">>>>>> " & commands.Get("sql." & Key) & " <<<<<<")
Return commands.Get("sql." & Key)
Return commands.Get("sql." & Key) ' Retorna la sentencia SQL.
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
Log("==== GetConnection ==== ")
If DB.EqualsIgnoreCase("DB1") Then DB = "" 'Esto para el config.properties or default
If DB.EqualsIgnoreCase("DB1") Then DB = ""
' 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)
Return pool.GetConnection
Return pool.GetConnection ' Retorna una conexión del pool.
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)
Log("==== LoadSQLCommands ==== ")
Log($"Cargamos los comandos desde el config.${DB}.properties"$)
Dim newCommands As Map
newCommands.Initialize
For Each k As String In config2.Keys
If k.StartsWith("sql.") Then
newCommands.Put(k, config2.Get(k))
If k.StartsWith("sql.") Then ' Busca claves que comiencen con "sql." (ej. "sql.select_user").
newCommands.Put(k, config2.Get(k)) ' Añade el comando al mapa.
End If
Next
commands = newCommands
' Log($"Inicializado: ${DB} "$ & Main.commandsMap.IsInitialized)
Main.commandsMap.Put(DB, commands)
commands = newCommands ' Actualiza el mapa de comandos de esta instancia de RDCConnector.
Main.commandsMap.Put(DB, commands) ' Almacena el mapa de comandos en el mapa global 'commandsMap' de Main.
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

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 ---

View File

@@ -3,6 +3,7 @@ ModuleBookmarks1=
ModuleBookmarks10=
ModuleBookmarks11=
ModuleBookmarks12=
ModuleBookmarks13=
ModuleBookmarks2=
ModuleBookmarks3=
ModuleBookmarks4=
@@ -16,6 +17,7 @@ ModuleBreakpoints1=
ModuleBreakpoints10=
ModuleBreakpoints11=
ModuleBreakpoints12=
ModuleBreakpoints13=
ModuleBreakpoints2=
ModuleBreakpoints3=
ModuleBreakpoints4=
@@ -29,6 +31,7 @@ ModuleClosedNodes1=
ModuleClosedNodes10=
ModuleClosedNodes11=
ModuleClosedNodes12=
ModuleClosedNodes13=
ModuleClosedNodes2=
ModuleClosedNodes3=
ModuleClosedNodes4=
@@ -37,6 +40,6 @@ ModuleClosedNodes6=
ModuleClosedNodes7=
ModuleClosedNodes8=
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
VisibleModules=2,3,11,9,12
VisibleModules=3,4,12,10,13,1