Commit inicial

This commit is contained in:
jaguerrau
2025-10-28 21:08:07 -06:00
parent 63cadcaf72
commit 82cd7c9a45
52 changed files with 5869 additions and 0 deletions

16
.gitattributes vendored Normal file
View File

@@ -0,0 +1,16 @@
# Auto detect text files and perform LF normalization
* text=auto
# linguist-language
*.b4a linguist-language=B4X
*.b4i linguist-language=B4X
*.b4j linguist-language=B4X
*.b4r linguist-language=B4X
*.bas linguist-language=B4X
# linguist-detectable
*.b4a linguist-detectable=true
*.b4i linguist-detectable=true
*.b4j linguist-detectable=true
*.b4r linguist-detectable=true
*.bas linguist-detectable=true

2
.gitignore vendored Normal file
View File

@@ -0,0 +1,2 @@
**/Objects
**/AutoBackups

235
Cambios.bas Normal file
View File

@@ -0,0 +1,235 @@
B4J=true
Group=Default Group
ModulesStructureVersion=1
Type=StaticCode
Version=10.3
@EndOfDesignText@
' ########################################
' ##### HISTORIAL DE CAMBIOS #####
' ########################################
Sub Process_Globals
' - VERSION X.XX.XX (cambios a implementar)
' - Agregar una forma de probar con carga el servidor.
' - Agregar algun tipo de autenticación, posiblemente con tokens ... se podria poner
' en el config.properties un token de conexion y solo las peticiones que lo incluyan sean atendidas, o
' guardar los tokens en la BD sqlite.
' - Si se implementan los tokens, se podrian ligar los tokens con el "*" de CORS, y los tokens definirian
' los dominios permitidos.
' - Ej: token:1224abcd5678fghi, dominio:"keymon.net"
' - Ej: token:4321abcd8765fghi, dominio:"*"
' - Que en el reporte de "Queries lentos" se pueda especificar de cuanto tiempo, ahorita esta solo de la ultima hora, pero que se pueda seleccionar desde una
' lista, por ejemplo 15, 30, 45 y 60 minutos antes.
' - VERSION: 5.10.25
' - refactor(hikari): Migración completa de C3P0 a HikariCP. Corrección de Hot-Swap y estabilización de la concurrencia.
' - El cambio principal es la sustitución del pool de conexiones C3P0 por HikariCP (versión 4.0.3). Esto resuelve problemas de estabilidad y reduce el overhead de sincronización, moviendo la infraestructura de pooling a un estándar industrial más robusto y rápido.
' - Los cambios funcionales y arquitectónicos más relevantes son:
' 1. Estabilización de Concurrencia (Segregación de Locks):
' 2. Monitoreo y Diagnóstico Fiable:
' ◦ Métricas de Carga: Se mueve la captura de métricas de BusyConnections y TotalConnections en DBHandlerB4X.bas y DBHandlerJSON.bas para que ocurra inmediatamente después de obtener la conexión del pool (con = Connector.GetConnection(...)). Esto asegura que las métricas registradas reflejen con precisión la carga real del pool en el instante de la petición.
' ◦ Configuración del Driver: El módulo RDCConnector ahora expone en el comando /getconfiginfo las propiedades específicas del driver (e.g., cachePrepStmts) que se aplicaron vía addDataSourceProperty.
' - VERSION 5.10.15
' - fix(concurrencia): Se implementa LogCacheLock en Main.bas para proteger exclusivamente las operaciones de caché de logs. El MainConnectorsLock queda reservado solo para la administración de conectores (como el reload). Esta segregación elimina un cuello de botella crítico donde la I/O pesada de SQLite bloqueaba la gestión de los pools de conexión.
' - Se corrige un problema arquitectónico donde el MainConnectorsLock creaba un cuello de botella para la concurrencia. Anteriormente, las operaciones pesadas de I/O (escritura de lotes a SQLite) y la administración de configuración compartían el mismo lock.
' - Se introduce el LogCacheLock para proteger exclusivamente la adición y el swap atómico de las cachés de logs en memoria (QueryLogCache/ErrorLogCache).
' - El MainConnectorsLock ahora se reserva únicamente para operaciones críticas de administración, como el Hot-Swap y la modificación atómica del estado de logging (SQLiteLoggingStatusByDB).
' - Esta segregación garantiza que las operaciones de monitoreo (SSE) y la recarga dinámica de bases de datos ya no se vean afectadas por la latencia de la escritura de logs a disco.
' - VERSION 5.10.10
' - feat(config): Implementa carga dinámica y escalable de bases de datos (ya no son solo 4), permite utilizar un numero "ilimitado" de bases de datos.
' - Hay que tomar en cuenta que el número "real" de bases de datos a conectar depende de los recursos del servidor.
' - * Detección e Inicialización Dinámica: Se refactoriza el proceso de arranque del servidor para que detecte y cargue automáticamente cualquier archivo de configuración que siga el patrón estándar (`config.XXX.properties`) en el directorio de la aplicación. Esto elimina las restricciones a un conteo fijo (como el límite anterior a 4 bases de datos).
' - * Asignación de `DBKey`: La clave de conexión (DBKey) se extrae del nombre del archivo de configuración (ej., el archivo `config.cliente_A.properties` se registra como el conector "CLIENTE_A"), permitiendo nombres descriptivos arbitrarios y escalables en lugar de un esquema numérico rígido.
'
' - VERSION 5.10.01
' - Se implemento el habilitar o deshabilitar los logs SIN modificar el config.properties y SIN reiniciar el servidor, el cambio no persiste un reinicio, solo es en "memoria".
'
' - VERSION 5.09.19
' - feat(sqlite): Implementa optimización de SQLite (WAL e Índices)
' - fix(manager): Extiende el comando 'test' para verificar todos los pools de conexión configurados, se conecta a cada uno de ellos y luego los libera.
' - Mejoras al subsistema de logs y diagnóstico del servidor jRDC2-Multi.
' - Cambios principales:
' 1. Optimización del Rendimiento de SQLite (users.db):
' * Habilitación de WAL: Se implementó PRAGMA journal_mode=WAL y PRAGMA synchronous=NORMAL en `InitializeSQLiteDatabase`. Esto reduce la contención de disco y mejora el rendimiento de I/O en las escrituras transaccionales de logs por lotes.
' * Índices de logs: Se agregaron índices a las columnas `timestamp` y `duration_ms` en `query_logs`, y a `timestamp` en `errores`. Esto acelera drásticamente las operaciones de limpieza periódica (`borraArribaDe15000Logs`) y la generación de reportes de consultas lentas (`slowqueries`).
' * Se cambio el cache de los logs de 350 a 400 registros.
' * Se cambio el query de limpieza y se aumento a 30,000 registros y cada 30 minutos.
' * Se cambio el codigo que dispara el "VACUUM" para que corra cada 24 hrs.
'
' 2. Mejora del Comando de Diagnóstico 'test':
' * Se corrigió el comando `manager?command=test` para que no solo pruebe la conexión de `DB1`, sino que itere sobre `Main.listaDeCP` y fuerce la adquisición y liberación de una conexión (`GetConnection`) en *todos* los `RDCConnector` configurados (DB1, DB2, DB3, etc.).
' * La nueva lógica garantiza una prueba de vida rigurosa de cada pool C3P0, devolviendo un mensaje detallado del estado de conectividad y registrando un error crítico vía `LogServerError` si algún pool no responde.
'
' - VERSION 5.09.18
' - feat(manager): Implementa recarga granular (Hot-Swap).
' - Actualiza manager.html para solicitar la DB Key a recargar (ej: DB2).
' - Se modifica Manager.bas para leer este parámetro y ejecutar el Hot-Swap de forma atómica solo en el pool de conexión especificado, lo cual mejora la eficiencia y la disponibilidad del servicio.
'
' - VERSION 5.09.17
' - fix(handlers, logs): Reporte robusto de AffectedRows (simbólico) y limpieza de tabla de errores
' - Aborda dos problemas críticos para la estabilidad y fiabilidad del servidor: el manejo del conteo de filas afectadas en DMLs y la gestión del crecimiento de la tabla de logs de errores.
' - Cambios Principales:
' 1. Fix AffectedRows (ExecuteBatch V1 y DBHandlerJSON): Dada la imposibilidad de capturar el conteo de filas afectadas real (Null) de forma segura o la falla total en tiempo de ejecución (Method: ExecNonQuery2 not matched) al usar reflexión, se revierte la lógica a la llamada directa de ExecNonQuery2. Si el comando DML se ejecuta sin lanzar una excepción SQL, se reporta simbólicamente '1' fila afectada al cliente (en el Protocolo V1 y en la respuesta JSON para executecommand) para confirmar el éxito de la operación.
' 2. Limpieza de Tabla de Errores: Se corrigió la subrutina Main.borraArribaDe15000Logs para incluir la tabla `errores` en la limpieza periódica. Esto asegura que el log de errores no crezca indefinidamente, manteniendo solo los 15,000 registros más recientes y realizando la optimización de espacio en disco con `vacuum`.
'
' - VERSION 5.09.16.2
' - feat(logs): Implementación de Cacheo y Escritura Transaccional en Lotes.
' - Implementa la funcionalidad de cacheo de logs en memoria y escritura transaccional para reducir el overhead de E/S de disco en SQLite.
' - Cambios principales:
' 1. Refactorización de LogQueryPerformance y LogServerError para que solo almacenen logs en las cachés globales (QueryLogCache y ErrorLogCache).
' 2. Introducción de WriteQueryLogsBatch y WriteErrorLogsBatch, que vacían las cachés y realizan la inserción a SQLite dentro de una única transacción atómica (`BeginTransaction`/`TransactionSuccessful`), disparada por umbral (`LOG_CACHE_THRESHOLD`) o periódicamente por `TimerLogs_Tick`.
' 3. Corrección del manejo de objetos List en las rutinas de lote (Write*LogsBatch): Se implementó la copia explícita de contenido (`List.AddAll`) dentro del bloqueo (`MainConnectorsLock`) para asegurar que el lote mantenga sus registros, resolviendo el problema de tamaño cero causado por la asignación de referencias.
'
' - VERSION 5.09.16.1
' - Detalle de Comandos Batch: Se modificó DBHandlerB4X.bas (ExecuteBatch V1 y ExecuteBatch2 V2) para que, en lotes de tamaño 1, el Log retorne el nombre específico del comando (queryName) en lugar del genérico "batch (size=1)". Esto asegura que el query_logs registre la query exacta junto a su dbKey.
' - Timestamp Legible en SQLite: Se añade la columna timestamp_text_local a la tabla query_logs (incluyendo la lógica de migración en Main.InitializeSQLiteDatabase) y se actualiza Main.LogQueryPerformance para calcular e insertar el tiempo en formato yyyy/mm/dd HH:mm:ss.sss. Esto permite la inspección directa de la base de datos Sin necesidad de conversiones, mejorando la usabilidad para el análisis de rendimiento.
'
' - Versión: 5.09.16
' - feat: Implementa control de logs de SQLite granular por DBKey y corrige la concurrencia del Timer en Hot-Swap.
' - Este commit introduce una mejora crucial en el rendimiento y la flexibilidad del servidor al permitir el control detallado del registro de logs en SQLite (users.db) por cada base de datos configurada (DB1, DB2, etc.).
' - Cambios Principales y Beneficios:
' 1. Control Granular de Logs: Se reemplazó el flag de control global de logs por un mapa (SQLiteLoggingStatusByDB), permitiendo al administrador deshabilitar el costoso proceso de escritura de query_logs y errores para bases de datos específicas mediante la propiedad enableSQLiteLogs en sus archivos .properties correspondientes.
' 2. Estabilización del Timer y Hot-Swap:
' ◦ Se corrigió un problema de concurrencia y estado asegurando que timerLogs se inicialice incondicionalmente, resolviendo el error IllegalStateException: Interval must be larger than 0 que ocurría durante el reload.
' ◦ El Timer de limpieza (borraArribaDe15000Logs y VACUUM) ahora se activa solo si al menos una base de datos tiene el logging habilitado (IsAnySQLiteLoggingEnabled), minimizando el overhead de E/S de disco cuando los logs no se requieren.
' 3. Recarga Dinámica de Estado: El comando manager?command=reload ahora lee la configuración enableSQLiteLogs de todos los conectores nuevos y actualiza atómicamente el estado global de logs, aplicando los cambios Sin requerir un reinicio del servidor.
'
' - VERSION 5.09.16
' feat: Implementa tolerancia de parámetros configurable y mejora estabilidad general del servidor.
' La tolerancia de parametros permite que si un query requiere 3 parametros y se mandan 4, NO mande un error,
' solo manda a la base de datos los parametros correctos y tira los extras, y guarda una "ADVERTENCIA" en el Log de errores.
'
' Este commit introduce la funcionalidad de `parameterTolerance` configurable y aborda varias mejoras críticas para la estabilidad y eficiencia del jRDC2-Multi.
' Principales cambios y beneficios:
' - **Tolerancia de Parámetros**: Añade la propiedad `parameterTolerance` en `config.properties` para controlar el manejo de parámetros de más. Cuando está habilitada, recorta los parámetros excesivos; si está deshabilitada (modo estricto, por defecto), genera un error, aumentando la robustez de la validación.
' - **Inicialización Multi-DB Confiable**: Corrige la lógica de inicialización en `Main.AppStart` para `RDCConnector` de DB3 y DB4, asegurando que cada base de datos tenga su propio *pool* de conexiones correctamente configurado.
' - **Optimización de Ejecución SQL**: Elimina llamadas duplicadas a `ExecQuery2` y `ExecNonQuery2` en `DBHandlerB4X.bas`, garantizando que solo los parámetros validados se utilicen y evitando ejecuciones redundantes en la base de datos.
' - **Refactorización y Limpieza**: Se eliminó la declaración duplicada de `ActiveRequestsCountByDB` en `Main.bas` y la subrutina `Handle0` obsoleta en `Manager.bas`, mejorando la claridad y mantenibilidad del código.
'
' - VERSION 5.09.15
' 1. **Nuevas Funcionalidades en el Panel de Administración (Manager):**
' * Se añadió el comando `slowqueries` al `Manager` para permitir la visualización de las 20 consultas más lentas registradas en la tabla `query_logs` de SQLite.
' * Se mejoró el comando `totalcon` en `Manager.bas` para mostrar estadísticas detalladas de *todos* los pools de conexión C3P0 configurados, obteniendo métricas en tiempo real (TotalConnections, BusyConnections, IdleConnections, etc.) de cada `RDCConnector`.
' * Beneficio: Mayor visibilidad y control proactivo sobre el rendimiento y el uso de recursos del servidor desde la interfaz de administración.
' 2. **Optimización de la Gestión de Logs (`query_logs`):**
' * Se implementó un `Public timerLogs As Timer` en `Main.bas`, que se inicializa en `AppStart` y ejecuta periódicamente (cada 10 minutos) la subrutina `borraArribaDe15000Logs`.
' * La subrutina `borraArribaDe15000Logs` recorta la tabla `query_logs` en `users.db` para mantener solo los 15,000 registros más recientes, y luego realiza un `vacuum` para optimizar el espacio en disco utilizado por la base de datos SQLite.
' * Beneficio: Prevención del crecimiento excesivo de la base de datos de logs de rendimiento, manteniendo un historial manejable y optimizando el uso del almacenamiento a largo plazo.
'
' - VERSION 5.09.14 (Ahora consolidado en 5.09.15)
' -feat: Implementación robusta de monitoreo de pool de conexiones y peticiones activas
' -Este commit resuelve problemas críticos en el monitoreo del pool de conexiones (C3P0) y el conteo de peticiones activas por base de datos, mejorando significativamente la visibilidad y fiabilidad del rendimiento del servidor jRDC2-Multi.
' -Problemas Identificados y Resueltos:
' 1. **Métricas de `BusyConnections` y `TotalConnections` inconsistentes o siempre en `0` en el `Manager` y `query_logs`:**
' * **Problema**: Anteriormente, la métrica `busy_connections` en `query_logs` a menudo reportaba `0` o no reflejaba el estado real. De manera similar, el panel de `Manager?command=totalcon` consistentemente mostraba `BusyConnections: 0` y `TotalConnections` estancadas en `InitialPoolSize`, a pesar de que Oracle sí reportaba conexiones activas. Esto generaba confusión sobre el uso real y la expansión del pool.
' * **Solución**: Se modificó la lógica en los *handlers* (`DBHandlerJSON.bas` y `DBHandlerB4X.bas`) para capturar la métrica `BusyConnections` directamente del pool de C3P0 **inmediatamente después de que el *handler* adquiere una conexión** (`con = Connector.GetConnection(finalDbKey)`). Este valor se pasa explícitamente a la subrutina `Main.LogQueryPerformance` para su registro en `query_logs` y para ser consumido por `Manager.bas` a través de `RDCConnector.GetPoolStats`. Esto garantiza que el valor registrado y reportado refleje con precisión el número de conexiones activas en el instante de su adquisición. Pruebas exhaustivas confirmaron que C3P0 sí reporta conexiones ocupadas y sí expande `TotalConnections` hasta `MaxPoolSize` cuando la demanda lo exige.
' 2. **Contador `handler_active_requests` no decrementaba correctamente:**
' * **Problema**: El contador de peticiones activas por base de datos (`GlobalParameters.ActiveRequestsCountByDB`) no mostraba un decremento consistente, resultando en un conteo que solo aumentaba o mostraba valores erráticos en los logs.
' * **Solución**:
' * Se aseguró la declaración `Public ActiveRequestsCountByDB As Map` en `GlobalParameters.bas`.
' * Se garantizó su inicialización como un `srvr.CreateThreadSafeMap` en `Main.AppStart` para un manejo concurrente seguro de los contadores.
' * En `DBHandlerJSON.bas`, la `dbKey` (obtenida del parámetro `dbx` del JSON) ahora se resuelve *antes* de incrementar el contador, asegurando que el incremento y el decremento se apliquen siempre a la misma clave de base de datos correcta.
' * Se implementó una coerción explícita a `Int` (`.As(Int)`) para todas las operaciones de lectura y escritura (`GetDefault`, `Put`) en `GlobalParameters.ActiveRequestsCountByDB`, resolviendo problemas de tipo que causaban inconsistencias y el fallo en el decremento.
' * La lógica de decremento en `Private Sub CleanupAndLog` (presente en ambos *handlers*) se hizo más robusta, verificando que el contador sea mayor que cero antes de decrementar para evitar valores negativos.
' -Beneficios de estos Cambios:
' * **Monitoreo Preciso y Fiable**: Las métricas `busy_connections` y `handler_active_requests` en `query_logs` y el panel `Manager` ahora son totalmente fiables, proporcionando una visión clara y en tiempo real del uso del pool de conexiones y la carga de peticiones activas por base de datos.
' * **Diagnóstico Mejorado**: La visibilidad interna del estado del pool de C3P0 durante las pruebas confirma que la configuración de `RDCConnector` es correcta y que el pool se expande y contrae según lo esperado por la demanda.
' * **Robustez del Código**: La gestión de contadores de peticiones activas es ahora consistente, thread-safe y a prueba de fallos de tipo, mejorando la estabilidad general del servidor bajo carga.
'
' - VERSION 5.09.13.3 (Ahora consolidado en 5.09.15)
' - 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 (Ahora consolidado en 5.09.15)
' - 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 (Ahora consolidado en 5.09.15)
' feat: Mejora la inicialización del pool de conexiones y el soporte multi-DB.
'
' **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, 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

52
ChangePassHandler.bas Normal file
View File

@@ -0,0 +1,52 @@
B4J=true
Group=Default Group
ModulesStructureVersion=1
Type=Class
Version=10.3
@EndOfDesignText@
'Class module: ChangePassHandler
Sub Class_Globals
Private bc As BCrypt
End Sub
Public Sub Initialize
bc.Initialize("BC")
End Sub
Public Sub Handle(req As ServletRequest, resp As ServletResponse)
Log("--- CHANGEPASSHANDLER FUE LLAMADO ---") ' <--- ¡PON ESTA LÍNEA AQUÍ!
If req.GetSession.GetAttribute2("user_is_authorized", False) = False Then
resp.SendRedirect("/login")
Return
End If
Dim currentUser As String = req.GetSession.GetAttribute("username")
Dim currentPass As String = req.GetParameter("current_password")
Dim newPass As String = req.GetParameter("new_password")
Dim confirmPass As String = req.GetParameter("confirm_password")
If newPass <> confirmPass Then
resp.Write("<script>alert('Error: La nueva contraseña no coincide con la confirmación.'); history.back();</script>")
Return
End If
Try
Dim storedHash As String = Main.SQL1.ExecQuerySingleResult2("SELECT password_hash FROM users WHERE username = ?", Array As String(currentUser))
Log("Valor de la BD (storedHash): " & storedHash)
If storedHash = Null Or bc.checkpw(currentPass, storedHash) = False Then ' <<--- CAMBIO CLAVE AQUÍ
resp.Write("<script>alert('Error: La contraseña actual es incorrecta.'); history.back();</script>")
Return
End If
' <<--- CORRECCIÓN 2: Usamos el método seguro y consistente con 'Main'.
Dim newHashedPass As String = bc.hashpw(newPass, bc.gensalt)
Main.SQL1.ExecNonQuery2("UPDATE users SET password_hash = ? WHERE username = ?", Array As Object(newHashedPass, currentUser))
resp.Write("<script>alert('Contraseña actualizada correctamente.'); window.location.href='/manager';</script>")
Catch
Log(LastException)
resp.Write("<script>alert('Error del servidor al intentar cambiar la contraseña.'); history.back();</script>")
End Try
End Sub

184
ConnectionPoolManager.bas Normal file
View File

@@ -0,0 +1,184 @@
B4J=true
Group=Default Group
ModulesStructureVersion=1
Type=Class
Version=6.01
@EndOfDesignText@
'Class module
'Author: Oliver Ackermann
'Created on: 2018/05/08
Sub Class_Globals
Private pools As Map
Private pool As Object 'Connection pool object
Private poolJO As JavaObject
Private standardConnection As Boolean
Private activePool As Boolean
Private defaultPoolType As String
End Sub
'Initialze ConnectionPoolWrapper
Public Sub Initialize
Reset
End Sub
Private Sub Reset
' Dim H2Pool As H2ConnectionPool
' H2Pool.Initialize
' Dim HSQLPool As HSQLDBConnectionPool
' HSQLPool.Initialize
Dim HikariPool As HikariConnectionPool
HikariPool.Initialize
' Dim C3P0Pool As C3P0ConnectionPool
' C3P0Pool.Initialize
' Dim TomcatPool As TomcatConnectionPool
' TomcatPool.Initialize
' Dim ViburPool As ViburConnectionPool
' ViburPool.Initialize
pools.Initialize
' pools.Put("H2", H2Pool)
' pools.Put("HSQLDB", HSQLPool)
pools.Put("Hikari", HikariPool)
' pools.Put("C3P0", C3P0Pool)
' pools.Put("Tomcat", TomcatPool)
' pools.Put("Vibur", ViburPool)
defaultPoolType = "Hikari" ' <-- Esto ya estaba bien, pero lo dejamos incondicionalmente [5]
standardConnection = False
activePool = False
End Sub
'Create a given pool type
' poolType - type of connection pool to create.
' driverClass - JDBC driver class.
' jdbcUrl - JDBC connection url.
' aUser / aPassword - Connection credentials.
' poolSize - Maximum size of connection pool.
Public Sub CreatePool(poolType As String, driverClass As String, jdbcUrl As String, aUser As String, aPassword As String, poolSize As Int)
'Check poolType
If pools.ContainsKey(poolType) = False Then
Log($"Warning: Connection pool type ${poolType} not supported"$)
If poolType = defaultPoolType Then
Log($" Error: Default pool type ${poolType} not supported."$)
Return
Else
Log($" Switching to default pool type ${defaultPoolType}"$)
If pools.ContainsKey(defaultPoolType) Then
poolType = defaultPoolType
Else
Log($" Error: Default pool type ${defaultPoolType} not supported."$)
Return
End If
End If
End If
Dim thePool As Object = pools.Get(poolType)
'Check jdbcUrl
If jdbcUrl.StartsWith("jdbc:") = False Then
Log($"Error: Invalid JDBC URL: ${jdbcUrl}"$)
Return
End If
If SubExists(thePool, "SupportUrl") And CallSub2(thePool, "SupportUrl", jdbcUrl) = False Then
Log($"Error: Unsupported JDBC URL: ${jdbcUrl}"$)
Return
End If
'Initialize pool
Dim options As Map
options.Initialize
options.Put("driver", driverClass)
options.Put("url", jdbcUrl)
options.Put("user", aUser)
options.Put("password", aPassword)
options.Put("poolsize", poolSize)
CallSub2(thePool, "CreatePool3", options)
'See if we can use own GetConnection without use of CallSub
If SubExists(thePool, "IsStandardConnection") And CallSub(thePool, "IsStandardConnection") Then
standardConnection = True
Log($"Info: ${poolType} supports getConnection JavaObject method."$)
poolJO = CallSub(thePool, "GetPoolJO")
Else
Log($"Info: ${poolType} does not support getConnection JavaObject Method."$)
Log($"Info: Checking if ${poolType} has alternative GetConnection method."$)
If SubExists(thePool, "GetConnection") = False Then
Log($"Error: ${poolType} has no GetConnection method."$)
Return
End If
End If
'Everthing looks good sofar
pool = thePool
activePool = True
End Sub
Public Sub SetProperties(properties As Map)
CallSub2(pool, "SetProperties", properties)
End Sub
'Retrieves a connection from the pool. Make sure to close the connection when you are done with it.
Public Sub GetConnection As SQL
Dim newSQL As SQL
If standardConnection Then
Dim jo As JavaObject = newSQL
jo.SetField("connection", poolJO.RunMethod("getConnection", Null))
Else
newSQL = CallSub(pool, "GetConnection")
End If
Return newSQL
End Sub
' Devuelve el wrapper del pool (ej. HikariConnectionPool) basado en el tipo.
Public Sub GetPool(poolType As String) As Object
If pools.ContainsKey(poolType) Then
Return pools.Get(poolType)
Else
' Si el poolType no está en el mapa, devolvemos Null.
Return Null
End If
End Sub
'Closes all unused pooled connections.
Public Sub ClosePool
Log("ConnectionManager ClosePool")
If activePool Then
Log("About to call Connection Pool's ClosePool")
CallSub(pool, "ClosePool")
Log("Returned from Connection Pool's ClosePool")
End If
Log("Re-initializing Connection Pool Manager")
Reset
Log("Done")
End Sub
' Cierra un pool específico identificado por su tipo (ej. "Hikari", "C3P0").
' Esto permite el cierre granular necesario para el Hot-Swap.
Public Sub ClosePoolByType(poolType As String)
Log($"ConnectionManager: Intentando cerrar pool de tipo ${poolType}"$)
If pools.ContainsKey(poolType) Then
Dim thePool As Object = pools.Get(poolType)
If thePool <> Null And SubExists(thePool, "ClosePool") Then
Log($"ConnectionManager: Delegando ClosePool a ${poolType} wrapper."$)
CallSub(thePool, "ClosePool")
' NOTA: Generalmente, mantenemos el wrapper en el mapa 'pools' (no lo removemos)
' porque la inicialización del Manager crea estos objetos una sola vez al inicio.
Else
Log($"ERROR: Wrapper de pool ${poolType} no disponible o no tiene método ClosePool."$)
End If
Else
Log($"ADVERTENCIA: Tipo de pool ${poolType} no registrado en el Manager."$)
End If
End Sub

772
DBHandlerB4X.bas Normal file
View File

@@ -0,0 +1,772 @@
B4J=true
Group=Default Group
ModulesStructureVersion=1
Type=Class
Version=10.3
@EndOfDesignText@
' Class module: DBHandlerB4X
' This generic handler is responsible for processing HTTP requests
' from B4A/B4i clients (using the DBRequestManager library).
' The database to use (DB1, DB2, etc.) is determined dynamically
' from the request URL.
' This version includes parameter validation and error handling.
Sub Class_Globals
' --- Class global variables ---
' The following section of constants and utilities is compiled conditionally
' only if the #if VERSION1 directive is active. This is to support
' an old version of the DBRequestManager communication protocol.
' #if VERSION1
' Constants to identify data types in custom serialization (V1 protocol).
Private const T_NULL = 0, T_STRING = 1, T_SHORT = 2, T_INT = 3, T_LONG = 4, T_FLOAT = 5 _
,T_DOUBLE = 6, T_BOOLEAN = 7, T_BLOB = 8 As Byte
' Utilities for converting between data types and byte arrays.
Private bc As ByteConverter
' Utility for compressing/decompressing data streams (used in V1).
Private cs As CompressedStreams
' #end if
' Map to convert JDBC date/time column types to Java method names
' to get the correct values from a ResultSet.
Private DateTimeMethods As Map
' Object that manages connections to a specific database's pool.
' This RDCConnector instance will be assigned in the Handle method based on the request's dbKey.
Private Connector As RDCConnector
End Sub
' Executes once when an instance of this class is created by the HTTP server.
Public Sub Initialize
' Initializes the map that associates JDBC date/time column type codes
' with the corresponding method names to read them correctly from a ResultSet.
DateTimeMethods = CreateMap(91: "getDate", 92: "getTime", 93: "getTimestamp")
End Sub
' Main method that handles each HTTP request arriving at this handler.
' req: The ServletRequest object containing incoming request information.
' resp: The ServletResponse object for building and sending the response to the client.
Sub Handle(req As ServletRequest, resp As ServletResponse)
' This section analyzes the request URL to determine which database
' (DB1, DB2, etc.) the request is for. For example, if the URL is "/DB2/query",
' the extracted 'dbKey' will be "DB2".
Dim URI As String = req.RequestURI
Dim dbKey As String ' Variable to store the database identifier.
If URI.Length > 1 And URI.StartsWith("/") Then
dbKey = URI.Substring(1) ' Remove the initial '/'.
If dbKey.Contains("/") Then
' If the URL has more segments (e.g., "/DB2/some_path"), take only the first segment as dbKey.
dbKey = dbKey.SubString2(0, dbKey.IndexOf("/"))
End If
Else
' If the URL is just "/", default to "DB1".
dbKey = "DB1"
End If
dbKey = dbKey.ToUpperCase ' Normalize the dbKey to uppercase for consistency.
' Check if the extracted dbKey corresponds to a database configured and loaded in Main.
If Main.Connectors.ContainsKey(dbKey) = False Then
Dim ErrorMsg As String = $"Invalid DB key specified in URL: '${dbKey}'. Valid keys are: ${Main.listaDeCP}"$
Log(ErrorMsg)
Main.LogServerError("ERROR", "DBHandlerB4X.Handle", ErrorMsg, dbKey, Null, req.RemoteAddress)
SendPlainTextError(resp, 400, ErrorMsg)
Return
End If
' Log("********************* " & dbKey & " ********************") ' Debug log to identify the database.
Dim start As Long = DateTime.Now ' Record the request start time to calculate duration.
' --- START: Active request count for this dbKey (Increment) ---
' This block increments a global counter that tracks how many requests are
' active for a specific database at any given time.
Dim currentActiveRequests As Int = GlobalParameters.ActiveRequestsCountByDB.GetDefault(dbKey, 0).As(Int)
GlobalParameters.ActiveRequestsCountByDB.Put(dbKey, currentActiveRequests + 1)
' requestsBeforeDecrement is the counter value right after this request increments it.
' This is the value that will be recorded in the 'query_logs' table.
Dim requestsBeforeDecrement As Int = currentActiveRequests + 1
' --- END: Active request count ---
' Variable declarations with scope throughout the sub to ensure final cleanup.
Dim q As String = "unknown_b4x_command" ' Command name for the log, with a default value.
Dim con As SQL ' The DB connection, will be initialized later.
Dim duration As Long ' The total request duration, calculated before logging.
Dim poolBusyConnectionsForLog As Int = 0 ' Contains the number of busy connections from the pool.
Try ' --- START: Try block wrapping the main Handler logic ---
Dim in As InputStream = req.InputStream ' Get the HTTP request's input stream.
Dim method As String = req.GetParameter("method") ' Get the 'method' parameter from the URL (e.g., "query2", "batch2").
Connector = Main.Connectors.Get(dbKey) ' Assign the RDCConnector instance for this dbKey.
con = Connector.GetConnection(dbKey) ' The DB connection is obtained here from the connection pool!
' This block captures the number of connections currently busy in the pool
' *after* this request has obtained its own.
If Connector.IsInitialized Then
Dim poolStats As Map = Connector.GetPoolStats
If poolStats.ContainsKey("BusyConnections") Then
poolBusyConnectionsForLog = poolStats.Get("BusyConnections").As(Int) ' We capture the value.
' If Main.logger Then Log($">>>>>>>>>> ${poolStats.Get("BusyConnections")} "$)
End If
End If
Dim cachedStatsB4X As Map = Main.LatestPoolStats.Get(dbKey).As(Map)
If cachedStatsB4X.IsInitialized Then
' 1. Update Busy Connections and Active Requests
cachedStatsB4X.Put("BusyConnections", poolBusyConnectionsForLog)
cachedStatsB4X.Put("HandlerActiveRequests", requestsBeforeDecrement)
' 2. Capture TotalConnections and IdleConnections (already available in poolStats)
If poolStats.ContainsKey("TotalConnections") Then
cachedStatsB4X.Put("TotalConnections", poolStats.Get("TotalConnections"))
End If
If poolStats.ContainsKey("IdleConnections") Then
cachedStatsB4X.Put("IdleConnections", poolStats.Get("IdleConnections"))
End If
' 3. Re-write the map to the global cache (it's Thread-Safe)
Main.LatestPoolStats.Put(dbKey, cachedStatsB4X)
End If
' Log("Metodo: " & method) ' Debug log to identify the request method.
' --- Logic to execute different command types based on the 'method' parameter ---
If method = "query2" Then
' Execute a single query using V2 protocol (B4XSerializator).
q = ExecuteQuery2(dbKey, con, in, resp)
If q = "error" Then ' If ExecuteQuery2 returned a validation error.
duration = DateTime.Now - start
CleanupAndLog(dbKey, "error_in_" & method, duration, req.RemoteAddress, requestsBeforeDecrement, poolBusyConnectionsForLog, con)
Return ' Early exit if there is an error.
End If
' #if VERSION1
' These branches are compiled only if #if VERSION1 is active (for old protocol).
Else if method = "query" Then
in = cs.WrapInputStream(in, "gzip") ' Decompress the input stream if it's V1 protocol.
q = ExecuteQuery(dbKey, con, in, resp)
If q = "error" Then
duration = DateTime.Now - start
CleanupAndLog(dbKey, "error_in_" & method, duration, req.RemoteAddress, requestsBeforeDecrement, poolBusyConnectionsForLog, con)
Return
End If
Else if method = "batch" Then
in = cs.WrapInputStream(in, "gzip") ' Decompress the input stream if it's V1 protocol.
q = ExecuteBatch(dbKey, con, in, resp)
If q = "error" Then
duration = DateTime.Now - start
CleanupAndLog(dbKey, "error_in_" & method, duration, req.RemoteAddress, requestsBeforeDecrement, poolBusyConnectionsForLog, con)
Return
End If
' #end if
Else if method = "batch2" Then
' Execute a batch of commands (INSERT, UPDATE, DELETE) using V2 protocol.
q = ExecuteBatch2(dbKey, con, in, resp)
If q = "error" Then
duration = DateTime.Now - start
CleanupAndLog(dbKey, "error_in_" & method, duration, req.RemoteAddress, requestsBeforeDecrement, poolBusyConnectionsForLog, con)
Return ' Early exit if there is an error.
End If
Else
Dim ErrorMsg As String = "Unknown method: " & method
Log(ErrorMsg)
Main.LogServerError("ERROR", "DBHandlerB4X.Handle", ErrorMsg, dbKey, method, req.RemoteAddress)
SendPlainTextError(resp, 500, "unknown method")
q = "unknown_method_handler"
duration = DateTime.Now - start
CleanupAndLog(dbKey, q, duration, req.RemoteAddress, requestsBeforeDecrement, poolBusyConnectionsForLog, con)
Return
End If
Catch ' --- CATCH: Handle general execution or SQL errors ---
Dim errorMessage As String = LastException.Message
Private source As String = "DBHandlerB4X.Handle"
If errorMessage.Contains("ORA-01002") Or errorMessage.Contains("recuperación fuera de secuencia") Then
errorMessage = "SE USA EXECUTEQUERY EN LUGAR DE EXECUTECOMMAND: " & errorMessage
source = "EXECUTEQUERY EN LUGAR DE EXECUTECOMMAND"
else If errorMessage.Contains("ORA-17003") Or errorMessage.Contains("Índice de columnas no válido") Then
errorMessage = "NUMERO DE PARAMETROS EQUIVOCADO: " & errorMessage
source = "NUMERO DE PARAMETROS EQUIVOCADO"
End If
' Log(errorMessage) ' Log the full exception.
Main.LogServerError("ERROR", source, errorMessage, dbKey, q, req.RemoteAddress)
SendPlainTextError(resp, 500, errorMessage) ' Send a 500 error to the client.
q = "error_in_b4x_handler" ' We ensure a value for 'q' in case of an exception.
If source <> "DBHandlerB4X.Handle" Then q = source
End Try ' --- END: Main Try block ---
' --- Final logging and cleanup logic (for normal execution paths or after Catch) ---
' This block ensures that, regardless of how the request ends (success or error),
' the duration is calculated and the cleanup and logging subs are called.
duration = DateTime.Now - start ' Calculate the total request duration.
' Log($"${dbKey} - Command: ${q}, took: ${duration}ms, client=${req.RemoteAddress}"$) ' Log the command and duration.
' Call the centralized subroutine to log performance and clean up resources.
CleanupAndLog(dbKey, q, duration, req.RemoteAddress, requestsBeforeDecrement, poolBusyConnectionsForLog, con)
End Sub
' --- Subroutine: Centralizes performance logging and resource cleanup ---
' This subroutine is called by Handle at all exit points, ensuring
' that counters are decremented and connections are closed consistently.
Private Sub CleanupAndLog(dbKey As String, qName As String, durMs As Long, clientIp As String, handlerReqs As Int, poolBusyConns As Int, conn As SQL)
' 1. Call the centralized subroutine in Main to log performance to SQLite.
Main.LogQueryPerformance(qName, durMs, dbKey, clientIp, handlerReqs, poolBusyConns)
' 2. Robustly decrement the active request counter for this dbKey.
Dim currentCount As Int = GlobalParameters.ActiveRequestsCountByDB.GetDefault(dbKey, 0).As(Int)
If currentCount > 0 Then
' If the counter is positive, decrement it.
GlobalParameters.ActiveRequestsCountByDB.Put(dbKey, currentCount - 1)
Else
' If the counter is already 0 or negative (which shouldn't happen with current logic,
' but is handled for robustness), we log a warning and ensure it is 0.
' Log($"ADVERTENCIA: Intento de decrementar ActiveRequestsCountByDB para ${dbKey} que ya estaba en ${currentCount}. Asegurando a 0."$)
GlobalParameters.ActiveRequestsCountByDB.Put(dbKey, 0)
End If
' 3. Ensure the DB connection is always closed and returned to the connection pool.
If conn <> Null And conn.IsInitialized Then conn.Close
End Sub
' --- Subroutines for handling query and batch execution (V2 Protocol) ---
' Executes a single query using V2 protocol (B4XSerializator).
' DB: Database identifier.
' con: The SQL connection obtained from the pool.
' in: Request InputStream.
' resp: ServletResponse to send the response.
' Returns the executed command name or "error" if it failed.
Private Sub ExecuteQuery2 (DB As String, con As SQL, in As InputStream, resp As ServletResponse) As String
Dim ser As B4XSerializator ' Object to deserialize data sent from the client.
' Convert the input stream to a byte array and then to a Map object.
Dim m As Map = ser.ConvertBytesToObject(Bit.InputStreamToBytes(in))
' Extract the DBCommand object (query name and its parameters) from the map.
Dim cmd As DBCommand = m.Get("command")
' Extract the row limit to return (for pagination).
Dim limit As Int = m.Get("limit")
' Get the SQL statement corresponding to the command name from config.properties.
Dim sqlCommand As String = Connector.GetCommand(DB, cmd.Name)
' <<< START VALIDATION: CHECK IF COMMAND EXISTS >>>
' Check if the command was not found in the configuration file.
If sqlCommand = Null Or sqlCommand = "null" Or sqlCommand.Trim = "" Then
Dim errorMessage As String = $"El comando '${cmd.Name}' no fue encontrado en el config.properties de '${DB}'."$
Log(errorMessage)
Main.LogServerError("ERROR", "DBHandlerB4X.ExecuteQuery2", errorMessage, DB, cmd.Name, Null)
' Send a 400 (Bad Request) error to the client informing them of the problem.
SendPlainTextError(resp, 400, errorMessage)
Return "error" ' Return a string for the log.
End If
' <<< END VALIDATION >>>
' <<< START CENTRALIZED PARAMETER VALIDATION >>>
' Convert the cmd.Parameters Object() array to a List for the validation utility.
Dim paramsAsList As List
paramsAsList.Initialize
If cmd.Parameters <> Null Then
For Each p As Object In cmd.Parameters
paramsAsList.Add(p)
Next
End If
Dim validationResult As ParameterValidationResult = ParameterValidationUtils.ValidateAndAdjustParameters(cmd.Name, DB, sqlCommand, paramsAsList, Connector.IsParameterToleranceEnabled)
If validationResult.Success = False Then
SendPlainTextError(resp, 400, validationResult.ErrorMessage)
Return "error" ' Early exit if validation fails.
End If
' Execute the SQL query with the validated parameter list.
Dim rs As ResultSet = con.ExecQuery2(sqlCommand, validationResult.ParamsToExecute)
' <<< END CENTRALIZED PARAMETER VALIDATION >>>
' If the limit is 0 or negative, set it to a very high value (max int).
If limit <= 0 Then limit = 0x7fffffff 'max int
' Get the underlying Java object from the ResultSet to access additional methods.
Dim jrs As JavaObject = rs
' Get the ResultSet metadata (information about the columns).
Dim rsmd As JavaObject = jrs.RunMethod("getMetaData", Null)
' Get the number of columns in the result.
Dim cols As Int = rs.ColumnCount
Dim res As DBResult ' Create a DBResult object to package the response.
res.Initialize
res.columns.Initialize
res.Tag = Null
' Fill the column map with each column's name and its index.
For i = 0 To cols - 1
res.columns.Put(rs.GetColumnName(i), i)
Next
' Initialize the row list.
res.Rows.Initialize
' Iterate over each row in the ResultSet, up to the limit.
Do While rs.NextRow And limit > 0
Dim row(cols) As Object
' Iterate over each column in the current row.
For i = 0 To cols - 1
' Get the column data type according to JDBC.
Dim ct As Int = rsmd.RunMethod("getColumnType", Array(i + 1))
' Handle different data types to read them correctly.
If ct = -2 Or ct = 2004 Or ct = -3 Or ct = -4 Then ' BLOB/binary types
row(i) = rs.GetBlob2(i)
Else If ct = 2005 Then ' CLOB type (long text)
row(i) = rs.GetString2(i)
Else if ct = 2 Or ct = 3 Then ' Numeric types that can have decimals
row(i) = rs.GetDouble2(i)
Else If DateTimeMethods.ContainsKey(ct) Then ' Date/Time types
' Get the Java time/date object.
Dim SQLTime As JavaObject = jrs.RunMethodJO(DateTimeMethods.Get(ct), Array(i + 1))
If SQLTime.IsInitialized Then
' Convert it to milliseconds (Long) for B4X.
row(i) = SQLTime.RunMethod("getTime", Null)
Else
row(i) = Null
End If
Else ' For all other data types
' Use getObject which works for most standard types.
row(i) = jrs.RunMethod("getObject", Array(i + 1))
End If
Next
' Add the complete row to the results list.
res.Rows.Add(row)
limit = limit - 1
Loop
' Close the ResultSet to free resources.
rs.Close
' Serialize the complete DBResult object to a byte array.
Dim data() As Byte = ser.ConvertObjectToBytes(res)
' Write the serialized data to the response stream.
resp.OutputStream.WriteBytes(data, 0, data.Length)
' Return the command name for the log.
Return "query: " & cmd.Name
End Sub
' Executes a batch of commands (INSERT, UPDATE, DELETE) using V2 protocol.
' DB: Database identifier.
' con: The SQL connection obtained from the pool.
' in: Request InputStream.
' resp: ServletResponse to send the response.
' Returns a summary of the batch for the log, or "error" if it failed.
Private Sub ExecuteBatch2(DB As String, con As SQL, in As InputStream, resp As ServletResponse) As String
Dim ser As B4XSerializator
' Deserialize the map containing the list of commands.
Dim m As Map = ser.ConvertBytesToObject(Bit.InputStreamToBytes(in))
' Get the list of DBCommand objects.
Dim commands As List = m.Get("commands")
Dim totalAffectedRows As Int = 0 ' Counter to accumulate the total affected rows.
' Prepare a DBResult object for the response (for batch, it doesn't return data, only confirmation).
Dim res As DBResult
res.Initialize
res.columns = CreateMap("AffectedRows": 0) ' Symbolic column.
res.Rows.Initialize
res.Tag = Null
Try
' Start a transaction. All commands in the batch will be executed as one unit.
con.BeginTransaction
' Iterate over each command in the list.
For Each cmd As DBCommand In commands
' Get the SQL statement for the current command.
Dim sqlCommand As String = Connector.GetCommand(DB, cmd.Name)
' <<< START VALIDATION: CHECK IF COMMAND EXISTS WITHIN BATCH >>>
If sqlCommand = Null Or sqlCommand = "null" Or sqlCommand.Trim = "" Then
con.Rollback ' Rollback the transaction if a command is invalid.
Dim errorMessage As String = $"El comando '${cmd.Name}' no fue encontrado en el config.properties de '${DB}'."$
Log(errorMessage)
Main.LogServerError("ERROR", "DBHandlerB4X.ExecuteBatch2", errorMessage, DB, cmd.Name, Null)
SendPlainTextError(resp, 400, errorMessage)
Return "error"
End If
' <<< END VALIDATION >>>
' <<< START CENTRALIZED PARAMETER VALIDATION WITHIN BATCH >>>
' Convert the cmd.Parameters Object() array to a List for the validation utility.
Dim paramsAsList As List
paramsAsList.Initialize
If cmd.Parameters <> Null Then
For Each p As Object In cmd.Parameters
paramsAsList.Add(p)
Next
End If
Dim validationResult As ParameterValidationResult = ParameterValidationUtils.ValidateAndAdjustParameters(cmd.Name, DB, sqlCommand, paramsAsList, Connector.IsParameterToleranceEnabled)
If validationResult.Success = False Then
con.Rollback ' Important: rollback if validation fails inside a transaction!
SendPlainTextError(resp, 400, validationResult.ErrorMessage)
Return "error" ' Early exit if validation fails.
End If
con.ExecNonQuery2(sqlCommand, validationResult.ParamsToExecute) ' Execute the command with the validated parameter list.
totalAffectedRows = totalAffectedRows + 1 ' We accumulate 1 for each command executed without error.
' <<< END CENTRALIZED PARAMETER VALIDATION WITHIN BATCH >>>
Next
res.Rows.Add(Array As Object(totalAffectedRows)) ' Add a symbolic row to the result to indicate success.
con.TransactionSuccessful ' If all commands executed without error, commit the transaction.
Catch
' If any command fails, the error is caught.
con.Rollback ' All changes made in the transaction are undone.
Log(LastException) ' Log the exception.
Main.LogServerError("ERROR", "DBHandlerB4X.ExecuteBatch2", LastException.Message, DB, "batch_execution_error", Null)
SendPlainTextError(resp, 500, LastException.Message) ' Send a 500 error to the client.
End Try
' Serialize and send the response to the client.
Dim data() As Byte = ser.ConvertObjectToBytes(res)
resp.OutputStream.WriteBytes(data, 0, data.Length)
' Return a summary for the log, including the query name if it's a batch of size 1.
If commands.Size = 1 Then
' Get the only command in the batch.
Dim cmd As DBCommand = commands.Get(0)
Return $"batch (size=1) - query: ${cmd.Name}"$
Else
' If the batch size is > 1, keep the summary by size.
Return $"batch (size=${commands.Size})"$
End If
End Sub
' --- Subroutines for handling query and batch execution (V1 Protocol - Conditional Compilation) ---
' This code is compiled only if #if VERSION1 is active, to maintain compatibility with old clients.
'#if VERSION1
' Executes a batch of commands using V1 protocol.
Private Sub ExecuteBatch(DB As String, con As SQL, in As InputStream, resp As ServletResponse) As String
' Log($"ExecuteBatch ${DB}"$)
' Read and discard the client version.
Dim clientVersion As Float = ReadObject(in) 'ignore
' Read how many commands are in the batch.
Dim numberOfStatements As Int = ReadInt(in)
Dim res(numberOfStatements) As Int ' Array for results (although not used).
Dim singleQueryName As String = ""
Dim affectedCounts As List
Dim totalAffectedRows As Int
affectedCounts.Initialize
Try
con.BeginTransaction
' Iterate to process each command in the batch.
' Log(numberOfStatements)
For i = 0 To numberOfStatements - 1
' Log($"i: ${i}"$)
' Read the command name and parameter list using the V1 deserializer.
Dim queryName As String = ReadObject(in)
Dim params As List = ReadList(in)
' Log(params)
If numberOfStatements = 1 Then
singleQueryName = queryName 'Capture the query name.
End If
Dim sqlCommand As String = Connector.GetCommand(DB, queryName)
' Log(sqlCommand)
' <<< START VALIDATION: CHECK IF COMMAND EXISTS (V1) >>>
If sqlCommand = Null Or sqlCommand = "null" Or sqlCommand.Trim = "" Then
con.Rollback ' Rollback the transaction if a command is invalid.
Dim errorMessage As String = $"El comando '${queryName}' no fue encontrado en el config.properties de '${DB}'."$
Log(errorMessage)
Main.LogServerError("ERROR", "DBHandlerB4X.ExecuteBatch (V1)", errorMessage, DB, queryName, Null)
SendPlainTextError(resp, 400, errorMessage)
Return "error"
End If
' <<< END VALIDATION >>>
' <<< START CENTRALIZED PARAMETER VALIDATION WITHIN BATCH (V1) >>>
Dim validationResult As ParameterValidationResult = ParameterValidationUtils.ValidateAndAdjustParameters(queryName, DB, sqlCommand, params, Connector.IsParameterToleranceEnabled)
If validationResult.Success = False Then
con.Rollback ' Important: rollback if validation fails inside a transaction!
SendPlainTextError(resp, 400, validationResult.ErrorMessage)
Return "error" ' Early exit if validation fails.
End If
' Log(validationResult.ParamsToExecute)
Dim affectedCount As Int = 1 ' Assume success (1) since the direct call is the only one that executes SQL without failing at runtime.
con.ExecNonQuery2(sqlCommand, validationResult.ParamsToExecute) ' Execute the command with the validated parameter list.
' <<< END CENTRALIZED PARAMETER VALIDATION WITHIN BATCH (V1) >>>
affectedCounts.Add(affectedCount) ' Add the result (1) to the V1 response list
totalAffectedRows = totalAffectedRows + affectedCount ' Accumulate the total for the log (even if it's symbolic 1)
Next
con.TransactionSuccessful ' Commit the transaction.
' Log("Transaction succesfull")
Dim out As OutputStream = cs.WrapOutputStream(resp.OutputStream, "gzip") ' Compress the output before sending it.
' Write the response using the V1 serializer.
WriteObject(Main.VERSION, out)
WriteObject("batch", out)
WriteInt(res.Length, out)
For Each r As Int In affectedCounts
WriteInt(r, out)
Next
out.Close
Catch
con.Rollback
Log(LastException)
Main.LogServerError("ERROR", "DBHandlerB4X.ExecuteBatch (V1)", LastException.Message, DB, "batch_execution_error_v1", Null)
SendPlainTextError(resp, 500, LastException.Message)
End Try
' Return $"batch (size=${numberOfStatements})"$
If numberOfStatements = 1 And singleQueryName <> "" Then
Return $"batch (size=1) - query: ${singleQueryName}"$
Else
Return $"batch (size=${numberOfStatements})"$
End If
End Sub
' Executes a single query using V1 protocol.
Private Sub ExecuteQuery(DB As String, con As SQL, in As InputStream, resp As ServletResponse) As String
' Log("====================== ExecuteQuery =====================")
' Deserialize the request data using V1 protocol.
Dim clientVersion As Float = ReadObject(in) 'ignore
Dim queryName As String = ReadObject(in)
Dim limit As Int = ReadInt(in)
Dim params As List = ReadList(in)
' Get the SQL statement.
Dim theSql As String = Connector.GetCommand(DB, queryName)
' <<< START VALIDATION: CHECK IF COMMAND EXISTS (V1) >>>
If theSql = Null Or theSql ="null" Or theSql.Trim = "" Then
Dim errorMessage As String = $"El comando '${queryName}' no fue encontrado en el config.properties de '${DB}'."$
Log(errorMessage)
Main.LogServerError("ERROR", "DBHandlerB4X.ExecuteQuery (V1)", errorMessage, DB, queryName, Null)
SendPlainTextError(resp, 400, errorMessage)
Return "error"
End If
' <<< END VALIDATION >>>
' <<< START CENTRALIZED PARAMETER VALIDATION (V1) >>>
Dim validationResult As ParameterValidationResult = ParameterValidationUtils.ValidateAndAdjustParameters(queryName, DB, theSql, params, Connector.IsParameterToleranceEnabled)
If validationResult.Success = False Then
SendPlainTextError(resp, 400, validationResult.ErrorMessage)
Return "error" ' Early exit if validation fails.
End If
' Execute the query with the validated parameter list.
Dim rs As ResultSet = con.ExecQuery2(theSql, validationResult.ParamsToExecute)
' <<< END CENTRALIZED PARAMETER VALIDATION (V1) >>>
If limit <= 0 Then limit = 0x7fffffff 'max int
Dim jrs As JavaObject = rs
Dim rsmd As JavaObject = jrs.RunMethod("getMetaData", Null)
Dim cols As Int = rs.ColumnCount
Dim out As OutputStream = cs.WrapOutputStream(resp.OutputStream, "gzip") ' Compress the output stream.
' Write the V1 response header.
WriteObject(Main.VERSION, out)
WriteObject("query", out)
WriteInt(rs.ColumnCount, out)
' Write the column names.
For i = 0 To cols - 1
WriteObject(rs.GetColumnName(i), out)
Next
' Iterate over the result rows.
Do While rs.NextRow And limit > 0
WriteByte(1, out) ' Write a '1' byte to indicate a row is coming.
' Iterate over the row's columns.
For i = 0 To cols - 1
Dim ct As Int = rsmd.RunMethod("getColumnType", Array(i + 1))
' Handle binary data types specially.
If ct = -2 Or ct = 2004 Or ct = -3 Or ct = -4 Then
WriteObject(rs.GetBlob2(i), out)
Else
' Write the column value.
WriteObject(jrs.RunMethod("getObject", Array(i + 1)), out)
End If
Next
limit = limit - 1
Loop
' Write a '0' byte to indicate the end of rows.
WriteByte(0, out)
out.Close
rs.Close
Return "query: " & queryName
End Sub
' Writes a single byte to the output stream.
Private Sub WriteByte(value As Byte, out As OutputStream)
out.WriteBytes(Array As Byte(value), 0, 1)
End Sub
' Main serializer for V1 protocol. Writes an object to the stream.
Private Sub WriteObject(o As Object, out As OutputStream)
Dim data() As Byte
' Write a type byte followed by the data.
If o = Null Then
out.WriteBytes(Array As Byte(T_NULL), 0, 1)
Else If o Is Short Then
out.WriteBytes(Array As Byte(T_SHORT), 0, 1)
data = bc.ShortsToBytes(Array As Short(o))
Else If o Is Int Then
out.WriteBytes(Array As Byte(T_INT), 0, 1)
data = bc.IntsToBytes(Array As Int(o))
Else If o Is Float Then
out.WriteBytes(Array As Byte(T_FLOAT), 0, 1)
data = bc.FloatsToBytes(Array As Float(o))
Else If o Is Double Then
out.WriteBytes(Array As Byte(T_DOUBLE), 0, 1)
data = bc.DoublesToBytes(Array As Double(o))
Else If o Is Long Then
out.WriteBytes(Array As Byte(T_LONG), 0, 1)
data = bc.LongsToBytes(Array As Long(o))
Else If o Is Boolean Then
out.WriteBytes(Array As Byte(T_BOOLEAN), 0, 1)
Dim b As Boolean = o
Dim data(1) As Byte
If b Then data(0) = 1 Else data(0) = 0
Else If GetType(o) = "[B" Then ' If the object is a byte array (BLOB)
data = o
out.WriteBytes(Array As Byte(T_BLOB), 0, 1)
' Write the data length before the data itself.
WriteInt(data.Length, out)
Else ' Treat everything else as a String
out.WriteBytes(Array As Byte(T_STRING), 0, 1)
data = bc.StringToBytes(o, "UTF8")
' Write the string length before the string.
WriteInt(data.Length, out)
End If
' Write the data bytes.
If data.Length > 0 Then out.WriteBytes(data, 0, data.Length)
End Sub
' Main deserializer for V1 protocol. Reads an object from the stream.
Private Sub ReadObject(In As InputStream) As Object
' Read the first byte to determine the data type.
Dim data(1) As Byte
In.ReadBytes(data, 0, 1)
Select data(0)
Case T_NULL
Return Null
Case T_SHORT
Dim data(2) As Byte
Return bc.ShortsFromBytes(ReadBytesFully(In, data, data.Length))(0)
Case T_INT
Dim data(4) As Byte
Return bc.IntsFromBytes(ReadBytesFully(In, data, data.Length))(0)
Case T_LONG
Dim data(8) As Byte
Return bc.LongsFromBytes(ReadBytesFully(In, data, data.Length))(0)
Case T_FLOAT
Dim data(4) As Byte
Return bc.FloatsFromBytes(ReadBytesFully(In, data, data.Length))(0)
Case T_DOUBLE
Dim data(8) As Byte
Return bc.DoublesFromBytes(ReadBytesFully(In, data, data.Length))(0)
Case T_BOOLEAN
Dim b As Byte = ReadByte(In)
Return b = 1
Case T_BLOB
' Read the length, then read that amount of bytes.
Dim len As Int = ReadInt(In)
Dim data(len) As Byte
Return ReadBytesFully(In, data, data.Length)
Case Else ' T_STRING
' Read the length, then read that amount of bytes and convert to string.
Dim len As Int = ReadInt(In)
Dim data(len) As Byte
ReadBytesFully(In, data, data.Length)
Return BytesToString(data, 0, data.Length, "UTF8")
End Select
End Sub
' Ensures that exactly the requested amount of bytes is read from the stream.
Private Sub ReadBytesFully(In As InputStream, Data() As Byte, Len As Int) As Byte()
Dim count = 0, Read As Int
' Keep reading in a loop to fill the buffer, in case data arrives in chunks.
Do While count < Len And Read > -1
Read = In.ReadBytes(Data, count, Len - count)
count = count + Read
Loop
Return Data
End Sub
' Writes an integer (4 bytes) to the stream.
Private Sub WriteInt(i As Int, out As OutputStream)
Dim data() As Byte
data = bc.IntsToBytes(Array As Int(i))
out.WriteBytes(data, 0, data.Length)
End Sub
' Reads an integer (4 bytes) from the stream.
Private Sub ReadInt(In As InputStream) As Int
Dim data(4) As Byte
Return bc.IntsFromBytes(ReadBytesFully(In, data, data.Length))(0)
End Sub
' Reads a single byte from the stream.
Private Sub ReadByte(In As InputStream) As Byte
Dim data(1) As Byte
In.ReadBytes(data, 0, 1)
Return data(0)
End Sub
' Reads a list of objects from the stream (V1 protocol).
Private Sub ReadList(in As InputStream) As List
' First, read the number of elements in the list.
Dim len As Int = ReadInt(in)
Dim l1 As List
l1.Initialize
' Then, read each object one by one and add it to the list.
For i = 0 To len - 1
l1.Add(ReadObject(in))
Next
Return l1
End Sub
'#end If ' End of conditional compilation block for VERSION1
' Sends an error response in plain text format.
' This avoids the default HTML error page generated by resp.SendError.
' resp: The ServletResponse object to send the response.
' statusCode: The HTTP status code (e.g., 400 for Bad Request, 500 for Internal Server Error).
' errorMessage: The error message to be sent to the client.
' In B4X clients, an HTML or JSON response is not ideal. The IDE displays the full error text, and plain text is much easier to read than HTML or JSON.
Private Sub SendPlainTextError(resp As ServletResponse, statusCode As Int, errorMessage As String)
Try
' Set the HTTP status code (e.g., 400, 500).
resp.Status = statusCode
' Set the content type to plain text, with UTF-8 encoding to support accents.
resp.ContentType = "text/plain; charset=utf-8"
' Get the response OutputStream to write data directly.
Dim out As OutputStream = resp.OutputStream
' Convert the error message to a byte array using UTF-8.
Dim data() As Byte = errorMessage.GetBytes("UTF8")
' Write the bytes to the output stream.
out.WriteBytes(data, 0, data.Length)
' Close the stream to ensure all data is sent correctly.
out.Close
Catch
' If something fails while trying to send the error response, log it
' so the original cause of the problem is not lost.
Dim ErrorMsg As String = "Error sending plain text error response: " & LastException
Log(ErrorMsg)
Main.LogServerError("ERROR", "DBHandlerB4X.SendPlainTextError", ErrorMsg, Null, Null, Null)
End Try
End Sub

311
DBHandlerJSON.bas Normal file
View File

@@ -0,0 +1,311 @@
B4J=true
Group=Default Group
ModulesStructureVersion=1
Type=Class
Version=10.3
@EndOfDesignText@
' Módulo de clase: DBHandlerJSON
' Este handler se encarga de procesar las peticiones HTTP que esperan o envían datos en formato JSON.
' Es ideal para clientes web (JavaScript, axios, etc.) o servicios que interactúan con el servidor
' mediante un API RESTful. Soporta tanto GET con JSON en un parámetro 'j' como POST con JSON
' en el cuerpo de la petición.
Sub Class_Globals
' Declara una variable privada para mantener una instancia del conector RDC.
' Este objeto maneja la comunicación con la base de datos específica de la petición.
Private Connector As RDCConnector
End Sub
' Subrutina de inicialización de la clase. Se llama cuando se crea un objeto de esta clase.
Public Sub Initialize
' No se requiere inicialización específica para esta clase en este momento.
End Sub
' 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)
' --- Headers CORS (Cross-Origin Resource Sharing) ---
' Estos encabezados son esenciales para permitir que aplicaciones web (clientes)
' alojadas en diferentes dominios puedan comunicarse con este servidor.
resp.SetHeader("Access-Control-Allow-Origin", "*") ' Permite peticiones desde cualquier origen.
resp.SetHeader("Access-Control-Allow-Methods", "GET, POST, OPTIONS") ' Métodos HTTP permitidos.
resp.SetHeader("Access-Control-Allow-Headers", "Content-Type") ' Encabezados permitidos.
' Las peticiones OPTIONS son pre-vuelos de CORS y no deben procesar lógica de negocio ni contadores.
If req.Method = "OPTIONS" Then
Return ' Salimos directamente para estas peticiones.
End If
Dim start As Long = DateTime.Now ' Registra el tiempo de inicio de la petición para calcular la duración.
' Declaraciones de variables con alcance en toda la subrutina para asegurar la limpieza final.
Dim con As SQL ' La conexión a la BD, se inicializará más tarde.
Dim queryNameForLog As String = "unknown_json_command" ' Nombre del comando para el log, con valor por defecto.
Dim duration As Long ' La duración total de la petición, calculada antes del log.
Dim poolBusyConnectionsForLog As Int = 0 ' Contiene el número de conexiones ocupadas del pool.
Dim finalDbKey As String = "DB1" ' Identificador de la base de datos, con valor por defecto "DB1".
Dim requestsBeforeDecrement As Int = 0 ' Contador de peticiones activas antes de decrementar, inicializado en 0.
Dim Total As Int = 0
Try ' --- INICIO: Bloque Try que envuelve la lógica principal del Handler ---
Dim jsonString As String
' <<<< INICIO: Lógica para manejar peticiones POST con JSON en el cuerpo >>>>
If req.Method = "POST" And req.ContentType.Contains("application/json") Then
' Si es un POST con JSON en el cuerpo, leemos directamente del InputStream.
Dim Is0 As InputStream = req.InputStream
Dim bytes() As Byte = Bit.InputStreamToBytes(Is0) ' Lee el cuerpo completo de la petición.
jsonString = BytesToString(bytes, 0, bytes.Length, "UTF8") ' Convierte los bytes a una cadena JSON.
Is0.Close ' Cierra explícitamente el InputStream para liberar recursos.
Else
' De lo contrario, asumimos que el JSON viene en el parámetro 'j' de la URL (método legacy/GET).
jsonString = req.GetParameter("j")
End If
' <<<< FIN: Lógica para manejar peticiones POST con JSON en el cuerpo >>>>
' Validación inicial: Si no hay JSON, se envía un error 400.
If jsonString = Null Or jsonString = "" Then
Dim ErrorMsg As String = "Falta el parámetro 'j' en el URL o el cuerpo JSON en la petición."
SendErrorResponse(resp, 400, ErrorMsg)
Main.LogServerError("ERROR", "DBHandlerJSON.Handle", ErrorMsg, finalDbKey, queryNameForLog, req.RemoteAddress) ' <-- Nuevo Log
duration = DateTime.Now - start
CleanupAndLog(finalDbKey, queryNameForLog, duration, req.RemoteAddress, requestsBeforeDecrement, poolBusyConnectionsForLog, con)
Return
End If
Dim parser As JSONParser
parser.Initialize(jsonString) ' Inicializa el parser JSON con la cadena recibida.
Dim RootMap As Map = parser.NextObject ' Parsea el JSON a un objeto Map.
Dim execType As String = RootMap.GetDefault("exec", "") ' Obtiene el tipo de ejecución (ej. "ExecuteQuery").
' Obtiene el nombre de la query. Si no está en "query", busca en "exec".
queryNameForLog = RootMap.GetDefault("query", "")
If queryNameForLog = "" Then queryNameForLog = RootMap.GetDefault("exec", "unknown_json_command")
Dim paramsList As List = RootMap.Get("params") ' Obtiene la lista de parámetros para la query.
If paramsList = Null Or paramsList.IsInitialized = False Then
paramsList.Initialize ' Si no hay parámetros, inicializa una lista vacía.
End If
' <<<< ¡CORRECCIÓN CLAVE: RESOLVEMOS finalDbKey del JSON ANTES de usarla para los contadores! >>>>
' Esto asegura que el contador y el conector usen la DB correcta.
If RootMap.Get("dbx") <> Null Then finalDbKey = RootMap.Get("dbx")
' <<<< ¡FIN DE CORRECCIÓN CLAVE! >>>>
' --- INICIO: Conteo de peticiones activas para esta finalDbKey (Incrementar) ---
' Este bloque incrementa un contador global que rastrea cuántas peticiones están
' activas para una base de datos específica en un momento dado.
' 1. Aseguramos que el valor inicial sea un Int y lo recuperamos como Int (usando .As(Int)).
Dim currentCountFromMap As Int = GlobalParameters.ActiveRequestsCountByDB.GetDefault(finalDbKey, 0).As(Int)
GlobalParameters.ActiveRequestsCountByDB.Put(finalDbKey, currentCountFromMap + 1)
' requestsBeforeDecrement es el valor del contador justo después de que esta petición lo incrementa.
' Este es el valor que se registrará en la tabla 'query_logs'.
requestsBeforeDecrement = currentCountFromMap + 1
' Los logs de depuración para el incremento del contador pueden ser descomentados para una depuración profunda.
' Log($"[DEBUG] Handle Increment (JSON): dbKey=${finalDbKey}, currentCountFromMap=${currentCountFromMap}, requestsBeforeDecrement=${requestsBeforeDecrement}, Map state: ${GlobalParameters.ActiveRequestsCountByDB}"$)
' --- FIN: Conteo de peticiones activas ---
' Inicializa el Connector con la finalDbKey resuelta.
Connector = Main.Connectors.Get(finalDbKey)
' Validación: Si el dbKey no es válido o no está configurado en Main.listaDeCP.
If Main.listaDeCP.IndexOf(finalDbKey) = -1 Then
Dim ErrorMsg As String = "Parámetro 'DB' inválido. El nombre '" & finalDbKey & "' no es válido."
SendErrorResponse(resp, 400, ErrorMsg)
Main.LogServerError("ERROR", "DBHandlerJSON.Handle", ErrorMsg, finalDbKey, queryNameForLog, req.RemoteAddress) ' <-- Nuevo Log
duration = DateTime.Now - start
CleanupAndLog(finalDbKey, queryNameForLog, duration, req.RemoteAddress, requestsBeforeDecrement, poolBusyConnectionsForLog, con)
Return
End If
con = Connector.GetConnection(finalDbKey) ' ¡La conexión a la BD se obtiene aquí del pool de conexiones!
' <<<< ¡CAPTURAMOS BUSY_CONNECTIONS INMEDIATAMENTE DESPUÉS DE OBTENER LA CONEXIÓN! >>>>
' Este bloque captura el número de conexiones actualmente ocupadas en el pool
' *después* de que esta petición ha obtenido la suya.
If Connector.IsInitialized Then
Dim poolStats As Map = Connector.GetPoolStats
If poolStats.ContainsKey("BusyConnections") Then
' <<<< ¡CORRECCIÓN CLAVE: Aseguramos que el valor sea Int! >>>>
poolBusyConnectionsForLog = poolStats.Get("BusyConnections").As(Int) ' Capturamos el valor.
' Log($">>>>>>>>>> ${poolStats.Get("BusyConnections")} "$)
End If
End If
' <<<< ¡FIN DE CAPTURA! >>>>
Dim cachedStatsJSON As Map = Main.LatestPoolStats.Get(finalDbKey).As(Map)
If cachedStatsJSON.IsInitialized Then
' Los valores ya fueron capturados: poolBusyConnectionsForLog y requestsBeforeDecrement
cachedStatsJSON.Put("BusyConnections", poolBusyConnectionsForLog)
cachedStatsJSON.Put("HandlerActiveRequests", requestsBeforeDecrement)
If poolStats.ContainsKey("TotalConnections") Then
cachedStatsJSON.Put("TotalConnections", poolStats.Get("TotalConnections"))
End If
If poolStats.ContainsKey("IdleConnections") Then
cachedStatsJSON.Put("IdleConnections", poolStats.Get("IdleConnections"))
End If
' Re-escribir el mapa en el cache global (es Thread-Safe)
Main.LatestPoolStats.Put(finalDbKey, cachedStatsJSON)
' Log(Main.LatestPoolStats)
End If
' Log($"Total: ${poolStats.Get("TotalConnections")}, Idle: ${poolStats.Get("IdleConnections")}, busy: ${poolBusyConnectionsForLog}, active: ${requestsBeforeDecrement}"$)
' Obtiene la sentencia SQL correspondiente al nombre del comando desde config.properties.
Dim sqlCommand As String = Connector.GetCommand(finalDbKey, queryNameForLog)
' Validación: Si el comando SQL no fue encontrado en la configuración.
If sqlCommand = Null Or sqlCommand = "null" Or sqlCommand.Trim = "" Then
Dim errorMessage As String = $"El comando '${queryNameForLog}' no fue encontrado en el config.properties de '${finalDbKey}'."$
Log(errorMessage)
Main.LogServerError("ERROR", "DBHandlerJSON.Handle", errorMessage, finalDbKey, queryNameForLog, req.RemoteAddress) ' <-- Nuevo Log
SendErrorResponse(resp, 400, errorMessage)
duration = DateTime.Now - start
CleanupAndLog(finalDbKey, queryNameForLog, duration, req.RemoteAddress, requestsBeforeDecrement, poolBusyConnectionsForLog, con)
Return
End If
' --- Lógica para ejecutar diferentes tipos de comandos basados en el parámetro 'execType' ---
If execType.ToLowerCase = "executequery" Then
' --- INICIO VALIDACIÓN DE PARÁMETROS CENTRALIZADA ---
Dim validationResult As ParameterValidationResult = ParameterValidationUtils.ValidateAndAdjustParameters(queryNameForLog, finalDbKey, sqlCommand, paramsList, Connector.IsParameterToleranceEnabled)
If validationResult.Success = False Then
SendErrorResponse(resp, 400, validationResult.ErrorMessage)
duration = DateTime.Now - start
CleanupAndLog(finalDbKey, queryNameForLog, duration, req.RemoteAddress, requestsBeforeDecrement, poolBusyConnectionsForLog, con)
Return ' Salida temprana.
End If
Dim rs As ResultSet
' Ejecuta la consulta SQL con la lista de parámetros validada.
rs = con.ExecQuery2(sqlCommand, validationResult.ParamsToExecute)
' --- FIN VALIDACIÓN DE PARÁMETROS CENTRALIZADA ---
Dim ResultList As List
ResultList.Initialize ' Lista para almacenar los resultados de la consulta.
Dim jrs As JavaObject = rs ' Objeto Java subyacente del ResultSet para metadatos.
Dim rsmd As JavaObject = jrs.RunMethod("getMetaData", Null) ' Metadatos del ResultSet.
Dim cols As Int = rsmd.RunMethod("getColumnCount", Null) ' Número de columnas.
Do While rs.NextRow ' Itera sobre cada fila del resultado.
Dim RowMap As Map
RowMap.Initialize ' Mapa para almacenar los datos de la fila actual.
For i = 1 To cols ' Itera sobre cada columna.
Dim ColumnName As String = rsmd.RunMethod("getColumnName", Array(i)) ' Nombre de la columna.
Dim value As Object = jrs.RunMethod("getObject", Array(i)) ' Valor de la columna.
RowMap.Put(ColumnName, value) ' Añade la columna y su valor al mapa de la fila.
Next
ResultList.Add(RowMap) ' Añade el mapa de la fila a la lista de resultados.
Loop
rs.Close ' Cierra el ResultSet.
SendSuccessResponse(resp, CreateMap("result": ResultList)) ' Envía la respuesta JSON de éxito.
Else If execType.ToLowerCase = "executecommand" Then
' --- INICIO VALIDACIÓN DE PARÁMETROS CENTRALIZADA ---
Dim validationResult As ParameterValidationResult = ParameterValidationUtils.ValidateAndAdjustParameters(queryNameForLog, finalDbKey, sqlCommand, paramsList, Connector.IsParameterToleranceEnabled)
If validationResult.Success = False Then
SendErrorResponse(resp, 400, validationResult.ErrorMessage)
duration = DateTime.Now - start
CleanupAndLog(finalDbKey, queryNameForLog, duration, req.RemoteAddress, requestsBeforeDecrement, poolBusyConnectionsForLog, con)
Return ' Salida temprana.
End If
Dim affectedCount As Int = 1 ' Asumimos éxito (1) si ExecNonQuery2 no lanza una excepción.
con.ExecNonQuery2(sqlCommand, validationResult.ParamsToExecute) ' Ejecuta un comando con la lista de parámetros validada.
SendSuccessResponse(resp, CreateMap("affectedRows": affectedCount, "message": "Command executed successfully")) ' Envía confirmación de éxito.
' --- FIN VALIDACIÓN DE PARÁMETROS CENTRALIZADA ---
Else
Dim ErrorMsg As String = "Parámetro 'exec' inválido. '" & execType & "' no es un valor permitido."
SendErrorResponse(resp, 400, ErrorMsg)
Main.LogServerError("ERROR", "DBHandlerJSON.Handle", ErrorMsg, finalDbKey, queryNameForLog, req.RemoteAddress) ' <-- Nuevo Log
' El flujo continúa hasta la limpieza final si no hay un Return explícito.
End If
Catch ' --- CATCH: Maneja errores generales de ejecución o de SQL/JSON ---
Log(LastException) ' Registra la excepción completa en el log.
Main.LogServerError("ERROR", "DBHandlerJSON.Handle", LastException.Message, finalDbKey, queryNameForLog, req.RemoteAddress) ' <-- Nuevo Log
SendErrorResponse(resp, 500, LastException.Message) ' Envía un error 500 al cliente.
queryNameForLog = "error_processing_json" ' Para registrar que hubo un error en el log.
End Try ' --- FIN: Bloque Try principal ---
' --- Lógica de logging y limpieza final (para rutas de ejecución normal o después de Catch) ---
' Este bloque se asegura de que, independientemente de cómo termine la petición (éxito o error),
' la duración se calcule y se llamen las subrutinas de limpieza y logging.
duration = DateTime.Now - start ' Calcula la duración total de la petición.
' Llama a la subrutina centralizada para registrar el rendimiento y limpiar recursos.
CleanupAndLog(finalDbKey, queryNameForLog, duration, req.RemoteAddress, requestsBeforeDecrement, poolBusyConnectionsForLog, con)
End Sub
' --- NUEVA SUBRUTINA: Centraliza el logging de rendimiento y la limpieza de recursos ---
' Esta subrutina es llamada por Handle en todos los puntos de salida, asegurando
' que los contadores se decrementen y las conexiones se cierren de forma consistente.
Private Sub CleanupAndLog(dbKey As String, qName As String, durMs As Long, clientIp As String, handlerReqs As Int, poolBusyConns As Int, conn As SQL)
' Los logs de depuración para CleanupAndLog pueden ser descomentados para una depuración profunda.
' Log($"[DEBUG] CleanupAndLog Entry (JSON): dbKey=${dbKey}, handlerReqs=${handlerReqs}, Map state: ${GlobalParameters.ActiveRequestsCountByDB}"$)
' 1. Llama a la subrutina centralizada en Main para registrar el rendimiento en SQLite.
Main.LogQueryPerformance(qName, durMs, dbKey, clientIp, handlerReqs, poolBusyConns)
' <<<< ¡CORRECCIÓN CLAVE: Aseguramos que currentCount sea Int al obtenerlo del mapa! >>>>
' 2. Decrementa el contador de peticiones activas para esta dbKey de forma robusta.
Dim currentCount As Int = GlobalParameters.ActiveRequestsCountByDB.GetDefault(dbKey, 0).As(Int)
' Log($"[DEBUG] CleanupAndLog Before Decrement (JSON): dbKey=${dbKey}, currentCount (as Int)=${currentCount}, Map state: ${GlobalParameters.ActiveRequestsCountByDB}"$)
If currentCount > 0 Then
' Si el contador es positivo, lo decrementamos.
GlobalParameters.ActiveRequestsCountByDB.Put(dbKey, currentCount - 1)
Else
' Si el contador ya está en 0 o negativo (lo cual no debería ocurrir con la lógica actual,
' pero se maneja para robustez), registramos una advertencia y lo aseguramos en 0.
' Log($"ADVERTENCIA: Intento de decrementar ActiveRequestsCountByDB para ${dbKey} que ya estaba en ${currentCount}. Asegurando a 0."$)
GlobalParameters.ActiveRequestsCountByDB.Put(dbKey, 0)
End If
' Log($"[DEBUG] CleanupAndLog After Decrement (JSON): dbKey=${dbKey}, New count (as Int)=${GlobalParameters.ActiveRequestsCountByDB.GetDefault(dbKey,0).As(Int)}, Map state: ${GlobalParameters.ActiveRequestsCountByDB}"$)
' <<<< ¡FIN DE CORRECCIÓN CLAVE! >>>>
' 3. Asegura que la conexión a la BD siempre se cierre y se devuelva al pool de conexiones.
If conn <> Null And conn.IsInitialized Then conn.Close
End Sub
' --- Subrutinas de ayuda para respuestas JSON ---
' Construye y envía una respuesta JSON de éxito.
' resp: El objeto ServletResponse para enviar la respuesta.
' dataMap: Un mapa que contiene los datos a incluir en la respuesta JSON.
Private Sub SendSuccessResponse(resp As ServletResponse, dataMap As Map)
' Añade el campo "success": true al mapa de datos para indicar que todo salió bien.
dataMap.Put("success", True)
' Crea un generador de JSON.
Dim jsonGenerator As JSONGenerator
jsonGenerator.Initialize(dataMap)
' Establece el tipo de contenido de la respuesta a "application/json".
resp.ContentType = "application/json"
' Escribe la cadena JSON generada en el cuerpo de la respuesta HTTP.
resp.Write(jsonGenerator.ToString)
End Sub
' Construye y envía una respuesta JSON de error.
' resp: El objeto ServletResponse para enviar la respuesta.
' statusCode: El código de estado HTTP (ej. 400 para error del cliente, 500 para error del servidor).
' errorMessage: El mensaje de error que se enviará al cliente.
Private Sub SendErrorResponse(resp As ServletResponse, statusCode As Int, errorMessage As String)
' Personaliza el mensaje de error si es un error común de parámetros de Oracle o JDBC.
If errorMessage.Contains("Índice de columnas no válido") Or errorMessage.Contains("ORA-17003") Then
errorMessage = "NUMERO DE PARAMETROS EQUIVOCADO: " & errorMessage
End If
' Crea un mapa con el estado de error y el mensaje.
Dim resMap As Map = CreateMap("success": False, "error": errorMessage)
' Genera la cadena JSON a partir del mapa.
Dim jsonGenerator As JSONGenerator
jsonGenerator.Initialize(resMap)
' Establece el código de estado HTTP (ej. 400 para error del cliente, 500 para error del servidor).
resp.Status = statusCode
' Establece el tipo de contenido y escribe la respuesta de error.
resp.ContentType = "application/json"
resp.Write(jsonGenerator.ToString)
End Sub

43
DoLoginHandler.bas Normal file
View File

@@ -0,0 +1,43 @@
B4J=true
Group=Default Group
ModulesStructureVersion=1
Type=Class
Version=10.3
@EndOfDesignText@
'Class module: DoLoginHandler
Sub Class_Globals
Private bc As BCrypt
End Sub
Public Sub Initialize
bc.Initialize("BC")
End Sub
Public Sub Handle(req As ServletRequest, resp As ServletResponse)
' Limpiamos el input del usuario para evitar errores
Dim u As String = req.GetParameter("username").Trim
Dim p As String = req.GetParameter("password")
Log(u)
Try
' Buscamos el hash en la base de datos usando el usuario limpio
Dim storedHash As String = Main.SQL1.ExecQuerySingleResult2("SELECT password_hash FROM users WHERE username = ?", Array As String(u))
Log($"${storedHash}"$)
' Log($"${bc.checkpw(p, storedHash)}"$)
' Verificamos la contraseña contra el hash
If storedHash <> Null And bc.checkpw(p, storedHash) Then
' CREDENCIALES CORRECTAS
' 1. Autorizamos la sesión
req.GetSession.SetAttribute("user_is_authorized", True)
' 2. ¡Y guardamos el nombre de usuario! (Esta es la línea que faltaba)
req.GetSession.SetAttribute("username", u)
resp.SendRedirect("/manager")
Else
' Credenciales incorrectas
resp.SendRedirect("/login")
End If
Catch
Log(LastException)
resp.SendRedirect("/login")
End Try
End Sub

View File

@@ -0,0 +1,29 @@
#Lines starting with '#' are comments.
#Backslash character at the end of line means that the command continues in the next line.
#DATABASE CONFIGURATION
DriverClass=com.mysql.jdbc.Driver
JdbcUrl=jdbc:mysql://localhost/$DB$?characterEncoding=utf8
User=root
Password=
#Java server port
ServerPort=17178
#example of MS SQL Server configuration:
#DriverClass=net.sourceforge.jtds.jdbc.Driver
#JdbcUrl=jdbc:jtds:sqlserver://<server address>/<database>
#example of postegres configuration:
#JdbcUrl=jdbc:postgresql://localhost/test
#DriverClass=org.postgresql.Driver
#SQL COMMANDS
sql.create_table=CREATE TABLE IF NOT EXISTS animals (\
id INTEGER PRIMARY KEY AUTO_INCREMENT,\
name CHAR(30) NOT NULL,\
image BLOB)
sql.insert_animal=INSERT INTO animals VALUES (null, ?,?)
sql.select_animal=SELECT name, image, id FROM animals WHERE id = ?;
sql.create_table=CREATE TABLE article (col1 numeric(10,4) ,col2 text);
sql.select=select * from article
sql.insert=INSERT INTO article VALUES(?, ?)

View File

@@ -0,0 +1,29 @@
#Lines starting with '#' are comments.
#Backslash character at the end of line means that the command continues in the next line.
#DATABASE CONFIGURATION
DriverClass=com.mysql.jdbc.Driver
JdbcUrl=jdbc:mysql://localhost/$DB$?characterEncoding=utf8
User=root
Password=
#Java server port
ServerPort=17178
#example of MS SQL Server configuration:
#DriverClass=net.sourceforge.jtds.jdbc.Driver
#JdbcUrl=jdbc:jtds:sqlserver://<server address>/<database>
#example of postegres configuration:
#JdbcUrl=jdbc:postgresql://localhost/test
#DriverClass=org.postgresql.Driver
#SQL COMMANDS
sql.create_table=CREATE TABLE IF NOT EXISTS animals (\
id INTEGER PRIMARY KEY AUTO_INCREMENT,\
name CHAR(30) NOT NULL,\
image BLOB)
sql.insert_animal=INSERT INTO animals VALUES (null, ?,?)
sql.select_animal=SELECT name, image, id FROM animals WHERE id = ?;
sql.create_table=CREATE TABLE article (col1 numeric(10,4) ,col2 text);
sql.select=select * from article
sql.insert=INSERT INTO article VALUES(?, ?)

View File

@@ -0,0 +1,29 @@
#Lines starting with '#' are comments.
#Backslash character at the end of line means that the command continues in the next line.
#DATABASE CONFIGURATION
DriverClass=com.mysql.jdbc.Driver
JdbcUrl=jdbc:mysql://localhost/$DB$?characterEncoding=utf8
User=root
Password=
#Java server port
ServerPort=17178
#example of MS SQL Server configuration:
#DriverClass=net.sourceforge.jtds.jdbc.Driver
#JdbcUrl=jdbc:jtds:sqlserver://<server address>/<database>
#example of postegres configuration:
#JdbcUrl=jdbc:postgresql://localhost/test
#DriverClass=org.postgresql.Driver
#SQL COMMANDS
sql.create_table=CREATE TABLE IF NOT EXISTS animals (\
id INTEGER PRIMARY KEY AUTO_INCREMENT,\
name CHAR(30) NOT NULL,\
image BLOB)
sql.insert_animal=INSERT INTO animals VALUES (null, ?,?)
sql.select_animal=SELECT name, image, id FROM animals WHERE id = ?;
sql.create_table=CREATE TABLE article (col1 numeric(10,4) ,col2 text);
sql.select=select * from article
sql.insert=INSERT INTO article VALUES(?, ?)

View File

@@ -0,0 +1,29 @@
#Lines starting with '#' are comments.
#Backslash character at the end of line means that the command continues in the next line.
#DATABASE CONFIGURATION
DriverClass=com.mysql.jdbc.Driver
JdbcUrl=jdbc:mysql://localhost/$DB$?characterEncoding=utf8
User=root
Password=
#Java server port
ServerPort=17178
#example of MS SQL Server configuration:
#DriverClass=net.sourceforge.jtds.jdbc.Driver
#JdbcUrl=jdbc:jtds:sqlserver://<server address>/<database>
#example of postegres configuration:
#JdbcUrl=jdbc:postgresql://localhost/test
#DriverClass=org.postgresql.Driver
#SQL COMMANDS
sql.create_table=CREATE TABLE IF NOT EXISTS animals (\
id INTEGER PRIMARY KEY AUTO_INCREMENT,\
name CHAR(30) NOT NULL,\
image BLOB)
sql.insert_animal=INSERT INTO animals VALUES (null, ?,?)
sql.select_animal=SELECT name, image, id FROM animals WHERE id = ?;
sql.create_table=CREATE TABLE article (col1 numeric(10,4) ,col2 text);
sql.select=select * from article
sql.insert=INSERT INTO article VALUES(?, ?)

View File

@@ -0,0 +1,9 @@
@rem Este script reinicia el proceso en PM2 del servidor de jRDC2
@rem estas lineas sirven para que el archivo bat corra en modo administrador.
set "params=%*"
cd /d "%~dp0" && ( if exist "%temp%\getadmin.vbs" del "%temp%\getadmin.vbs" ) && fsutil dirty query %systemdrive% 1>nul 2>nul || ( echo Set UAC = CreateObject^("Shell.Application"^) : UAC.ShellExecute "cmd.exe", "/k cd ""%~sdp0"" && ""%~s0"" %params%", "", "runas", 1 >> "%temp%\getadmin.vbs" && "%temp%\getadmin.vbs" && exit /B )
pm2 restart BotSoporte_4.0
exit

View File

@@ -0,0 +1,9 @@
@rem Este script reinicia el proceso en PM2 del servidor de jRDC2
@rem estas lineas sirven para que el archivo bat corra en modo administrador.
set "params=%*"
cd /d "%~dp0" && ( if exist "%temp%\getadmin.vbs" del "%temp%\getadmin.vbs" ) && fsutil dirty query %systemdrive% 1>nul 2>nul || ( echo Set UAC = CreateObject^("Shell.Application"^) : UAC.ShellExecute "cmd.exe", "/k cd ""%~sdp0"" && ""%~s0"" %params%", "", "runas", 1 >> "%temp%\getadmin.vbs" && "%temp%\getadmin.vbs" && exit /B )
pm2 restart jRDC-Multi
exit

8
Files ejemplo/start.bat Normal file
View File

@@ -0,0 +1,8 @@
@rem Este script mata el proceso del servidor y despues lo reinicia, necesita los archivos stop.bat y start2.bat
start cmd.exe /c stop.bat
timeout 2
start cmd.exe /c start2.bat %1
exit

3
Files ejemplo/start2.bat Normal file
View File

@@ -0,0 +1,3 @@
@TITLE -== DBR Server %1 %2 ==-
"C:\Program Files (x86)\Java\jdk-14\bin\java.exe" -jar jRDC_Multi.jar

1
Files ejemplo/stop.bat Normal file
View File

@@ -0,0 +1 @@
wmic Path win32_process Where "CommandLine Like '%%jRDC_Multi.jar%%'" Call Terminate

View File

@@ -0,0 +1,21 @@
<!DOCTYPE html>
<html lang="es">
<head>
<meta charset="UTF-8">
<title>Login jRDC Server</title>
<style>
body { font-family: sans-serif; display: flex; justify-content: center; align-items: center; height: 100vh; background-color: #f0f0f0; }
form { background: white; padding: 2em; border-radius: 8px; box-shadow: 0 4px 8px rgba(0,0,0,0.1); }
input { display: block; margin-bottom: 1em; padding: 0.5em; width: 200px; }
button { padding: 0.7em; width: 100%; border: none; background-color: #007bff; color: white; cursor: pointer; border-radius: 4px; }
</style>
</head>
<body>
<form action="/dologin" method="post">
<h2>Acceso al Manager</h2>
<input type="text" name="username" placeholder="Usuario" required>
<input type="password" name="password" placeholder="Contraseña" required>
<button type="submit">Entrar</button>
</form>
</body>
</html>

View File

@@ -0,0 +1,392 @@
<!DOCTYPE html>
<html lang="es">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>jRDC2-Multi - Panel de Administración</title>
<style>
body {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto,
Helvetica, Arial, sans-serif;
background-color: #f8f9fa;
color: #212529;
margin: 0;
padding: 0;
}
.admin-menu {
background-color: #e9ecef;
padding: 1.5em 2em;
border-bottom: 1px solid #dee2e6;
}
.admin-menu h2 {
margin: 0 0 0.2em 0;
color: #343a40;
}
.admin-menu p {
margin: 0 0 1em 0;
color: #495057;
}
.admin-menu a {
color: #007bff;
text-decoration: none;
font-weight: 500;
margin-right: 0.5em;
padding-right: 0.5em;
border-right: 1px solid #ccc;
cursor: pointer;
}
.admin-menu a:last-child {
border-right: none;
}
.admin-menu a:hover {
text-decoration: underline;
}
.main-content {
padding: 2em;
}
#output-container {
background: #fff;
padding: 1em;
border: 1px solid #eee;
border-radius: 8px;
font-family: monospace;
white-space: pre-wrap;
word-wrap: break-word;
min-height: 200px;
overflow-x: auto;
}
table {
width: 100%;
border-collapse: collapse;
margin-top: 1.5em;
font-family: sans-serif;
font-size: 0.9em;
}
th,
td {
padding: 10px 12px;
text-align: center;
border-bottom: 1px solid #dee2e6;
}
thead {
background-color: #007bff;
color: #fff;
}
th {
font-weight: 600;
cursor: help;
}
tbody td:first-child {
font-weight: bold;
color: #0056b3;
text-align: left;
}
/* --- ESTILOS PARA EL INDICADOR SSE --- */
.sse-status-container {
display: flex;
align-items: center;
margin-top: 1em;
font-size: 0.8em;
}
.sse-status {
display: inline-block;
padding: 4px 8px;
border-radius: 4px;
font-weight: bold;
margin-left: 10px;
min-width: 80px;
text-align: center;
}
.sse-connected {
background-color: #28a745;
color: white;
}
.sse-disconnected {
background-color: #dc3545;
color: white;
}
.sse-connecting {
background-color: #ffc107;
color: #212529;
}
.cell-update {
animation: flash 0.7s ease-out;
}
@keyframes flash {
0% {
background-color: #ffc107;
}
100% {
background-color: transparent;
}
}
</style>
</head>
<body>
<div class="admin-menu">
<h2>Panel de Administración jRDC</h2>
<p>Bienvenido, <strong>admin</strong></p>
<nav id="main-nav">
<a data-command="test">Test</a>
<a data-command="ping">Ping</a>
<a data-command="reload">Reload</a>
<a data-command="slowqueries">Queries Lentas</a>
<a data-command="getstats">Estadísticas Pool</a>
<a data-command="rpm2">Reiniciar (pm2)</a>
<a data-command="reviveBow">Revive Bow</a>
<a data-command="getconfiginfo">Info</a>
</nav>
<div class="sse-status-container">
<span>Estado de Estadísticas en Tiempo Real:</span>
<span id="sse-status" class="sse-status sse-disconnected"
>Desconectado</span
>
</div>
</div>
<div class="main-content">
<h1 id="content-title">Bienvenido</h1>
<div id="output-container">
<p style="font-family: sans-serif">
Selecciona una opción del menú para comenzar.
</p>
</div>
</div>
<script>
const outputContainer = document.getElementById("output-container");
const contentTitle = document.getElementById("content-title");
const sseStatus = document.getElementById("sse-status");
let sseConnection = null;
// --- CONFIGURACIÓN PARA LA TABLA DE ESTADÍSTICAS ---
const COLUMN_ORDER = [
"InitialPoolSize",
"MinPoolSize",
"MaxPoolSize",
"AcquireIncrement",
"TotalConnections",
"BusyConnections",
"IdleConnections",
"CheckoutTimeout",
"MaxIdleTime",
"MaxConnectionAge",
];
const HEADER_TOOLTIPS = {
InitialPoolSize:
"Número de conexiones que el pool intenta adquirir al arrancar.",
MinPoolSize: "Número mínimo de conexiones que el pool mantendrá.",
MaxPoolSize: "Número máximo de conexiones que el pool puede mantener.",
AcquireIncrement:
"Número de conexiones a adquirir cuando el pool se queda sin conexiones.",
TotalConnections: "Número total de conexiones (ocupadas + libres).",
BusyConnections: "Número de conexiones activamente en uso.",
IdleConnections: "Número de conexiones disponibles en el pool.",
CheckoutTimeout: "Tiempo máximo de espera por una conexión (ms).",
MaxIdleTime:
"Tiempo máximo que una conexión puede estar inactiva (segundos).",
MaxConnectionAge: "Tiempo máximo de vida de una conexión (segundos).",
};
// --- MANEJO DE LA CONEXIÓN SSE ---
function connectSSE() {
if (sseConnection && sseConnection.readyState !== EventSource.CLOSED) {
return; // Ya está conectado o conectando
}
outputContainer.innerHTML =
'<p style="font-family: sans-serif;">Esperando datos del pool de conexiones...</p>';
updateSSEStatus("connecting");
// La ruta debe coincidir con la que registraste en srvr.AddHandler
const SSE_ENDPOINT = "/stats-stream";
sseConnection = new EventSource(SSE_ENDPOINT);
sseConnection.onopen = () => {
console.log("Conexión SSE establecida.");
updateSSEStatus("connected");
};
// Escucha el evento específico "stats_update"
sseConnection.addEventListener("stats_update", (event) => {
try {
const data = JSON.parse(event.data);
renderOrUpdateStatsTable(data);
} catch (e) {
console.error("Error al parsear datos SSE:", e);
}
});
sseConnection.onerror = () => {
console.error("Error en la conexión SSE. Reintentando...");
updateSSEStatus("disconnected");
sseConnection.close();
// El navegador reintentará automáticamente la conexión
};
}
function disconnectSSE() {
if (sseConnection) {
sseConnection.close();
sseConnection = null;
console.log("Conexión SSE cerrada.");
updateSSEStatus("disconnected");
}
}
function updateSSEStatus(status) {
switch (status) {
case "connected":
sseStatus.textContent = "Conectado";
sseStatus.className = "sse-status sse-connected";
break;
case "connecting":
sseStatus.textContent = "Conectando";
sseStatus.className = "sse-status sse-connecting";
break;
case "disconnected":
sseStatus.textContent = "Desconectado";
sseStatus.className = "sse-status sse-disconnected";
break;
}
}
// --- RENDERIZADO Y ACTUALIZACIÓN DE LA TABLA ---
function renderOrUpdateStatsTable(data) {
const table = document.getElementById("stats-table");
// Si la tabla no existe, la crea
if (!table) {
outputContainer.innerHTML = createStatsTableHTML(data);
return;
}
// Si la tabla ya existe, solo actualiza las celdas
for (const dbKey in data) {
const poolData = data[dbKey];
COLUMN_ORDER.forEach((metric) => {
const cell = document.getElementById(`${dbKey}_${metric}`);
if (cell && cell.textContent != poolData[metric]) {
cell.textContent = poolData[metric];
cell.classList.add("cell-update");
setTimeout(() => cell.classList.remove("cell-update"), 700);
}
});
}
}
function createStatsTableHTML(data) {
let tableHtml = `<table id="stats-table"><thead><tr><th title="Nombre de la Base de Datos">DB Key</th>`;
COLUMN_ORDER.forEach((key) => {
tableHtml += `<th title="${HEADER_TOOLTIPS[key] || ""}">${key}</th>`;
});
tableHtml += `</tr></thead><tbody>`;
for (const dbKey in data) {
const poolData = data[dbKey];
tableHtml += `<tr><td>${dbKey}</td>`;
COLUMN_ORDER.forEach((metric) => {
// Se añade un ID único a cada celda: "DB1_TotalConnections"
tableHtml += `<td id="${dbKey}_${metric}">${
poolData[metric] ?? "N/A"
}</td>`;
});
tableHtml += `</tr>`;
}
tableHtml += `</tbody></table>`;
return tableHtml;
}
// --- MANEJO DE COMANDOS ESTÁTICOS (SIN CAMBIOS) ---
async function loadStaticContent(command) {
contentTitle.textContent = `Resultado del Comando: '${command}'`;
outputContainer.innerHTML = "Cargando...";
try {
const response = await fetch(`/manager?command=${command}`);
const responseText = await response.text();
if (!response.ok)
throw new Error(
`Error del servidor (${response.status}): ${responseText}`
);
const contentType = response.headers.get("content-type");
if (contentType && contentType.includes("application/json")) {
const data = JSON.parse(responseText);
if (command === "slowqueries") {
outputContainer.innerHTML = data.message
? `<p>${data.message}</p>`
: createTableFromJSON(data.data);
} else {
outputContainer.textContent = JSON.stringify(data, null, 2);
}
} else {
outputContainer.textContent = responseText;
}
} catch (error) {
outputContainer.textContent = `Error al procesar la respuesta:\n${error.message}`;
}
}
function createTableFromJSON(data) {
if (!data || data.length === 0)
return "<p>No se encontraron queries lentas.</p>";
const headers = Object.keys(data[0]);
let table = "<table><thead><tr>";
headers.forEach((h) => (table += `<th>${h.replace(/_/g, " ")}</th>`));
table += "</tr></thead><tbody>";
data.forEach((row) => {
table += "<tr>";
headers.forEach((h) => (table += `<td>${row[h]}</td>`));
table += "</tr>";
});
table += "</tbody></table>";
return table;
}
// --- EVENT LISTENER PRINCIPAL ---
document.getElementById("main-nav").addEventListener("click", (event) => {
if (event.target.tagName === "A") {
const command = event.target.dataset.command;
if (!command) return;
if (command === "reload") {
// Pedimos al usuario la DB Key. Si la deja vacía, se asume recarga total.
const dbKey = prompt(
"Ingrese la llave de la DB a recargar (ej: DB2, DB3). Deje vacío para recargar TODAS:",
""
);
if (dbKey === null) {
// El usuario presionó Cancelar o cerró la ventana. NO HACER NADA.
outputContainer.textContent = "Recarga cancelada por el usuario.";
contentTitle.textContent = "Administración";
return;
}
let finalCommand = "reload";
if (dbKey && dbKey.trim() !== "") {
// Si el usuario especificó una DB (ej. DB2), construimos el comando con el parámetro 'db'.
const key = dbKey.toUpperCase().trim();
finalCommand = `reload&db=${key}`;
outputContainer.innerHTML = `<p style="font-family: sans-serif;">Intentando recargar: <b>${key}</b> (Hot-Swap)...</p>`;
} else {
// Recarga total.
outputContainer.innerHTML = `<p style="font-family: sans-serif;">Intentando recargar: <b>TODAS</b> (Hot-Swap)...</p>`;
}
disconnectSSE();
// Llamamos a la función loadStaticContent con el comando completo (ej: 'reload&db=DB2' o 'reload')
loadStaticContent(finalCommand);
} else if (command === "getstats") {
contentTitle.textContent = `Estadísticas del Pool en Tiempo Real`;
connectSSE();
} else {
// Si se selecciona cualquier otro comando, se desconecta del SSE
disconnectSSE();
loadStaticContent(command);
}
}
});
</script>
</body>
</html>

View File

@@ -0,0 +1,29 @@
#Lines starting with '#' are comments.
#Backslash character at the end of line means that the command continues in the next line.
#DATABASE CONFIGURATION
DriverClass=com.mysql.jdbc.Driver
JdbcUrl=jdbc:mysql://localhost/$DB$?characterEncoding=utf8
User=root
Password=
#Java server port
ServerPort=17178
#example of MS SQL Server configuration:
#DriverClass=net.sourceforge.jtds.jdbc.Driver
#JdbcUrl=jdbc:jtds:sqlserver://<server address>/<database>
#example of postegres configuration:
#JdbcUrl=jdbc:postgresql://localhost/test
#DriverClass=org.postgresql.Driver
#SQL COMMANDS
sql.create_table=CREATE TABLE IF NOT EXISTS animals (\
id INTEGER PRIMARY KEY AUTO_INCREMENT,\
name CHAR(30) NOT NULL,\
image BLOB)
sql.insert_animal=INSERT INTO animals VALUES (null, ?,?)
sql.select_animal=SELECT name, image, id FROM animals WHERE id = ?;
sql.create_table=CREATE TABLE article (col1 numeric(10,4) ,col2 text);
sql.select=select * from article
sql.insert=INSERT INTO article VALUES(?, ?)

View File

@@ -0,0 +1,29 @@
#Lines starting with '#' are comments.
#Backslash character at the end of line means that the command continues in the next line.
#DATABASE CONFIGURATION
DriverClass=com.mysql.jdbc.Driver
JdbcUrl=jdbc:mysql://localhost/$DB$?characterEncoding=utf8
User=root
Password=
#Java server port
ServerPort=17178
#example of MS SQL Server configuration:
#DriverClass=net.sourceforge.jtds.jdbc.Driver
#JdbcUrl=jdbc:jtds:sqlserver://<server address>/<database>
#example of postegres configuration:
#JdbcUrl=jdbc:postgresql://localhost/test
#DriverClass=org.postgresql.Driver
#SQL COMMANDS
sql.create_table=CREATE TABLE IF NOT EXISTS animals (\
id INTEGER PRIMARY KEY AUTO_INCREMENT,\
name CHAR(30) NOT NULL,\
image BLOB)
sql.insert_animal=INSERT INTO animals VALUES (null, ?,?)
sql.select_animal=SELECT name, image, id FROM animals WHERE id = ?;
sql.create_table=CREATE TABLE article (col1 numeric(10,4) ,col2 text);
sql.select=select * from article
sql.insert=INSERT INTO article VALUES(?, ?)

View File

@@ -0,0 +1,29 @@
#Lines starting with '#' are comments.
#Backslash character at the end of line means that the command continues in the next line.
#DATABASE CONFIGURATION
DriverClass=com.mysql.jdbc.Driver
JdbcUrl=jdbc:mysql://localhost/$DB$?characterEncoding=utf8
User=root
Password=
#Java server port
ServerPort=17178
#example of MS SQL Server configuration:
#DriverClass=net.sourceforge.jtds.jdbc.Driver
#JdbcUrl=jdbc:jtds:sqlserver://<server address>/<database>
#example of postegres configuration:
#JdbcUrl=jdbc:postgresql://localhost/test
#DriverClass=org.postgresql.Driver
#SQL COMMANDS
sql.create_table=CREATE TABLE IF NOT EXISTS animals (\
id INTEGER PRIMARY KEY AUTO_INCREMENT,\
name CHAR(30) NOT NULL,\
image BLOB)
sql.insert_animal=INSERT INTO animals VALUES (null, ?,?)
sql.select_animal=SELECT name, image, id FROM animals WHERE id = ?;
sql.create_table=CREATE TABLE article (col1 numeric(10,4) ,col2 text);
sql.select=select * from article
sql.insert=INSERT INTO article VALUES(?, ?)

29
Files/config.properties Normal file
View File

@@ -0,0 +1,29 @@
#Lines starting with '#' are comments.
#Backslash character at the end of line means that the command continues in the next line.
#DATABASE CONFIGURATION
DriverClass=com.mysql.jdbc.Driver
JdbcUrl=jdbc:mysql://localhost/$DB$?characterEncoding=utf8
User=root
Password=
#Java server port
ServerPort=17178
#example of MS SQL Server configuration:
#DriverClass=net.sourceforge.jtds.jdbc.Driver
#JdbcUrl=jdbc:jtds:sqlserver://<server address>/<database>
#example of postegres configuration:
#JdbcUrl=jdbc:postgresql://localhost/test
#DriverClass=org.postgresql.Driver
#SQL COMMANDS
sql.create_table=CREATE TABLE IF NOT EXISTS animals (\
id INTEGER PRIMARY KEY AUTO_INCREMENT,\
name CHAR(30) NOT NULL,\
image BLOB)
sql.insert_animal=INSERT INTO animals VALUES (null, ?,?)
sql.select_animal=SELECT name, image, id FROM animals WHERE id = ?;
sql.create_table=CREATE TABLE article (col1 numeric(10,4) ,col2 text);
sql.select=select * from article
sql.insert=INSERT INTO article VALUES(?, ?)

View File

@@ -0,0 +1,9 @@
@rem Este script reinicia el proceso en PM2 del servidor de jRDC2
@rem estas lineas sirven para que el archivo bat corra en modo administrador.
set "params=%*"
cd /d "%~dp0" && ( if exist "%temp%\getadmin.vbs" del "%temp%\getadmin.vbs" ) && fsutil dirty query %systemdrive% 1>nul 2>nul || ( echo Set UAC = CreateObject^("Shell.Application"^) : UAC.ShellExecute "cmd.exe", "/k cd ""%~sdp0"" && ""%~s0"" %params%", "", "runas", 1 >> "%temp%\getadmin.vbs" && "%temp%\getadmin.vbs" && exit /B )
pm2 restart BotSoporte_4.0
exit

View File

@@ -0,0 +1,9 @@
@rem Este script reinicia el proceso en PM2 del servidor de jRDC2
@rem estas lineas sirven para que el archivo bat corra en modo administrador.
set "params=%*"
cd /d "%~dp0" && ( if exist "%temp%\getadmin.vbs" del "%temp%\getadmin.vbs" ) && fsutil dirty query %systemdrive% 1>nul 2>nul || ( echo Set UAC = CreateObject^("Shell.Application"^) : UAC.ShellExecute "cmd.exe", "/k cd ""%~sdp0"" && ""%~s0"" %params%", "", "runas", 1 >> "%temp%\getadmin.vbs" && "%temp%\getadmin.vbs" && exit /B )
pm2 restart jRDC-Multi
exit

8
Files/start.bat Normal file
View File

@@ -0,0 +1,8 @@
@rem Este script mata el proceso del servidor y despues lo reinicia, necesita los archivos stop.bat y start2.bat
start cmd.exe /c stop.bat
timeout 2
start cmd.exe /c start2.bat %1
exit

3
Files/start2.bat Normal file
View File

@@ -0,0 +1,3 @@
@TITLE -== DBR Server %1 %2 ==-
"C:\Program Files (x86)\Java\jdk-14\bin\java.exe" -jar jRDC_Multi.jar

1
Files/stop.bat Normal file
View File

@@ -0,0 +1 @@
wmic Path win32_process Where "CommandLine Like '%%jRDC_Multi.jar%%'" Call Terminate

21
Files/www/login.html Normal file
View File

@@ -0,0 +1,21 @@
<!DOCTYPE html>
<html lang="es">
<head>
<meta charset="UTF-8">
<title>Login jRDC Server</title>
<style>
body { font-family: sans-serif; display: flex; justify-content: center; align-items: center; height: 100vh; background-color: #f0f0f0; }
form { background: white; padding: 2em; border-radius: 8px; box-shadow: 0 4px 8px rgba(0,0,0,0.1); }
input { display: block; margin-bottom: 1em; padding: 0.5em; width: 200px; }
button { padding: 0.7em; width: 100%; border: none; background-color: #007bff; color: white; cursor: pointer; border-radius: 4px; }
</style>
</head>
<body>
<form action="/dologin" method="post">
<h2>Acceso al Manager</h2>
<input type="text" name="username" placeholder="Usuario" required>
<input type="password" name="password" placeholder="Contraseña" required>
<button type="submit">Entrar</button>
</form>
</body>
</html>

392
Files/www/manager.html Normal file
View File

@@ -0,0 +1,392 @@
<!DOCTYPE html>
<html lang="es">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>jRDC2-Multi - Panel de Administración</title>
<style>
body {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto,
Helvetica, Arial, sans-serif;
background-color: #f8f9fa;
color: #212529;
margin: 0;
padding: 0;
}
.admin-menu {
background-color: #e9ecef;
padding: 1.5em 2em;
border-bottom: 1px solid #dee2e6;
}
.admin-menu h2 {
margin: 0 0 0.2em 0;
color: #343a40;
}
.admin-menu p {
margin: 0 0 1em 0;
color: #495057;
}
.admin-menu a {
color: #007bff;
text-decoration: none;
font-weight: 500;
margin-right: 0.5em;
padding-right: 0.5em;
border-right: 1px solid #ccc;
cursor: pointer;
}
.admin-menu a:last-child {
border-right: none;
}
.admin-menu a:hover {
text-decoration: underline;
}
.main-content {
padding: 2em;
}
#output-container {
background: #fff;
padding: 1em;
border: 1px solid #eee;
border-radius: 8px;
font-family: monospace;
white-space: pre-wrap;
word-wrap: break-word;
min-height: 200px;
overflow-x: auto;
}
table {
width: 100%;
border-collapse: collapse;
margin-top: 1.5em;
font-family: sans-serif;
font-size: 0.9em;
}
th,
td {
padding: 10px 12px;
text-align: center;
border-bottom: 1px solid #dee2e6;
}
thead {
background-color: #007bff;
color: #fff;
}
th {
font-weight: 600;
cursor: help;
}
tbody td:first-child {
font-weight: bold;
color: #0056b3;
text-align: left;
}
/* --- ESTILOS PARA EL INDICADOR SSE --- */
.sse-status-container {
display: flex;
align-items: center;
margin-top: 1em;
font-size: 0.8em;
}
.sse-status {
display: inline-block;
padding: 4px 8px;
border-radius: 4px;
font-weight: bold;
margin-left: 10px;
min-width: 80px;
text-align: center;
}
.sse-connected {
background-color: #28a745;
color: white;
}
.sse-disconnected {
background-color: #dc3545;
color: white;
}
.sse-connecting {
background-color: #ffc107;
color: #212529;
}
.cell-update {
animation: flash 0.7s ease-out;
}
@keyframes flash {
0% {
background-color: #ffc107;
}
100% {
background-color: transparent;
}
}
</style>
</head>
<body>
<div class="admin-menu">
<h2>Panel de Administración jRDC</h2>
<p>Bienvenido, <strong>admin</strong></p>
<nav id="main-nav">
<a data-command="test">Test</a>
<a data-command="ping">Ping</a>
<a data-command="reload">Reload</a>
<a data-command="slowqueries">Queries Lentas</a>
<a data-command="getstats">Estadísticas Pool</a>
<a data-command="rpm2">Reiniciar (pm2)</a>
<a data-command="reviveBow">Revive Bow</a>
<a data-command="getconfiginfo">Info</a>
</nav>
<div class="sse-status-container">
<span>Estado de Estadísticas en Tiempo Real:</span>
<span id="sse-status" class="sse-status sse-disconnected"
>Desconectado</span
>
</div>
</div>
<div class="main-content">
<h1 id="content-title">Bienvenido</h1>
<div id="output-container">
<p style="font-family: sans-serif">
Selecciona una opción del menú para comenzar.
</p>
</div>
</div>
<script>
const outputContainer = document.getElementById("output-container");
const contentTitle = document.getElementById("content-title");
const sseStatus = document.getElementById("sse-status");
let sseConnection = null;
// --- CONFIGURACIÓN PARA LA TABLA DE ESTADÍSTICAS ---
const COLUMN_ORDER = [
"InitialPoolSize",
"MinPoolSize",
"MaxPoolSize",
"AcquireIncrement",
"TotalConnections",
"BusyConnections",
"IdleConnections",
"CheckoutTimeout",
"MaxIdleTime",
"MaxConnectionAge",
];
const HEADER_TOOLTIPS = {
InitialPoolSize:
"Número de conexiones que el pool intenta adquirir al arrancar.",
MinPoolSize: "Número mínimo de conexiones que el pool mantendrá.",
MaxPoolSize: "Número máximo de conexiones que el pool puede mantener.",
AcquireIncrement:
"Número de conexiones a adquirir cuando el pool se queda sin conexiones.",
TotalConnections: "Número total de conexiones (ocupadas + libres).",
BusyConnections: "Número de conexiones activamente en uso.",
IdleConnections: "Número de conexiones disponibles en el pool.",
CheckoutTimeout: "Tiempo máximo de espera por una conexión (ms).",
MaxIdleTime:
"Tiempo máximo que una conexión puede estar inactiva (segundos).",
MaxConnectionAge: "Tiempo máximo de vida de una conexión (segundos).",
};
// --- MANEJO DE LA CONEXIÓN SSE ---
function connectSSE() {
if (sseConnection && sseConnection.readyState !== EventSource.CLOSED) {
return; // Ya está conectado o conectando
}
outputContainer.innerHTML =
'<p style="font-family: sans-serif;">Esperando datos del pool de conexiones...</p>';
updateSSEStatus("connecting");
// La ruta debe coincidir con la que registraste en srvr.AddHandler
const SSE_ENDPOINT = "/stats-stream";
sseConnection = new EventSource(SSE_ENDPOINT);
sseConnection.onopen = () => {
console.log("Conexión SSE establecida.");
updateSSEStatus("connected");
};
// Escucha el evento específico "stats_update"
sseConnection.addEventListener("stats_update", (event) => {
try {
const data = JSON.parse(event.data);
renderOrUpdateStatsTable(data);
} catch (e) {
console.error("Error al parsear datos SSE:", e);
}
});
sseConnection.onerror = () => {
console.error("Error en la conexión SSE. Reintentando...");
updateSSEStatus("disconnected");
sseConnection.close();
// El navegador reintentará automáticamente la conexión
};
}
function disconnectSSE() {
if (sseConnection) {
sseConnection.close();
sseConnection = null;
console.log("Conexión SSE cerrada.");
updateSSEStatus("disconnected");
}
}
function updateSSEStatus(status) {
switch (status) {
case "connected":
sseStatus.textContent = "Conectado";
sseStatus.className = "sse-status sse-connected";
break;
case "connecting":
sseStatus.textContent = "Conectando";
sseStatus.className = "sse-status sse-connecting";
break;
case "disconnected":
sseStatus.textContent = "Desconectado";
sseStatus.className = "sse-status sse-disconnected";
break;
}
}
// --- RENDERIZADO Y ACTUALIZACIÓN DE LA TABLA ---
function renderOrUpdateStatsTable(data) {
const table = document.getElementById("stats-table");
// Si la tabla no existe, la crea
if (!table) {
outputContainer.innerHTML = createStatsTableHTML(data);
return;
}
// Si la tabla ya existe, solo actualiza las celdas
for (const dbKey in data) {
const poolData = data[dbKey];
COLUMN_ORDER.forEach((metric) => {
const cell = document.getElementById(`${dbKey}_${metric}`);
if (cell && cell.textContent != poolData[metric]) {
cell.textContent = poolData[metric];
cell.classList.add("cell-update");
setTimeout(() => cell.classList.remove("cell-update"), 700);
}
});
}
}
function createStatsTableHTML(data) {
let tableHtml = `<table id="stats-table"><thead><tr><th title="Nombre de la Base de Datos">DB Key</th>`;
COLUMN_ORDER.forEach((key) => {
tableHtml += `<th title="${HEADER_TOOLTIPS[key] || ""}">${key}</th>`;
});
tableHtml += `</tr></thead><tbody>`;
for (const dbKey in data) {
const poolData = data[dbKey];
tableHtml += `<tr><td>${dbKey}</td>`;
COLUMN_ORDER.forEach((metric) => {
// Se añade un ID único a cada celda: "DB1_TotalConnections"
tableHtml += `<td id="${dbKey}_${metric}">${
poolData[metric] ?? "N/A"
}</td>`;
});
tableHtml += `</tr>`;
}
tableHtml += `</tbody></table>`;
return tableHtml;
}
// --- MANEJO DE COMANDOS ESTÁTICOS (SIN CAMBIOS) ---
async function loadStaticContent(command) {
contentTitle.textContent = `Resultado del Comando: '${command}'`;
outputContainer.innerHTML = "Cargando...";
try {
const response = await fetch(`/manager?command=${command}`);
const responseText = await response.text();
if (!response.ok)
throw new Error(
`Error del servidor (${response.status}): ${responseText}`
);
const contentType = response.headers.get("content-type");
if (contentType && contentType.includes("application/json")) {
const data = JSON.parse(responseText);
if (command === "slowqueries") {
outputContainer.innerHTML = data.message
? `<p>${data.message}</p>`
: createTableFromJSON(data.data);
} else {
outputContainer.textContent = JSON.stringify(data, null, 2);
}
} else {
outputContainer.textContent = responseText;
}
} catch (error) {
outputContainer.textContent = `Error al procesar la respuesta:\n${error.message}`;
}
}
function createTableFromJSON(data) {
if (!data || data.length === 0)
return "<p>No se encontraron queries lentas.</p>";
const headers = Object.keys(data[0]);
let table = "<table><thead><tr>";
headers.forEach((h) => (table += `<th>${h.replace(/_/g, " ")}</th>`));
table += "</tr></thead><tbody>";
data.forEach((row) => {
table += "<tr>";
headers.forEach((h) => (table += `<td>${row[h]}</td>`));
table += "</tr>";
});
table += "</tbody></table>";
return table;
}
// --- EVENT LISTENER PRINCIPAL ---
document.getElementById("main-nav").addEventListener("click", (event) => {
if (event.target.tagName === "A") {
const command = event.target.dataset.command;
if (!command) return;
if (command === "reload") {
// Pedimos al usuario la DB Key. Si la deja vacía, se asume recarga total.
const dbKey = prompt(
"Ingrese la llave de la DB a recargar (ej: DB2, DB3). Deje vacío para recargar TODAS:",
""
);
if (dbKey === null) {
// El usuario presionó Cancelar o cerró la ventana. NO HACER NADA.
outputContainer.textContent = "Recarga cancelada por el usuario.";
contentTitle.textContent = "Administración";
return;
}
let finalCommand = "reload";
if (dbKey && dbKey.trim() !== "") {
// Si el usuario especificó una DB (ej. DB2), construimos el comando con el parámetro 'db'.
const key = dbKey.toUpperCase().trim();
finalCommand = `reload&db=${key}`;
outputContainer.innerHTML = `<p style="font-family: sans-serif;">Intentando recargar: <b>${key}</b> (Hot-Swap)...</p>`;
} else {
// Recarga total.
outputContainer.innerHTML = `<p style="font-family: sans-serif;">Intentando recargar: <b>TODAS</b> (Hot-Swap)...</p>`;
}
disconnectSSE();
// Llamamos a la función loadStaticContent con el comando completo (ej: 'reload&db=DB2' o 'reload')
loadStaticContent(finalCommand);
} else if (command === "getstats") {
contentTitle.textContent = `Estadísticas del Pool en Tiempo Real`;
connectSSE();
} else {
// Si se selecciona cualquier otro comando, se desconecta del SSE
disconnectSSE();
loadStaticContent(command);
}
}
});
</script>
</body>
</html>

18
GlobalParameters.bas Normal file
View File

@@ -0,0 +1,18 @@
B4J=true
Group=Default Group
ModulesStructureVersion=1
Type=StaticCode
Version=8.8
@EndOfDesignText@
Sub Process_Globals
Public javaExe As String
' Public WorkingDirectory As String ="C:\jrdcinterface"
Public WorkingDirectory As String = File.DirApp
Public IsPaused As Int = 0
Public mpLogs As Map
Public mpTotalRequests As Map
Public mpTotalConnections As Map
Public mpBlockConnection As Map
Public ActiveRequestsCountByDB As Map ' Mapa para contar las peticiones activas por DB
End Sub

347
HikariConnectionPool.bas Normal file
View File

@@ -0,0 +1,347 @@
B4J=true
Group=Default Group
ModulesStructureVersion=1
Type=Class
Version=6.01
@EndOfDesignText@
'Class module
'Author: Oliver Ackermann
'Created on: 2018/05/07
'Based on: https://github.com/AnywhereSoftware/B4J_Server/blob/master/src/anywheresoftware/b4j/object/ConnectionPool.java
'Resources:
' https://github.com/brettwooldridge/HikariCP
' addDataSourceProperty - individual driver property
' setDataSourceProperties - Java Property values
Sub Class_Globals
Private poolJO As JavaObject
Private poolProperties As Map
Private PoolSizeConfig As Int
Private MinIdleConfig As Int
Private MaxLifeConfig As Long
Private ConnTimeoutConfig As Long
Private LeakDetectionThresholdConfig As Long
Private KeepAliveTimeConfig As Long
End Sub
Public Sub Initialize
Dim properties As Map
properties.Initialize
'
'Frequently used
properties.Put("autocommit", "boolean")
properties.Put("connectiontimeout", "int")
properties.Put("idletimeout", "int")
properties.Put("maxlifetime", "int")
properties.Put("connectiontestquery", "string")
properties.Put("minimumidle", "int")
properties.Put("maximumpoolsize", "int")
properties.Put("poolname", "string")
properties.Put("keepalivetime", "int")
'Infrequently used
properties.Put("initializationfailtimeout", "int")
properties.Put("isolateinternalqueries", "boolean")
properties.Put("allowpoolsuspension", "boolean")
properties.Put("readonly", "boolean")
properties.Put("registermbeans", "boolean")
properties.Put("connectioninitsql", "string")
properties.Put("validationtimeout", "int")
properties.Put("leakdetectionthreshold", "int")
poolProperties = properties
End Sub
'Creates the pool.
' driverClass - The JDBC driver class.
' jdbcUrl - JDBC connection url.
' aUser / aPassword - Connection credentials.
Public Sub CreatePool(driverClass As String, jdbcUrl As String, aUser As String, aPassword As String)
CreatePool2(driverClass, jdbcUrl, aUser, aPassword, 0)
End Sub
'Creates the pool.
' driverClass - The JDBC driver class.
' jdbcUrl - JDBC connection url.
' aUser / aPassword - Connection credentials.
' poolSize - Maximum size of connection pool. Pool's default used when < 1.
Public Sub CreatePool2(driverClass As String, jdbcUrl As String, aUser As String, aPassword As String, poolSize As Int)
Dim jo As JavaObject
poolJO = jo.InitializeNewInstance("com.zaxxer.hikari.HikariDataSource", Null)
poolJO.RunMethod("setDriverClassName",Array As Object(driverClass))
poolJO.RunMethod("setJdbcUrl", Array As Object(jdbcUrl))
poolJO.RunMethod("setUsername", Array As Object(aUser))
poolJO.RunMethod("setPassword", Array As Object(aPassword))
If poolSize > 0 Then poolJO.RunMethod("setMaximumPoolSize", Array As Object(poolSize))
End Sub
Public Sub CreatePool3(options As Map)
CreatePool2(options.Get("driver"), options.Get("url"), options.Get("user"), options.Get("password"), options.Get("poolsize"))
End Sub
'https://github.com/openbouquet/HikariCP/blob/master/src/main/java/com/zaxxer/hikari/HikariConfig.java
'HikariConfig can use Properties object. But this must be done before creating the pool. The pool than is created using
'that HikariConfig object.
Public Sub SetProperties(properties As Map)
' Valores óptimos por defecto para HikariCP (en milisegundos)
Private const DEFAULT_MAX_LIFE As Long = 1750000
Private const DEFAULT_CONN_TIMEOUT As Long = 30000
Private const DEFAULT_LEAK_THR As Long = 35000
Private const DEFAULT_REG_MBEANS As Boolean = True
Private const DEFAULT_MIN_IDLE As Int = 10
Private const DEFAULT_MAX_SIZE As Int = 10
Private const DEFAULT_KEEP_ALIVE_TIME As Long = 300000
Dim rawValue As String
Dim processedKeys As List
processedKeys.Initialize
Dim maxSize As Int ' Variable temporal para el tamaño máximo calculado
' --- INICIO: Procesamiento de Propiedades Críticas ---
' 1. MaxLifetime (Long) - Clave: maxlifetime
Dim maxLife As Long
If properties.ContainsKey("maxlifetime") Then
rawValue = properties.Get("maxlifetime").As(String).Trim
maxLife = rawValue
poolJO.RunMethod("setMaxLifetime", Array As Object(maxLife))
Log($"Ponemos (LONG, Config): MaxLifetime, ${maxLife}"$)
Else
maxLife = DEFAULT_MAX_LIFE
poolJO.RunMethod("setMaxLifetime", Array As Object(maxLife))
Log($"Ponemos (LONG, Default): MaxLifetime, ${DEFAULT_MAX_LIFE}"$)
End If
processedKeys.Add("maxlifetime")
MaxLifeConfig = maxLife ' <-- ALMACENAMIENTO CRÍTICO
' 2. ConnectionTimeout (Long) - Clave: connectiontimeout
Dim connTimeout As Long
If properties.ContainsKey("connectiontimeout") Then
rawValue = properties.Get("connectiontimeout").As(String).Trim
connTimeout = rawValue
poolJO.RunMethod("setConnectionTimeout", Array As Object(connTimeout))
Log($"Ponemos (LONG, Config): ConnectionTimeout, ${connTimeout}"$)
Else
connTimeout = DEFAULT_CONN_TIMEOUT
poolJO.RunMethod("setConnectionTimeout", Array As Object(connTimeout))
Log($"Ponemos (LONG, Default): ConnectionTimeout, ${DEFAULT_CONN_TIMEOUT}"$)
End If
processedKeys.Add("connectiontimeout")
ConnTimeoutConfig = connTimeout ' <-- ALMACENAMIENTO CRÍTICO
' 3. LeakDetectionThreshold (Long) - Clave: leakdetectionthreshold
Dim leakThr As Long
If properties.ContainsKey("leakdetectionthreshold") Then
rawValue = properties.Get("leakdetectionthreshold").As(String).Trim
leakThr = rawValue
poolJO.RunMethod("setLeakDetectionThreshold", Array As Object(leakThr))
Log($"Ponemos (LONG, Config): LeakDetectionThreshold, ${leakThr}"$)
Else
leakThr = DEFAULT_LEAK_THR
poolJO.RunMethod("setLeakDetectionThreshold", Array As Object(leakThr))
Log($"Ponemos (LONG, Default): LeakDetectionThreshold, ${DEFAULT_LEAK_THR}"$)
End If
processedKeys.Add("leakdetectionthreshold")
LeakDetectionThresholdConfig = leakThr ' <-- ALMACENAMIENTO CRÍTICO
' ********** LÓGICA DE FIXED POOL: MaximumPoolSize primero **********
' 4. MaximumPoolSize (Int) - Clave: maximumpoolsize
If properties.ContainsKey("maximumpoolsize") Then
rawValue = properties.Get("maximumpoolsize").As(String).Trim
maxSize = rawValue.As(Int)
poolJO.RunMethod("setMaximumPoolSize", Array As Object(maxSize))
Log($"Ponemos (INT, Config): MaximumPoolSize, ${maxSize}"$)
Else
maxSize = DEFAULT_MAX_SIZE
poolJO.RunMethod("setMaximumPoolSize", Array As Object(DEFAULT_MAX_SIZE))
Log($"Ponemos (INT, Default): MaximumPoolSize, ${DEFAULT_MAX_SIZE}"$)
End If
processedKeys.Add("maximumpoolsize")
PoolSizeConfig = maxSize ' <-- ALMACENAMIENTO CRÍTICO
' 5. MinimumIdle (Int) - Clave: minimumidle
Dim minIdleFinal As Int
If properties.ContainsKey("minimumidle") Then
rawValue = properties.Get("minimumidle").As(String).Trim
minIdleFinal = rawValue.As(Int)
poolJO.RunMethod("setMinimumIdle", Array As Object(minIdleFinal))
Log($"Ponemos (INT, Config): MinimumIdle, ${minIdleFinal}"$)
Else
' APLICAMOS EL FIXED POOL AXIOM: MinimumIdle = MaximumPoolSize (maxSize)
minIdleFinal = maxSize
poolJO.RunMethod("setMinimumIdle", Array As Object(minIdleFinal))
Log($"Ponemos (INT, Fixed Default): MinimumIdle, ${minIdleFinal} (Igual a MaximumPoolSize)"$)
End If
processedKeys.Add("minimumidle")
MinIdleConfig = minIdleFinal ' <-- ALMACENAMIENTO CRÍTICO
' ********** FIN DE LA LÓGICA DE FIXED POOL **********
' 6. RegisterMbeans (Boolean) - Clave: registermbeans
If properties.ContainsKey("registermbeans") Then
Dim regMbeans As Boolean = properties.Get("registermbeans")
poolJO.RunMethod("setRegisterMbeans", Array As Object(regMbeans))
Log($"Ponemos (BOOL, Config): RegisterMbeans, ${regMbeans}"$)
Else
poolJO.RunMethod("setRegisterMbeans", Array As Object(DEFAULT_REG_MBEANS))
Log($"Ponemos (BOOL, Default): RegisterMbeans, ${DEFAULT_REG_MBEANS}"$)
End If
processedKeys.Add("registermbeans")
' 7. KeepaliveTime (Long) - Clave: keepalivetime
Dim keepAlive As Long
If properties.ContainsKey("keepalivetime") Then
rawValue = properties.Get("keepalivetime").As(String).Trim
keepAlive = rawValue
' El valor mínimo aceptado es 30,000 ms [4].
If keepAlive < 30000 Then keepAlive = 30000
poolJO.RunMethod("setKeepaliveTime", Array As Object(keepAlive))
Log($"Ponemos (LONG, Config): KeepaliveTime, ${keepAlive}"$)
Else
keepAlive = DEFAULT_KEEP_ALIVE_TIME
poolJO.RunMethod("setKeepaliveTime", Array As Object(keepAlive))
Log($"Ponemos (LONG, Default): KeepaliveTime, ${DEFAULT_KEEP_ALIVE_TIME} (50 minutos)"$)
End If
processedKeys.Add("keepalivetime")
KeepAliveTimeConfig = keepAlive ' <-- ALMACENAMIENTO CRÍTICO
' --- INICIO: PROCESAMIENTO DE PROPIEDADES RESTANTES ---
' ... (El resto del código de procesamiento de propiedades restantes se mantiene igual)
Dim intValue As Int
Dim booleanValue As Boolean
Dim variableType As String
For Each k As String In properties.Keys
If processedKeys.IndexOf(k) = -1 Then
If poolProperties.ContainsKey(k) Then
variableType = poolProperties.Get(k)
Dim valueToUse As String = properties.Get(k)
If variableType = "string" Then
valueToUse = valueToUse.Trim
End If
Dim dynamicMethodName As String = $"set${k}"$
Try
If variableType = "int" Then
intValue = valueToUse
poolJO.RunMethod(dynamicMethodName, Array As Object(intValue))
Else If variableType = "string" Then
poolJO.RunMethod(dynamicMethodName, Array As Object(valueToUse))
Else If variableType = "boolean" Then
booleanValue = valueToUse
GetPoolJO.RunMethod(dynamicMethodName, Array As Object(booleanValue))
Else
Log($"Connection pool property ${k} has unsupported variable type of ${variableType}"$)
End If
Log($"Ponemos (Restante): ${k}, ${properties.Get(k)}"$)
Catch
Log($"Warning (Restante): Method: ${dynamicMethodName} not matched or type mismatch for property ${k}. Error: ${LastException.Message}"$)
End Try
Else
Log($"Warning: Property ${k} not supported"$)
End If
End If
Next
End Sub
' // ---------------------------------------------------------------------------------------------------------------------------------------------------
' // NUEVA SUBRUTINA: Aplica propiedades específicas del Driver JDBC (Ej: MySQL caching)
' // Estas propiedades se aplican usando addDataSourceProperty, que no se soporta en SetProperties estándar.
' // ---------------------------------------------------------------------------------------------------------------------------------------------------
Public Sub SetDriverProperties(properties As Map)
' properties es el mapa que RDCConnector.LoadDriverProperties extrae (ej: driver.mysql.*).
Dim value As Object
For Each k As String In properties.Keys
value = properties.Get(k)
' CRÍTICO: Usar addDataSourceProperty para configurar el Driver, no el Pool.
poolJO.RunMethod("addDataSourceProperty", Array As Object(k, value))
Log($"HikariCP: Driver Property [${k}] added with value: ${value}"$)
Next
End Sub
'Check if JDBC URL is supported
Public Sub SupportUrl(jdbcUrl As String) As Boolean
If jdbcUrl.StartsWith("jdbc:sqlite") Then
Log("Error: Hikari connection pool does not support SQLite")
Return False
End If
Return True
End Sub
'See if pool's java object supports standard getConnection method
Public Sub IsStandardConnection As Boolean
Return True
End Sub
'Get pool's underlying java object. Used if IsStandardConnection is True
Public Sub GetPoolJO As JavaObject
Return poolJO
End Sub
'Retrieves a connection from the pool. Make sure to close the connection when you are done with it.
Public Sub GetConnection As SQL
Dim newSQL As SQL
Dim jo As JavaObject = newSQL
jo.SetField("connection", poolJO.RunMethod("getConnection", Null))
Return newSQL
End Sub
'Closes all unused pooled connections.
Public Sub ClosePool
If poolJO.IsInitialized Then
poolJO.RunMethod("close", Null)
End If
End Sub
' ** ADVERTENCIA: Esta subrutina traduce los nombres de métodos internos de HikariCP
' ** a las claves genéricas (ej. BusyConnections) que RDCConnector espera.
' ** CRÍTICO: Ahora incluye la CONFIGURACIÓN ESTÁTICA guardada en las variables de clase.
Public Sub GetStats As Map
Dim stats As Map
stats.Initialize
' 1. AGREGAR PROPIEDADES ESTÁTICAS (CONFIGURACIÓN ALMACENADA)
stats.Put("MaxPoolSize", PoolSizeConfig)
stats.Put("MinPoolSize", MinIdleConfig)
stats.Put("MaxLifetime", MaxLifeConfig)
stats.Put("ConnectionTimeout", ConnTimeoutConfig)
stats.Put("LeakDetectionThreshold", LeakDetectionThresholdConfig)
stats.Put("KeepaliveTime", KeepAliveTimeConfig)
' Nota: Aquí puedes agregar las propiedades estáticas adicionales que sean relevantes.
' 2. OBTENER MÉTRICAS DE TIEMPO DE EJECUCIÓN (JMX/MBean)
If poolJO.IsInitialized Then
Try
Dim poolMBean As JavaObject = poolJO.RunMethod("getHikariPoolMXBean", Null)
Dim busyConn As Object = poolMBean.RunMethod("getActiveConnections", Null)
stats.Put("BusyConnections", busyConn)
Dim idleConn As Object = poolMBean.RunMethod("getIdleConnections", Null)
stats.Put("IdleConnections", idleConn)
Dim activeCount As Int = busyConn
Dim idleCount As Int = idleConn
stats.Put("TotalConnections", activeCount + idleCount)
Dim awaitingConn As Object = poolMBean.RunMethod("getThreadsAwaitingConnection", Null)
stats.Put("ThreadsAwaitingConnection", awaitingConn)
Catch
Dim ErrorMsg As String = "HikariCP.GetStats: Error al obtener métricas dinámicas del MBean: " & LastException.Message
Log(ErrorMsg)
stats.Put("Error_Runtime", ErrorMsg)
End Try
Else
stats.Put("Error", "Pool JO no inicializado")
End If
Return stats
End Sub

23
LoginHandler.bas Normal file
View File

@@ -0,0 +1,23 @@
B4J=true
Group=Default Group
ModulesStructureVersion=1
Type=Class
Version=10.3
@EndOfDesignText@
'Class module: LoginHandler
Sub Class_Globals
End Sub
Public Sub Initialize
End Sub
Public Sub Handle(req As ServletRequest, resp As ServletResponse)
resp.ContentType = "text/html"
' Suponiendo que el archivo login.html está en la carpeta www de tu proyecto
Try
resp.Write(File.ReadString(File.DirApp, "www/login.html"))
Catch
Log("Error: No se encontró el archivo www/login.html")
resp.Write("Error: Archivo de login no encontrado.")
End Try
End Sub

17
LogoutHandler.bas Normal file
View File

@@ -0,0 +1,17 @@
B4J=true
Group=Default Group
ModulesStructureVersion=1
Type=Class
Version=10.3
@EndOfDesignText@
'Class module: LogoutHandler
Sub Class_Globals
End Sub
Public Sub Initialize
End Sub
Public Sub Handle(req As ServletRequest, resp As ServletResponse)
req.GetSession.Invalidate ' Cierra la sesión
resp.SendRedirect("/login") ' Manda al usuario a la página de login
End Sub

568
Manager.bas Normal file
View File

@@ -0,0 +1,568 @@
B4J=true
Group=Default Group
ModulesStructureVersion=1
Type=Class
Version=10.3
@EndOfDesignText@
' Módulo de clase: Manager
' Este handler proporciona un panel de administración web para el servidor jRDC2-Multi.
' Permite monitorear el estado del servidor, recargar configuraciones de bases de datos,
' ver estadísticas de rendimiento, reiniciar servicios externos, y gestionar la autenticación de usuarios.
Sub Class_Globals
' Objeto para generar respuestas JSON. Se utiliza para mostrar mapas de datos de forma legible.
Dim j As JSONGenerator
' La clase BCrypt no se usa directamente en este módulo, pero se mantiene si hubiera planes futuros.
' Private bc As BCrypt
End Sub
' Subrutina de inicialización de la clase. Se llama cuando se crea un objeto de esta clase.
Public Sub Initialize
' No se requiere inicialización específica para esta clase en este momento.
End Sub
' Método principal que maneja las peticiones HTTP para el panel de administración.
' req: El objeto ServletRequest que contiene la información de la petición entrante.
' resp: El objeto ServletResponse para construir y enviar la respuesta al cliente.
' Módulo de clase: Manager
' ... (tu código de Class_Globals e Initialize se queda igual) ...
' Método principal que maneja las peticiones HTTP para el panel de administración.
' Refactorizado para funcionar como una API con un frontend estático.
Sub Handle(req As ServletRequest, resp As ServletResponse)
' --- 1. Bloque de Seguridad ---
If req.GetSession.GetAttribute2("user_is_authorized", False) = False Then
resp.SendRedirect("/login")
Return
End If
Dim Command As String = req.GetParameter("command")
' --- 2. Servidor de la Página Principal ---
If Command = "" Then
Try
resp.ContentType = "text/html; charset=utf-8"
resp.Write(File.ReadString(File.DirApp, "www/manager.html"))
Catch
resp.SendError(500, "Error: No se pudo encontrar el archivo principal del panel (www/manager.html). " & LastException.Message)
End Try
Return
End If
' --- 3. Manejo de Comandos como API ---
Select Command.ToLowerCase
' --- Comandos que devuelven JSON (Métricas del Pool) ---
Case "getstatsold"
resp.ContentType = "application/json; charset=utf-8"
Dim allPoolStats As Map
allPoolStats.Initialize
For Each dbKey As String In Main.listaDeCP
Dim connector As RDCConnector = Main.Connectors.Get(dbKey)
If connector.IsInitialized Then
allPoolStats.Put(dbKey, connector.GetPoolStats)
Else
allPoolStats.Put(dbKey, CreateMap("Error": "Conector no inicializado"))
End If
Next
j.Initialize(allPoolStats)
resp.Write(j.ToString)
Return
Case "getstats"
resp.ContentType = "application/json; charset=utf-8"
Dim allPoolStats As Map
' Leemos del caché global actualizado por el Timer SSE
allPoolStats = Main.LatestPoolStats
For Each dbKey As String In Main.listaDeCP
If allPoolStats.ContainsKey(dbKey) = False Then
allPoolStats.Put(dbKey, CreateMap("Error": "Métricas no disponibles/Pool no inicializado"))
End If
Next
j.Initialize(allPoolStats)
resp.Write(j.ToString)
Return
Case "slowqueries"
resp.ContentType = "application/json; charset=utf-8"
Dim results As List
results.Initialize
Try
' Verifica la existencia de la tabla de logs antes de consultar
Dim tableExists As Boolean = Main.SQL1.ExecQuerySingleResult($"SELECT name FROM sqlite_master WHERE type='table' AND name='query_logs';"$) <> Null
If tableExists = False Then
j.Initialize(CreateMap("message": "La tabla de logs ('query_logs') no existe. Habilita 'enableSQLiteLogs=1' en la configuración."))
resp.Write(j.ToString)
Return
End If
' Consulta las 20 queries más lentas de la última hora
Dim oneHourAgoMs As Long = DateTime.Now - 3600000
Dim rs As ResultSet = Main.SQL1.ExecQuery($"SELECT query_name, duration_ms, datetime(timestamp / 1000, 'unixepoch', 'localtime') as timestamp_local, db_key, client_ip, busy_connections, handler_active_requests FROM query_logs WHERE timestamp >= ${oneHourAgoMs} ORDER BY duration_ms DESC LIMIT 20"$)
Do While rs.NextRow
Dim row As Map
row.Initialize
row.Put("Query", rs.GetString("query_name"))
row.Put("Duracion_ms", rs.GetLong("duration_ms"))
row.Put("Fecha_Hora", rs.GetString("timestamp_local"))
row.Put("DB_Key", rs.GetString("db_key"))
row.Put("Cliente_IP", rs.GetString("client_ip"))
row.Put("Conexiones_Ocupadas", rs.GetInt("busy_connections"))
row.Put("Peticiones_Activas", rs.GetInt("handler_active_requests"))
results.Add(row)
Loop
rs.Close
Dim root As Map
root.Initialize
root.Put("data", results)
j.Initialize(root)
resp.Write(j.ToString)
Catch
Log("Error CRÍTICO al obtener queries lentas en Manager API: " & LastException.Message)
resp.Status = 500
Dim root As Map
root.Initialize
root.Put("data", results)
j.Initialize(root)
resp.Write(j.ToString)
End Try
Return
Case "logs", "totalrequests", "totalblocked"
resp.ContentType = "application/json; charset=utf-8"
Dim mp As Map
If Command = "logs" And GlobalParameters.mpLogs.IsInitialized Then mp = GlobalParameters.mpLogs
If Command = "totalrequests" And GlobalParameters.mpTotalRequests.IsInitialized Then mp = GlobalParameters.mpTotalRequests
If Command = "totalblocked" And GlobalParameters.mpBlockConnection.IsInitialized Then mp = GlobalParameters.mpBlockConnection
If mp.IsInitialized Then
j.Initialize(mp)
resp.Write(j.ToString)
Else
resp.Write("{}")
End If
Return
' --- Comandos que devuelven TEXTO PLANO ---
Case "ping"
resp.ContentType = "text/plain"
resp.Write($"Pong ($DateTime{DateTime.Now})"$)
Return
Case "reload"
resp.ContentType = "text/plain; charset=utf-8"
Dim sbTemp As StringBuilder
sbTemp.Initialize
' ***** LÓGICA DE RECARGA GRANULAR/SELECTIVA *****
Dim dbKeyToReload As String = req.GetParameter("db").ToUpperCase ' Leer parámetro 'db' opcional (ej: /manager?command=reload&db=DB3)
Dim targets As List ' Lista de DBKeys a recargar.
targets.Initialize
' 1. Determinar el alcance de la recarga (selectiva o total)
If dbKeyToReload.Length > 0 Then
' Recarga selectiva
If Main.listaDeCP.IndexOf(dbKeyToReload) = -1 Then
resp.Write($"ERROR: DBKey '${dbKeyToReload}' no es válida o no está configurada."$)
Return
End If
targets.Add(dbKeyToReload)
sbTemp.Append($"Iniciando recarga selectiva de ${dbKeyToReload} (Hot-Swap)..."$).Append(" " & CRLF)
Else
' Recarga completa (comportamiento por defecto)
targets.AddAll(Main.listaDeCP)
sbTemp.Append($"Iniciando recarga COMPLETA de configuración (Hot-Swap) ($DateTime{DateTime.Now})"$).Append(" " & CRLF)
End If
' 2. Deshabilitar el Timer de logs (si es necesario)
Dim oldTimerState As Boolean = Main.timerLogs.Enabled
If oldTimerState Then
Main.timerLogs.Enabled = False
sbTemp.Append(" -> Timer de limpieza de logs (SQLite) detenido temporalmente.").Append(" " & CRLF)
End If
Dim reloadSuccessful As Boolean = True
Dim oldConnectorsToClose As Map ' Guardaremos los conectores antiguos aquí.
oldConnectorsToClose.Initialize
' 3. Procesar solo los conectores objetivos
For Each dbKey As String In targets
sbTemp.Append($" -> Procesando recarga de ${dbKey}..."$).Append(CRLF)
Dim newRDC As RDCConnector
Try
' Crear el nuevo conector con la configuración fresca
newRDC.Initialize(dbKey)
' Adquirimos el lock para el reemplazo atómico
Main.MainConnectorsLock.RunMethod("lock", Null)
' Guardamos el conector antiguo (si existe)
Dim oldRDC As RDCConnector = Main.Connectors.Get(dbKey)
' Reemplazo atómico en el mapa global compartido
Main.Connectors.Put(dbKey, newRDC)
' Liberamos el bloqueo inmediatamente
Main.MainConnectorsLock.RunMethod("unlock", Null)
' Si había un conector antiguo, lo guardamos para cerrarlo después
If oldRDC.IsInitialized Then
oldConnectorsToClose.Put(dbKey, oldRDC)
End If
' 4. Actualizar el estado de logs (Granular)
Dim enableLogsSetting As Int = newRDC.config.GetDefault("enableSQLiteLogs", 0)
Dim isEnabled As Boolean = (enableLogsSetting = 1)
Main.SQLiteLoggingStatusByDB.Put(dbKey, isEnabled)
sbTemp.Append($" -> ${dbKey} recargado. Logs (config): ${isEnabled}"$).Append(CRLF)
Catch
' Si falla la inicialización del pool, no actualizamos Main.Connectors
' ¡CRÍTICO! Aseguramos que el lock se libere si hubo excepción antes de liberar.
If Main.MainConnectorsLock.RunMethod("isHeldByCurrentThread", Null).As(Boolean) Then
Main.MainConnectorsLock.RunMethod("unlock", Null)
End If
sbTemp.Append($" -> ERROR CRÍTICO al inicializar conector para ${dbKey}: ${LastException.Message}"$).Append(" " & CRLF)
reloadSuccessful = False
Exit
End Try
Next
' 5. Cerrar los pools antiguos liberados (FUERA del Lock)
If reloadSuccessful Then
For Each dbKey As String In oldConnectorsToClose.Keys
Dim oldRDC As RDCConnector = oldConnectorsToClose.Get(dbKey)
oldRDC.Close ' Cierre limpio del pool C3P0
sbTemp.Append($" -> Pool antiguo de ${dbKey} cerrado limpiamente."$).Append(" " & CRLF)
Next
' 6. Re-evaluar el estado global de Logs (CRÍTICO: debe revisar TODAS las DBs)
Main.IsAnySQLiteLoggingEnabled = False
For Each dbKey As String In Main.listaDeCP
' Revisamos el estado de log de CADA conector activo
If Main.SQLiteLoggingStatusByDB.GetDefault(dbKey, False) Then
Main.IsAnySQLiteLoggingEnabled = True
Exit
End If
Next
If Main.IsAnySQLiteLoggingEnabled Then
Main.timerLogs.Enabled = True
sbTemp.Append($" -> Timer de limpieza de logs ACTIVADO (estado global: HABILITADO)."$).Append(" " & CRLF)
Else
Main.timerLogs.Enabled = False
sbTemp.Append($" -> Timer de limpieza de logs DESHABILITADO (estado global: DESHABILITADO)."$).Append(" " & CRLF)
End If
sbTemp.Append($"¡Recarga de configuración completada con éxito!"$).Append(" " & CRLF)
Else
' Si falló, restauramos el estado del timer anterior.
If oldTimerState Then
Main.timerLogs.Enabled = True
sbTemp.Append(" -> Restaurando Timer de limpieza de logs al estado ACTIVO debido a fallo en recarga.").Append(" " & CRLF)
End If
sbTemp.Append($"¡ERROR: La recarga de configuración falló! Los conectores antiguos siguen activos."$).Append(" " & CRLF)
End If
resp.Write(sbTemp.ToString)
Return
Case "test"
resp.ContentType = "text/plain; charset=utf-8"
Dim sb As StringBuilder
sb.Initialize
sb.Append("--- INICIANDO PRUEBA DE CONECTIVIDAD A TODOS LOS POOLS CONFIGURADOS ---").Append(CRLF).Append(CRLF)
' Iteramos sobre la lista de DB Keys cargadas al inicio (DB1, DB2, etc.)
For Each dbKey As String In Main.listaDeCP
Dim success As Boolean = False
Dim errorMsg As String = ""
Dim con As SQL ' Conexión para la prueba
Try
' 1. Obtener el RDCConnector para esta DBKey
Dim connector As RDCConnector = Main.Connectors.Get(dbKey)
If connector.IsInitialized = False Then
errorMsg = "Conector no inicializado (revisa logs de AppStart)"
Else
' 2. Forzar la adquisición de una conexión del pool C3P0
con = connector.GetConnection(dbKey)
If con.IsInitialized Then
' 3. Si la conexión es válida, la cerramos inmediatamente para devolverla al pool
con.Close
success = True
Else
errorMsg = "La conexión devuelta no es válida (SQL.IsInitialized = False)"
End If
End If
Catch
' Capturamos cualquier excepción (ej. fallo de JDBC, timeout de C3P0)
errorMsg = LastException.Message
End Try
If success Then
sb.Append($"* ${dbKey}: Conexión adquirida y liberada correctamente."$).Append(CRLF)
Else
' Si falla, registramos el error para el administrador.
Main.LogServerError("ERROR", "Manager.TestCommand", $"Falló la prueba de conectividad para ${dbKey}: ${errorMsg}"$, dbKey, "test_command", req.RemoteAddress)
sb.Append($"[FALLO] ${dbKey}: ERROR CRÍTICO al obtener conexión. Mensaje: ${errorMsg}"$).Append(CRLF)
End If
Next
sb.Append(CRLF).Append("--- FIN DE PRUEBA DE CONEXIONES ---").Append(CRLF)
' Mantenemos la lista original de archivos de configuración cargados (esto es informativo)
sb.Append(CRLF).Append("Archivos de configuración cargados:").Append(CRLF)
For Each item As String In Main.listaDeCP
Dim configName As String = "config"
If item <> "DB1" Then configName = configName & "." & item
sb.Append($" -> Usando ${configName}.properties"$).Append(CRLF)
Next
resp.Write(sb.ToString)
Return
Case "rsx", "rpm2", "revivebow", "restartserver"
resp.ContentType = "text/plain; charset=utf-8"
Dim batFile As String
Select Command
Case "rsx": batFile = "start.bat"
Case "rpm2": batFile = "reiniciaProcesoPM2.bat"
Case "reviveBow": batFile = "reiniciaProcesoBow.bat"
Case "restartserver": batFile = "restarServer.bat" ' Nota: este bat no estaba definido, se usó el nombre del comando
End Select
Log($"Ejecutando ${File.DirApp}\${batFile}"$)
Try
Dim shl As Shell
shl.Initialize("shl","cmd", Array("/c", File.DirApp & "\" & batFile & " " & Main.srvr.Port))
shl.WorkingDirectory = File.DirApp
shl.Run(-1)
resp.Write($"Comando '${Command}' ejecutado. Script invocado: ${batFile}"$)
Catch
resp.Write($"Error al ejecutar el script para '${Command}': ${LastException.Message}"$)
End Try
Return
Case "paused", "continue"
resp.ContentType = "text/plain; charset=utf-8"
If Command = "paused" Then
GlobalParameters.IsPaused = 1
resp.Write("Servidor pausado.")
Else
GlobalParameters.IsPaused = 0
resp.Write("Servidor reanudado.")
End If
Return
Case "block", "unblock"
resp.ContentType = "text/plain; charset=utf-8"
Dim ip As String = req.GetParameter("IP")
If ip = "" Then
resp.Write("Error: El parámetro IP es requerido.")
Return
End If
If GlobalParameters.mpBlockConnection.IsInitialized Then
If Command = "block" Then
GlobalParameters.mpBlockConnection.Put(ip, ip)
resp.Write($"IP bloqueada: ${ip}"$)
Else
GlobalParameters.mpBlockConnection.Remove(ip)
resp.Write($"IP desbloqueada: ${ip}"$)
End If
Else
resp.Write("Error: El mapa de bloqueo no está inicializado.")
End If
Return
Case "getconfiginfo"
resp.ContentType = "text/plain; charset=utf-8"
Dim sbInfo As StringBuilder
sbInfo.Initialize
Dim allKeys As List
allKeys.Initialize
allKeys.AddAll(Main.listaDeCP)
sbInfo.Append("======================================================================").Append(CRLF)
sbInfo.Append($"=== CONFIGURACIÓN jRDC2-Multi V$1.2{Main.VERSION} (ACTIVA) ($DateTime{DateTime.Now}) ==="$).Append(CRLF)
sbInfo.Append("======================================================================").Append(CRLF).Append(CRLF)
' ***** GLOSARIO DE PARÁMETROS CONFIGURABLES *****
sbInfo.Append("### GLOSARIO DE PARÁMETROS PERMITIDOS EN CONFIG.PROPERTIES (HIKARICP) ###").Append(CRLF)
sbInfo.Append("--------------------------------------------------").Append(CRLF)
sbInfo.Append("DriverClass: Clase del driver JDBC (ej: oracle.jdbc.driver.OracleDriver).").Append(CRLF)
sbInfo.Append("JdbcUrl: URL de conexión a la base de datos (IP, puerto, servicio).").Append(CRLF)
sbInfo.Append("User/Password: Credenciales de acceso a la BD.").Append(CRLF)
sbInfo.Append("ServerPort: Puerto de escucha del servidor B4J (solo lo toma de config.properties).").Append(CRLF)
sbInfo.Append("Debug: Si es 'true', los comandos SQL se recargan en cada petición (DESHABILITADO, USAR COMANDO RELOAD).").Append(CRLF)
sbInfo.Append("parameterTolerance: Define si se recortan (1) o se rechazan (0) los parámetros SQL sobrantes a los requeridos por el query.").Append(CRLF)
sbInfo.Append("enableSQLiteLogs: Control granular. Habilita (1) o deshabilita (0) la escritura de logs en users.db para esta DB.").Append(CRLF)
' --- Parámetros de HIKARICP (Foco en el mínimo set de tuning) ---
sbInfo.Append("pool.hikari.maximumPoolSize: Máximo de conexiones simultáneas permitido. (Recomendado N*Cores DB),").Append(CRLF)
sbInfo.Append("pool.hikari.minimumIdle: Mínimo de conexiones inactivas. Recomendado igual a maximumPoolSize para pool de tamaño fijo,").Append(CRLF)
sbInfo.Append("pool.hikari.maxLifetime (ms): Tiempo máximo de vida de una conexión. CRÍTICO: Debe ser menor que el timeout del firewall/DB,").Append(CRLF)
sbInfo.Append("pool.hikari.connectionTimeout (ms): Tiempo máximo de espera del cliente por una conexión disponible (Default: 30000 ms),").Append(CRLF)
sbInfo.Append("pool.hikari.idleTimeout (ms): Tiempo inactivo antes de retirar la conexión (ms). Solo aplica si minimumIdle < maximumPoolSize,").Append(CRLF)
sbInfo.Append("pool.hikari.leakDetectionThreshold (ms): Umbral (ms) para detectar conexiones no devueltas (fugas).").Append(CRLF)
sbInfo.Append(CRLF)
For Each dbKey As String In allKeys
' --- COMIENZA EL DETALLE POR CONECTOR ---
Dim connector As RDCConnector = Main.Connectors.Get(dbKey)
sbInfo.Append("--------------------------------------------------").Append(CRLF).Append(CRLF)
sbInfo.Append($"---------------- ${dbKey} ------------------"$).Append(CRLF).Append(CRLF)
If connector.IsInitialized Then
Dim configMap As Map = connector.config
' Obtener las métricas y la configuración REAL aplicada por HikariCP
Dim poolStats As Map = connector.GetPoolStats
sbInfo.Append($"DriverClass: ${configMap.GetDefault("DriverClass", "N/A")}"$).Append(CRLF)
sbInfo.Append($"JdbcUrl: ${configMap.GetDefault("JdbcUrl", "N/A")}"$).Append(CRLF)
sbInfo.Append($"User: ${configMap.GetDefault("User", "N/A")}"$).Append(CRLF)
sbInfo.Append($"ServerPort: ${configMap.GetDefault("ServerPort", "N/A")}"$).Append(CRLF).Append(CRLF)
sbInfo.Append("--- CONFIGURACIÓN DEL POOL (HIKARICP - Valores Aplicados) ---").Append(CRLF)
sbInfo.Append($"MaximumPoolSize (Aplicado): ${poolStats.GetDefault("MaxPoolSize", 10).As(Int)}"$).Append(CRLF)
sbInfo.Append($"MinimumIdle (Aplicado): ${poolStats.GetDefault("MinPoolSize", 10).As(Int)}"$).Append(CRLF)
' Reportamos los timeouts en Milisegundos (ms)
sbInfo.Append($"MaxLifetime (ms): ${poolStats.GetDefault("MaxLifetime", 1800000).As(Long)}"$).Append(CRLF)
sbInfo.Append($"ConnectionTimeout (ms): ${poolStats.GetDefault("ConnectionTimeout", 30000).As(Long)}"$).Append(CRLF)
sbInfo.Append($"IdleTimeout (ms): ${poolStats.GetDefault("IdleTimeout", 600000).As(Long)}"$).Append(CRLF)
sbInfo.Append($"LeakDetectionThreshold (ms): ${poolStats.GetDefault("LeakDetectionThreshold", 0).As(Long)}"$).Append(CRLF).Append(CRLF)
' *** NUEVA SECCIÓN: PROPIEDADES ESPECÍFICAS DEL DRIVER ***
If connector.driverProperties.IsInitialized And connector.driverProperties.Size > 0 Then
sbInfo.Append("--- PROPIEDADES DE RENDIMIENTO DEL DRIVER JDBC (Optimización de Sentencias) ---").Append(CRLF)
For Each propKey As String In connector.driverProperties.Keys
Dim propValue As Object = connector.driverProperties.Get(propKey)
sbInfo.Append($"[Driver] ${propKey}: ${propValue}"$).Append(CRLF)
Next
sbInfo.Append(CRLF)
End If
' *** FIN DE LA NUEVA SECCIÓN ***
' Reportamos métricas de runtime del pool (si están disponibles).
sbInfo.Append("--- ESTADO DE RUNTIME (Métricas Dinámicas) ---").Append(CRLF)
sbInfo.Append($"Total Connections: ${poolStats.GetDefault("TotalConnections", "N/A")}"$).Append(CRLF)
sbInfo.Append($"Busy Connections: ${poolStats.GetDefault("BusyConnections", "N/A")}"$).Append(CRLF)
sbInfo.Append($"Idle Connections: ${poolStats.GetDefault("IdleConnections", "N/A")}"$).Append(CRLF)
sbInfo.Append($"Handler Active Requests: ${poolStats.GetDefault("HandlerActiveRequests", "N/A")}"$).Append(CRLF).Append(CRLF)
sbInfo.Append("--- COMPORTAMIENTO ---").Append(CRLF)
sbInfo.Append($"Debug (Recarga Queries - DESHABILITADO): ${configMap.GetDefault("Debug", "false")}"$).Append(CRLF)
Dim tolerance As Int = configMap.GetDefault("parameterTolerance", 0).As(Int)
sbInfo.Append($"ParameterTolerance: ${tolerance} (0=Estricto, 1=Habilitado)"$).Append(CRLF)
Dim isLogsEnabledRuntime As Boolean = Main.SQLiteLoggingStatusByDB.GetDefault(dbKey, False).As(Boolean)
Dim logsEnabledRuntimeInt As Int = 0
If isLogsEnabledRuntime Then
logsEnabledRuntimeInt = 1
End If
sbInfo.Append($"EnableSQLiteLogs: ${logsEnabledRuntimeInt} (0=Deshabilitado, 1=Habilitado)"$).Append(CRLF)
sbInfo.Append(CRLF)
Else
sbInfo.Append($"ERROR: Conector ${dbKey} no inicializado o falló al inicio."$).Append(CRLF).Append(CRLF)
End If
Next
resp.Write(sbInfo.ToString)
Return
Case "setlogstatus"
Log(123)
resp.ContentType = "text/plain; charset=utf-8"
Dim dbKeyToChange As String = req.GetParameter("db").ToUpperCase
Dim status As String = req.GetParameter("status")
If Main.listaDeCP.IndexOf(dbKeyToChange) = -1 Then
resp.Write($"ERROR: DBKey '${dbKeyToChange}' no es válida."$)
Return
End If
Dim isEnabled As Boolean = (status = "1")
Dim resultMsg As String
' *** 1. Adquisición del Lock (CRÍTICO) ***
Main.MainConnectorsLock.RunMethod("lock", Null)
Try
' 2. Lógica Crítica de Modificación de Estado (Protegida)
Main.SQLiteLoggingStatusByDB.Put(dbKeyToChange, isEnabled)
Private hab As String = "DESHABILITADOS"
If isEnabled Then hab = "HABILITADOS"
resultMsg = $"Logs de ${dbKeyToChange} ${hab} en caliente."$
' 3. Re-evaluación del estado global
Main.IsAnySQLiteLoggingEnabled = False
For Each dbKey As String In Main.listaDeCP
If Main.SQLiteLoggingStatusByDB.GetDefault(dbKey, False) Then
Main.IsAnySQLiteLoggingEnabled = True
Exit
End If
Next
' 4. Ajustar el Timer
If Main.IsAnySQLiteLoggingEnabled Then
If Main.timerLogs.Enabled = False Then Main.timerLogs.Enabled = True
resultMsg = resultMsg & " Timer de limpieza ACTIVADO."
Else
Main.timerLogs.Enabled = False
resultMsg = resultMsg & " Timer de limpieza DESHABILITADO globalmente."
End If
' ** LIBERACIÓN EN CASO DE ÉXITO **
' En el camino de éxito, liberamos inmediatamente antes de que la subrutina termine.
If Main.MainConnectorsLock.RunMethod("isHeldByCurrentThread", Null).As(Boolean) Then
Main.MainConnectorsLock.RunMethod("unlock", Null)
End If
Catch
' 5. Manejo de Excepción y ** LIBERACIÓN EN CASO DE FALLO **
resultMsg = $"ERROR CRÍTICO al modificar el estado de logs: ${LastException.Message}"$
' ¡ESTE ES EL EQUIVALENTE AL FINALLY EN B4X!
' Verificamos si este hilo retiene el lock y, si es así, lo liberamos de inmediato.
If Main.MainConnectorsLock.RunMethod("isHeldByCurrentThread", Null).As(Boolean) Then
Main.MainConnectorsLock.RunMethod("unlock", Null)
End If
End Try
resp.Write(resultMsg)
Return
Case Else
resp.ContentType = "text/plain; charset=utf-8"
resp.SendError(404, $"Comando desconocido: '{Command}'"$)
Return
End Select
End Sub

View File

@@ -0,0 +1,70 @@
B4J=true
Group=Default Group
ModulesStructureVersion=1
Type=StaticCode
Version=10.3
@EndOfDesignText@
' Archivo: ParameterValidationUtils.bas
' Módulo de utilidad: ParameterValidationUtils
' Centraliza la lógica de validación y ajuste de parámetros SQL.
' Ahora soporta recorte de parámetros excesivos.
Sub Process_Globals
' El Type ParameterValidationResult está declarado en Main.bas, no se declara aquí.
End Sub
' Valida y ajusta la lista de parámetros para la ejecución SQL, aplicando la lógica de tolerancia.
' Retorna un ParameterValidationResult indicando éxito/error y los parámetros a usar.
Public Sub ValidateAndAdjustParameters (CommandName As String, DBKey As String, sqlCommand As String, receivedParams As List, IsToleranceEnabled As Boolean) As ParameterValidationResult
Dim res As ParameterValidationResult
res.Initialize
res.Success = True ' Asumimos éxito inicialmente
' Log(">>>> IsToleranceEnabled: " & IsToleranceEnabled)
' Aseguramos que receivedParams esté inicializada, incluso si está vacía o Null
If receivedParams = Null Or receivedParams.IsInitialized = False Then
receivedParams.Initialize ' Inicializa una lista vacía si es Null o no inicializada.
End If
' Contar cuántos '?' hay en la sentencia SQL para saber cuántos parámetros se esperan.
Dim expectedParams As Int = sqlCommand.Length - sqlCommand.Replace("?", "").Length
Dim receivedParamsSize As Int = receivedParams.Size
If receivedParamsSize < expectedParams Then
' Caso 1: Se recibieron MENOS parámetros de los esperados. Esto es un error.
res.Success = False
res.ErrorMessage = $"ERROR: Número de parámetros insuficiente para "${CommandName}" (DB: ${DBKey}). Se esperaban ${expectedParams} y se recibieron ${receivedParamsSize}."$
Log(res.ErrorMessage)
Main.LogServerError("ERROR", "ParameterValidationUtils.ValidateAndAdjustParameters", res.ErrorMessage, DBKey, CommandName, Null) ' <-- Nuevo Log
Return res
Else If receivedParamsSize > expectedParams Then
' Caso 2: Se recibieron MÁS parámetros de los esperados.
If IsToleranceEnabled Then ' Solo recortamos si la tolerancia está habilitada
Dim adjustedParams As List
adjustedParams.Initialize
For i = 0 To expectedParams - 1
adjustedParams.Add(receivedParams.Get(i))
Next
res.ParamsToExecute = adjustedParams
res.Success = True
Dim WarningMsg As String = $"ADVERTENCIA: Se recibieron más parámetros de los esperados para "${CommandName}" (DB: ${DBKey}). Se esperaban ${expectedParams} y se recibieron ${receivedParamsSize}. Se ajustó la lista de parámetros a ${expectedParams} elementos."$
' Log(WarningMsg)
' Log("Cache: " & Main.LOG_CACHE_THRESHOLD & "|" & Main.ErrorLogCache.Size)
Main.LogServerError("ADVERTENCIA", "ParameterValidationUtils.ValidateAndAdjustParameters", WarningMsg, DBKey, CommandName, Null)
Else
' Si la tolerancia NO está habilitada, esto es un error crítico.
res.Success = False
res.ErrorMessage = $"ERROR: Número de parámetros excesivo para "${CommandName}" (DB: ${DBKey}). Se esperaban ${expectedParams} y se recibieron ${receivedParamsSize}. La tolerancia a parámetros extra está DESHABILITADA."$
Log(res.ErrorMessage)
Main.LogServerError("ERROR", "ParameterValidationUtils.ValidateAndAdjustParameters", res.ErrorMessage, DBKey, CommandName, Null)
Return res
End If
Else
' Caso 3: Se recibieron el número EXACTO de parámetros. Todo bien.
res.ParamsToExecute = receivedParams ' Usamos la lista original tal cual.
res.Success = True ' Confirmamos éxito.
End If
Return res
End Sub

253
RDCConnector.bas Normal file
View File

@@ -0,0 +1,253 @@
B4J=true
Group=Default Group
ModulesStructureVersion=1
Type=Class
Version=4.19
@EndOfDesignText@
' Módulo de clase: RDCConnector
' Esta clase gestiona el pool de conexiones a una base de datos específica.
' REFRACTORIZADO: Usa ConnectionPoolManager y delega a HikariCP.
Sub Class_Globals
' --- Variables globales de la clase ---
Private pool As ConnectionPoolManager ' Usa el Manager para la modularidad
Private MyHikariPool As HikariConnectionPool ' <-- NUEVO: Pool dedicado a esta DBKey.
Private DebugQueries As Boolean = False ' Bandera para activar/desactivar el modo de depuración
Public commands As Map ' Comandos SQL cargados
Public serverPort As Int
Public usePool As Boolean = True
Public config As Map ' Configuración completa (JdbcUrl, User, Password, etc.)
Public IsParameterToleranceEnabled As Boolean ' Tolerancia a parámetros de más
Dim driverProperties As Map ' CRÍTICO: Propiedades específicas del driver (MySQL statement caching)
Private configLocation As String ' Ubicación del archivo de configuración
Private poolProperties As Map
End Sub
' Subrutina de inicialización para el conector de una base de datos específica.
Public Sub Initialize(DB As String)
' Nota: Este código asume que MyHikariPool ya está declarado en Class_Globals
Dim dbKey As String = DB ' Usaremos DB como la llave
If DB.EqualsIgnoreCase("DB1") Then dbKey = ""
poolProperties.Initialize
driverProperties.Initialize
' PASO 1: Cargar la configuración desde el archivo .properties correspondiente.
config = LoadConfigMap(dbKey) ' Aseguramos la carga en la variable de clase [1]
' Lectura de la configuración de tolerancia de parámetros
Dim toleranceSetting As Int = config.GetDefault("parameterTolerance", 0).As(Int)
IsParameterToleranceEnabled = (toleranceSetting = 1)
If IsParameterToleranceEnabled Then
Log($"RDCConnector.Initialize para ${dbKey}: Tolerancia a parámetros extras, HABILITADA."$)
Else
Log($"RDCConnector.Initialize para ${dbKey}: Tolerancia a parámetros extras, DESHABILITADA (modo estricto)."$)
End If
' Bloque Try-Catch para la inicialización y configuración del pool.
Try
Dim driverClass As String = config.Get("DriverClass")
Dim jdbcUrl As String = config.Get("JdbcUrl")
Dim aUser As String = config.Get("User")
Dim aPassword As String = config.Get("Password")
Dim poolType As String = "Hikari" ' Forzamos Hikari
' *** INICIO DE LA LÓGICA DE PRECEDENCIA DE TAMAÑO (HIKARI) ***
Dim maxSizeKey As String = $"pool.${poolType.ToLowerCase}.maximumpoolsize"$
Dim poolSizeString As String
Dim poolSize As Int
' Intentamos leer el valor específico (pool.hikari.maximumpoolsize).
If config.ContainsKey(maxSizeKey) Then
poolSizeString = config.Get(maxSizeKey)
poolSize = poolSizeString.As(Int)
Else
' Si no está definido, usamos el default recomendado por Hikari (10). [2]
poolSize = 10
End If
If poolSize < 1 Then poolSize = 10 ' Mantenemos la sensatez
Log($"RDCConnector: Usando MaximumPoolSize para ${poolType} calculado: ${poolSize}"$)
' *** PASO 2: INICIALIZA/CREA EL POOL LOCALMENTE (Decoupling CRÍTICO) ***
If MyHikariPool.IsInitialized = False Then MyHikariPool.Initialize ' Inicializa el wrapper local
' Crea el pool subyacente (DataSource) en esta instancia dedicada. [3]
MyHikariPool.CreatePool2(driverClass, jdbcUrl, aUser, aPassword, poolSize)
' PASO 3a: Cargar y filtrar SOLO las propiedades del Pool (ej. las que comienzan con 'pool.hikari.')
LoadPoolProperties(poolType, config)
' PASO 3b: Aplicar propiedades de ESTABILIDAD (Pool Properties)
If poolProperties.Size > 0 Then
' Aplicación directa al pool local. [4]
CallSub2(MyHikariPool, "SetProperties", poolProperties)
End If
' PASO 4: Cargar propiedades específicas del Driver (ej. Statement Caching)
If config.ContainsKey("DriverShortName") Then
LoadDriverProperties(config.Get("DriverShortName"), config)
End If
' PASO 5: Aplicar propiedades de RENDIMIENTO (Driver Properties)
If driverProperties.Size > 0 Then
' Aplicación directa al pool local. [5]
CallSub2(MyHikariPool, "SetDriverProperties", driverProperties)
Log($"RDCConnector.Initialize para ${DB}: {driverProperties.Size} propiedades del Driver aplicadas a HikariCP."$)
End If
' PASO 6 (Prueba de vida): Forzar la creación de conexiones iniciales y verificar el estado.
' Esto garantiza el fail-fast. [6]
Dim tempCon As SQL = MyHikariPool.GetConnection
If tempCon.IsInitialized Then
tempCon.Close
End If
' Cargar configuración estática en el cache global
Dim dbKeyToStore As String = DB
If dbKeyToStore = "" Then dbKeyToStore = "DB1"
' Almacenamos el mapa completo (configuración estática + métricas dinámicas iniciales) en el cache global.
' GetPoolStats ahora usa MyHikariPool.
Dim initialPoolStats As Map = GetPoolStats
Main.LatestPoolStats.Put(dbKeyToStore, initialPoolStats)
Catch
' Si ocurre un error durante la inicialización del pool o al forzar la conexión.
Dim ErrorMsg As String = $"RDCConnector.Initialize para ${DB}: ERROR CRÍTICO al inicializar/forzar conexión: ${LastException.Message}"$
Log(ErrorMsg)
Main.LogServerError("ERROR", "RDCConnector.Initialize", ErrorMsg, DB, Null, Null)
' Si falla la inicialización, la instancia local MyHikariPool se queda inutilizada.
' Aquí podríamos considerar la opción de llamar a MyHikariPool.ClosePool para asegurar
' que no queden recursos parciales, aunque HikariCP debería manejarse con fail-fast.
End Try
' Carga los comandos SQL predefinidos de esta base de datos en el mapa global 'commandsMap' de Main.
If dbKey = "" Then dbKey = "DB1"
LoadSQLCommands(config, dbKey)
serverPort = config.Get("ServerPort")
End Sub
' Carga el mapa de configuración
Private Sub LoadConfigMap(DB As String) As Map
Private DBX As String = ""
If DB <> "" Then DBX = "." & DB
Log($"RDCConnector.LoadConfigMap: Leemos el config${DBX}.properties"$)
Return File.ReadMap("./", "config" & DBX & ".properties")
End Sub
' Obtiene la sentencia SQL completa para un comando dado.
Public Sub GetCommand(DB As String, Key As String) As String
commands = Main.commandsMap.Get(DB).As(Map)
If commands.ContainsKey("sql." & Key) = False Then
Dim ErrorMsg As String = $"RDCConnector.GetCommand: *** Comando no encontrado: '${Key}' para DB: '${DB}' ***"$
Log(ErrorMsg)
Main.LogServerError("ERROR", "RDCConnector.GetCommand", ErrorMsg, DB, Key, Null)
End If
Return commands.Get("sql." & Key)
End Sub
' Obtiene una conexión SQL funcional del pool de conexiones.
Public Sub GetConnection(DB As String) As SQL
' If DB.EqualsIgnoreCase("DB1") Then DB = ""
' If DebugQueries Then LoadSQLCommands(LoadConfigMap(DB), DB) ' Deshabilitado por defecto. [13]
' Devolvemos la conexión del pool local, si está inicializado.
If MyHikariPool.IsInitialized Then
Return MyHikariPool.GetConnection
Else
Log($"ERROR: Intento de obtener conexión de DBKey ${DB}, pero MyHikariPool no está inicializado."$)
' Devolver Null o lanzar excepción, dependiendo del manejo de errores deseado.
Return Null
End If
' ANTES: Return Main.ConnectionPoolManager1.GetConnection
End Sub
' Carga todos los comandos SQL del mapa de configuración en el mapa global 'commandsMap' de Main.
Private Sub LoadSQLCommands(config2 As Map, DB As String)
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))
End If
Next
commands = newCommands
Main.commandsMap.Put(DB, commands)
End Sub
' ** Delegación de estadísticas de C3P0 a HikariCP **
Public Sub GetPoolStats As Map
Dim stats As Map
stats.Initialize
If MyHikariPool.IsInitialized Then
Try
' 2. Llamamos al método delegado GetStats en el wrapper de HikariCP.
Dim hikariStats As Map = MyHikariPool.GetStats
Return hikariStats
Catch
' Fallo en el método GetStats del wrapper.
Dim ErrorMsg As String = $"RDCConnector.GetPoolStats: ERROR CRÍTICO al obtener estadísticas de HikariCP: ${LastException.Message}"$
Log(ErrorMsg)
stats.Put("Error", LastException.Message)
End Try
Else
stats.Put("Error", "Pool local MyHikariPool no inicializado.")
End If
Return stats
End Sub
' *** NUEVA SUBRUTINA: Cierra el pool de conexiones de forma ordenada ***
Public Sub Close
If MyHikariPool.IsInitialized Then
' Cierre limpio del pool subyacente.
MyHikariPool.ClosePool
Log($"RDCConnector.Close: Pool Hikari cerrado limpiamente para este conector."$)
End If
' Ya NO delegamos el cierre al Manager.
' ANTES: Main.ConnectionPoolManager1.ClosePoolByType(poolType) [15]
End Sub
' --- SUBRUTINAS DE UTILIDAD PARA CARGA DE PROPIEDADES ---
' [2]
Private Sub LoadDriverProperties(driverShortName As String, config_ As Map)
driverProperties = ExtractProperties($"driver.${driverShortName.trim}."$, config_, Null, Null)
End Sub
' [3]
Private Sub ExtractProperties(prefix As String, input As Map, newPrefix As String, output As Map) As Map
Dim properties As Map
If output = Null Or output.IsInitialized = False Then
properties.Initialize
Else
properties = output
End If
If newPrefix.EqualsIgnoreCase(Null) Then newPrefix = ""
Dim prefixLength As Int = prefix.Length
' Log($"Prefijo=${prefix}, ${newPrefix}"$)
For Each k As String In input.Keys
' Log(k)
If k.ToLowerCase.StartsWith(prefix) Then
' Log($"found ${prefix}"$)
Dim standardizedKey As String = k.SubString(prefixLength).ToLowerCase
' Log("Ponemos: " & $"${newPrefix}${k.SubString(prefixLength)}, ${input.Get(k)}"$)
properties.Put($"${newPrefix}${standardizedKey}"$, input.Get(k))
End If
Next
Return properties
End Sub
Private Sub LoadPoolProperties(poolType As String, config_ As Map)
' Busca entradas como 'pool.hikari.<propiedad>' y las extrae.
poolProperties = ExtractProperties($"pool.${poolType.ToLowerCase}."$, config_, Null, Null)
End Sub

161
README0.md Normal file
View File

@@ -0,0 +1,161 @@
# **Servidor jRDC2-Multi Mod (B4J)**
## **1. Introducción**
Este proyecto es una versión modificada del servidor [jRDC2 original](https://www.b4x.com/android/forum/threads/b4x-jrdc2-b4j-implementation-of-rdc-remote-database-connector.61801/#content), diseñada para actuar como un backend robusto y flexible. Su función principal es recibir peticiones HTTP, ejecutar comandos SQL predefinidos contra una base de datos y devolver los resultados en un formato estructurado.
Ha sido adaptado para servir tanto a clientes nativos (`B4A/B4i`) como a clientes web modernos (`JavaScript`, a través de frameworks como `NodeJS, React, Vue, Angular, etc`.).
-----
## **2. Características Principales**
* **Soporte para Múltiples Bases de Datos**: Puede cargar y gestionar hasta 4 archivos de configuración (`config.properties`) simultáneamente.
* **Comandos SQL Externalizados**: Las sentencias SQL se definen en los archivos de configuración, permitiendo modificarlas sin recompilar el servidor.
* **Doble Handler de Peticiones**: Incluye un handler clásico para clientes B4X y un handler JSON para clientes web.
* **Validaciones de Seguridad**: Verifica la existencia de comandos y la correspondencia en el número de parámetros antes de la ejecución.
* **Administración Remota**: Permite verificar el estado, recargar la configuración y reiniciar el servidor a través de URLs específicas, con un sistema de autenticación.
-----
## **3. Configuración**
### **3.1. Archivos de Configuración**
El sistema está preparado para manejar hasta **cuatro configuraciones de bases de datos** (de `DB1` a `DB4`). No es necesario tener los cuatro archivos; el servidor cargará únicamente los que encuentre.
La nomenclatura de los archivos es fundamental:
* `config.properties` (para `DB1`)
* `config.DB2.properties`
* `config.DB3.properties`
* `config.DB4.properties`
**Notas importantes:**
* El **puerto** del servidor se toma **únicamente** del archivo principal `config.properties`, sin importar lo que digan los demás.
* Los datos de conexión (`JdbcUrl`, `usuario`, `contraseña`) sí se toman del archivo correspondiente a cada base de datos.
### **3.2. Añadir Drivers de Bases de Datos Adicionales**
Si necesitas conectarte a otros tipos de bases de datos (ej. Oracle), debes agregar el archivo del controlador .jar al proyecto antes de compilar. En el módulo `Main`, añade una línea como la siguiente:
```b4x
' Este es el nombre del archivo .jar, en este caso "C:\\Ruta\\LibsAdicionales\\ojdbc11.jar"
#AdditionalJar: ojdbc11
```
Al compilar, el driver se incluirá en el `.jar` final del servidor, por lo que no será necesario copiarlo por separado al directorio de producción.
-----
## **4. Validaciones de Seguridad**
El servidor realiza dos comprobaciones automáticas **en ambos handlers (B4X y JSON)** antes de ejecutar cualquier consulta:
1. **Verificación de Existencia del Comando**: El servidor comprueba que el nombre del comando SQL solicitado (ej. `"get_user"`) exista como una clave válida en el archivo `.properties` correspondiente. Si no lo encuentra, devolverá un error y no intentará ejecutar nada.
2. **Conteo de Parámetros**: Si el comando SQL en el archivo de configuración espera parámetros (contiene `?`), el servidor cuenta cuántos son y lo compara con el número de parámetros recibidos en la petición. Si las cantidades no coinciden, devolverá un error específico, evitando una ejecución fallida en la base de datos.
Estas validaciones aseguran que el desarrollador reciba feedback inmediato y claro si una petición está mal formada.
-----
## **5. Uso del Handler Clásico (Para Clientes B4X)**
Este handler mantiene la compatibilidad con `DBRequestManager`. La selección de la base de datos se realiza dinámicamente a través de la URL.
* Para `config.properties` =\> `http://tu-dominio.com:8090`
* Para `config.DB2.properties` =\> `http://tu-dominio.com:8090/DB2`
* Para `config.DB3.properties` =\> `http://tu-dominio.com:8090/DB3`
* Para `config.DB4.properties` =\> `http://tu-dominio.com:8090/DB4`
-----
## **6. Uso del DBHandlerJSON (Para Clientes Web)**
Este handler está diseñado para clientes que se comunican vía `JSON`, como aplicaciones web JavaScript.
### **6.1. Endpoint y Métodos de Envío**
Las peticiones van dirigidas al endpoint `/DBJ`. El handler es flexible y acepta datos de dos maneras:
**Método Recomendado: POST con Body JSON**
* **Método HTTP**: POST
* **URL**: `http://tu-dominio.com:8090/DBJ`
* **Header Requerido**: `Content-Type: application/json`
* **Body (Payload)**: El objeto JSON se envía directamente en el cuerpo de la petición.
**Ejemplo de Body:**
```json
{
"dbx": "DB2",
"query": "get_user",
"exec": "executeQuery",
"params": [
"CDAZA"
]
}
```
**Método Legacy: GET con Parámetro `j`**
* **Método HTTP**: GET
* **URL**: El JSON completo se envía como el valor del parámetro `j` en la URL.
**Ejemplo con GET:**
`http://tu-dominio.com:8090/DBJ?j={"dbx":"DB2","query":"get_user","exec":"executeQuery","params":["CDAZA"]}`
### **6.2. Formato del Payload JSON**
La estructura del objeto JSON es la misma para ambos métodos:
```json
{
"exec": "executeQuery",
"query": "nombre_del_comando_sql",
"dbx": "DB1",
"params": [
"valor1",
123
]
}
```
* `exec`: `"executeQuery"` (para `SELECT`) o `"executeCommand"` (para `INSERT`, `UPDATE`, `DELETE`).
* `query`: Nombre del comando SQL tal como está definido en el archivo de configuración.
* `dbx` (opcional): La llave de la base de datos (`DB1`, `DB2`, etc.). Si se omite, se usará **DB1**.
* `params` (opcional): Un **array** que contiene los parámetros para la consulta SQL, en el orden exacto que se esperan.
### **6.3. Respuestas JSON**
Las respuestas del servidor siempre son en formato JSON e incluyen un campo booleano `success`.
* **Si `success` es `true`**, los datos se encontrarán en la llave `result`.
* **Si `success` es `false`**, el mensaje de error se encontrará en la llave `error`.
-----
## **7. Administración del Servidor**
Se pueden ejecutar comandos de gestión directamente desde un navegador o una herramienta como `cURL`.
### **7.1. Comandos de Administración**
#### **Comandos Públicos (sin autenticación)**
* **Verificar Conectividad**: `http://tu-dominio.com:8090/ping`
* Responde con un simple `PONG` y la `hora` para confirmar que el servidor está en línea.
* **Verificar Estado Detallado**: `http://tu-dominio.com:8090/test`
* Muestra información sobre las conexiones a la base de datos y el estado general.
#### **Comandos Protegidos (requieren autenticación)**
* **Recargar Configuración**: `http://tu-dominio.com:8090/manager?command=reload`
(Vuelve a leer todos los archivos `config.*.properties` sin reiniciar el servidor).
* **Reiniciar Servidor (Estándar)**: `http://tu-dominio.com:8090/manager?command=rsx`
(Ejecuta los scripts `start.bat`, `start2.bat` y `stop.bat`).
* **Reiniciar Servidor (con PM2)**: `http://tu-dominio.com:8090/manager?command=rpm2`
(Ejecuta `reiniciaProcesoPM2.bat` y asume que el nombre del proceso es "RDC-Multi". Modificar el `.bat` si el nombre es diferente).

195
SSE.bas Normal file
View File

@@ -0,0 +1,195 @@
B4J=true
Group=Default Group
ModulesStructureVersion=1
Type=StaticCode
Version=10.3
@EndOfDesignText@
' Módulo para gestionar conexiones y transmisiones de Server-Sent Events (SSE).
' Declaración de variables globales a nivel de proceso.
Sub Process_Globals
' 'Connections' es un mapa (diccionario) para almacenar las conexiones SSE activas.
' La clave será una combinación del 'path' y un GUID único, y el valor será el OutputStream de la respuesta.
' Se usará un 'ThreadSafeMap' para evitar problemas de concurrencia entre hilos.
Dim Connections As Map
' Timer #1 ("El Vigilante"): Se encarga de detectar y eliminar conexiones muertas.
Private RemoveTimer As Timer
' Timer #2 ("El Informante"): Se encarga de recolectar y enviar los datos de estadísticas.
Dim StatsTimer As Timer
Dim const UPDATE_INTERVAL_MS As Long = 2000 ' Intervalo de envío de estadísticas: 2 segundos.
End Sub
' Subrutina de inicialización del módulo. Se llama una vez cuando el objeto es creado.
public Sub Initialize()
' Crea el mapa 'Connections' como un mapa seguro para hilos (ThreadSafeMap).
' Esto es fundamental porque múltiples peticiones (hilos) pueden intentar agregar o remover conexiones simultáneamente.
Connections = Main.srvr.CreateThreadSafeMap
' Inicializa el temporizador 'RemoveTimer' para que dispare el evento "RemoveTimer" cada 5000 milisegundos (5 segundos).
RemoveTimer.Initialize("RemoveTimer", 5000)
' Habilita el temporizador para que comience a funcionar.
RemoveTimer.Enabled = True
Log("Stats SSE Handler Initialized (Singleton Mode)")
' Crea el mapa de conexiones, asegurando que sea seguro para el manejo de múltiples hilos.
Connections = Main.srvr.CreateThreadSafeMap
' Configura y activa el timer para la limpieza de conexiones cada 5 segundos.
' NOTA: El EventName "RemoveTimer" debe coincidir con el nombre de la subrutina del tick.
RemoveTimer.Initialize("RemoveTimer", 5000)
RemoveTimer.Enabled = True
' Configura y activa el timer para el envío de estadísticas.
' NOTA: El EventName "StatsTimer" debe coincidir con el nombre de la subrutina del tick.
StatsTimer.Initialize("StatsTimer", UPDATE_INTERVAL_MS)
StatsTimer.Enabled = True
End Sub
' Subrutina para agregar un nuevo cliente (target) al stream de eventos SSE.
' Se llama cuando un cliente se conecta al endpoint SSE.
' Registra formalmente a un nuevo cliente en el sistema.
Sub AddTarget(path As String, resp As ServletResponse)
' Genera una clave única para esta conexión específica.
Dim connectionKey As String = path & "|" & GetGUID
Log("--- [SSE] Cliente conectado: " & connectionKey & " ---")
' Configura las cabeceras HTTP necesarias para que el navegador mantenga la conexión abierta.
resp.ContentType = "text/event-stream"
resp.SetHeader("Cache-Control", "no-cache")
resp.SetHeader("Connection", "keep-alive")
resp.CharacterEncoding = "UTF-8"
resp.Status = 200
' Añade al cliente y su canal de comunicación al mapa central.
Connections.Put(connectionKey, resp.OutputStream)
' Envía un primer mensaje de bienvenida para confirmar la conexión.
SendMessage(resp.OutputStream, "open", "Connection established", 0, connectionKey)
End Sub
' Envía un mensaje a todos los clientes suscritos a un "path" específico.
Sub Broadcast(Path As String, EventName As String, Message As String, Retry As Long)
' Itera sobre la lista de clientes activos.
For Each key As String In Connections.Keys
' Log(key)
' Filtra para enviar solo a los clientes del path correcto (en este caso, "stats").
If key.StartsWith(Path & "|") Then
Try
' Llama a la función de bajo nivel para enviar el mensaje formateado.
SendMessage(Connections.Get(key), EventName, Message, Retry, DateTime.Now)
Catch
' Si el envío falla, asume que el cliente se desconectó y lo elimina.
Log("######################")
Log("## Removing (broadcast failed): " & key)
Log("######################")
Connections.Remove(key)
End Try
End If
Next
End Sub
' Formatea y envía un único mensaje SSE a un cliente específico.
Sub SendMessage(out As OutputStream, eventName As String, message As String, retry As Int, id As String)
' Construye el mensaje siguiendo el formato oficial del protocolo SSE.
Dim sb As StringBuilder
sb.Initialize
sb.Append("id: " & id).Append(CRLF)
sb.Append("event: " & eventName).Append(CRLF)
If message <> "" Then
sb.Append("data: " & message).Append(CRLF)
End If
If retry > 0 Then
sb.Append("retry: " & retry).Append(CRLF)
End If
sb.Append(CRLF) ' El doble salto de línea final es obligatorio.
' Convierte el texto a bytes y lo escribe en el canal de comunicación del cliente.
Dim Bytes() As Byte = sb.ToString.GetBytes("UTF-8")
out.WriteBytes(Bytes, 0, Bytes.Length)
out.Flush ' Fuerza el envío inmediato de los datos.
End Sub
' Genera un Identificador Único Global (GUID) para cada conexión.
Private Sub GetGUID() As String
Dim jo As JavaObject
Return jo.InitializeStatic("java.util.UUID").RunMethod("randomUUID", Null)
End Sub
' Evento que se dispara cada vez que el 'RemoveTimer' completa su intervalo (cada 5 segundos).
' Su propósito es proactivamente limpiar conexiones muertas.
Sub RemoveTimer_Tick
' Log("remove timer")
' Itera sobre todas las conexiones activas.
For Each key As String In Connections.Keys
' Intenta enviar un mensaje de prueba ("ping" o "heartbeat") a cada cliente.
Try
' Obtiene el OutputStream del cliente.
Dim out As OutputStream = Connections.Get(key)
' Envía un evento de tipo "Test" sin datos. Si la conexión está viva, esto no hará nada visible.
SendMessage(out, "Test", "", 0, "")
Catch
' Si el 'SendMessage' falla, significa que el socket está cerrado (el cliente se desconectó).
' Registra en el log que se está eliminando una conexión muerta.
Log("######################")
Log("## Removing (timer): " & key)
Log("######################")
' Elimina la conexión del mapa para liberar recursos.
Connections.Remove(key)
End Try
Next
End Sub
' Evento del Timer #2 ("El Informante"): se dispara cada 2 segundos.
public Sub StatsTimer_Tick
' Optimización: si no hay nadie conectado, no realiza el trabajo pesado.
' Log($"Conexiones: ${Connections.Size}"$)
If Connections.Size = 0 Then Return
Try
' Prepara un mapa para almacenar las estadísticas recolectadas.
Dim allPoolStats As Map
allPoolStats.Initialize
' Bloquea el acceso a los conectores para leer sus datos de forma segura.
Main.MainConnectorsLock.RunMethod("lock", Null)
For Each dbKey As String In Main.listaDeCP
Dim connector As RDCConnector
If Main.Connectors.ContainsKey(dbKey) Then
connector = Main.Connectors.Get(dbKey)
If connector.IsInitialized Then
' 1. Obtener las métricas del pool C3P0 (incluye la configuración estática)
Dim statsMap As Map = connector.GetPoolStats
' 2. OBTENER LAS MÉTRICAS DE LA CAPA DE APLICACIÓN DESDE EL CACHÉ GLOBAL
Dim cachedAppStats As Map = Main.LatestPoolStats.GetDefault(dbKey, CreateMap()).As(Map)
' 3. FUSIONAR: Agregar el contador de peticiones activas al mapa que se va a enviar
If cachedAppStats.ContainsKey("HandlerActiveRequests") Then
statsMap.Put("HandlerActiveRequests", cachedAppStats.Get("HandlerActiveRequests"))
End If
allPoolStats.Put(dbKey, statsMap)
Else
allPoolStats.Put(dbKey, CreateMap("Error": "Conector no inicializado"))
End If
End If
Next
' Libera el bloqueo para que otras partes del programa puedan usar los conectores.
Main.MainConnectorsLock.RunMethod("unlock", Null)
' Convierte el mapa de estadísticas a un formato de texto JSON.
Dim j As JSONGenerator
j.Initialize(allPoolStats)
Dim jsonStats As String = j.ToString
' Llama al "locutor" para enviar el JSON a todos los clientes conectados.
Broadcast("stats", "stats_update", jsonStats, 0)
Catch
' Captura y registra cualquier error que ocurra durante la recolección de datos.
Log($"[SSE] Error CRÍTICO durante la adquisición de estadísticas: ${LastException.Message}"$)
End Try
End Sub

131
SSEHandler.bas Normal file
View File

@@ -0,0 +1,131 @@
B4J=true
Group=Default Group
ModulesStructureVersion=1
Type=Class
Version=10.3
@EndOfDesignText@
' Handler class: StatsSSEHandler.b4j
' Gestiona y transmite en tiempo real las estadísticas del pool de conexiones vía Server-Sent Events (SSE).
' Opera en modo Singleton: una única instancia maneja todas las conexiones.
Sub Class_Globals
' Almacena de forma centralizada a todos los clientes (navegadores) conectados.
' La clave es un ID único y el valor es el canal de comunicación (OutputStream).
Private Connections As Map
' Timer #1 ("El Vigilante"): Se encarga de detectar y eliminar conexiones muertas.
Private RemoveTimer As Timer
' Timer #2 ("El Informante"): Se encarga de recolectar y enviar los datos de estadísticas.
Dim StatsTimer As Timer
Dim const UPDATE_INTERVAL_MS As Long = 2000 ' Intervalo de envío de estadísticas: 2 segundos.
End Sub
' Se ejecuta UNA SOLA VEZ cuando el servidor arranca, gracias al modo Singleton.
Public Sub Initialize
Log("Stats SSE Handler Initialized (Singleton Mode)")
' Crea el mapa de conexiones, asegurando que sea seguro para el manejo de múltiples hilos.
Connections = Main.srvr.CreateThreadSafeMap
' Configura y activa el timer para la limpieza de conexiones cada 5 segundos.
' NOTA: El EventName "RemoveTimer" debe coincidir con el nombre de la subrutina del tick.
RemoveTimer.Initialize("RemoveTimer", 5000)
RemoveTimer.Enabled = True
' Configura y activa el timer para el envío de estadísticas.
' NOTA: El EventName "StatsTimer" debe coincidir con el nombre de la subrutina del tick.
StatsTimer.Initialize("StatsTimer", UPDATE_INTERVAL_MS)
StatsTimer.Enabled = True
End Sub
' Es el punto de entrada principal. Atiende todas las peticiones HTTP dirigidas a este handler.
Sub Handle(req As ServletRequest, resp As ServletResponse)
Log($"StatsTimerinicializado: ${StatsTimer.IsInitialized}, StatsTimer habilitado: ${StatsTimer.Enabled}"$)
StatsTimer.Initialize("StatsTimer", 2000)
StatsTimer.Enabled = True
' Filtro de seguridad: verifica si el usuario tiene una sesión autorizada.
If req.GetSession.GetAttribute2("user_is_authorized", False) = False Then
resp.SendRedirect("/login")
Return
End If
' Procesa únicamente las peticiones GET, que son las que usan los navegadores para iniciar una conexión SSE.
If req.Method = "GET" Then
' Mantiene la petición activa de forma asíncrona para poder enviar datos en el futuro.
Dim reqJO As JavaObject = req
reqJO.RunMethod("startAsync", Null)
' Registra al nuevo cliente para que empiece a recibir eventos.
SSE.AddTarget("stats", resp)
Else
' Rechaza cualquier otro método HTTP (POST, PUT, etc.) con un error.
resp.SendError(405, "Method Not Allowed")
End If
End Sub
' --- LÓGICA DE LOS TIMERS ---
' Evento del Timer #1 ("El Vigilante"): se dispara cada 5 segundos.
Sub RemoveTimer_Tick
' Log("REMOVETIMER TICK")
' Optimización: si no hay nadie conectado, no hace nada.
If Connections.Size = 0 Then Return
' Itera sobre todos los clientes para verificar si siguen activos.
For Each key As String In Connections.Keys
Try
' Envía un evento "ping" silencioso. Si la conexión está viva, no pasa nada.
SSE.SendMessage(Connections.Get(key), "ping", "", 0, "")
Catch
' Si el envío falla, la conexión está muerta. Se procede a la limpieza.
Log("######################")
Log("## Removing (timer cleanup): " & key)
Log("######################")
Connections.Remove(key)
End Try
Next
End Sub
' Evento del Timer #2 ("El Informante"): se dispara cada 2 segundos.
public Sub StatsTimer_Tick
' Optimización: si no hay nadie conectado, no realiza el trabajo pesado.
If Connections.Size = 0 Then Return
Try
' Prepara un mapa para almacenar las estadísticas recolectadas.
Dim allPoolStats As Map
allPoolStats.Initialize
' Bloquea el acceso a los conectores para leer sus datos de forma segura.
Main.MainConnectorsLock.RunMethod("lock", Null)
For Each dbKey As String In Main.listaDeCP
Dim connector As RDCConnector
If Main.Connectors.ContainsKey(dbKey) Then
connector = Main.Connectors.Get(dbKey)
If connector.IsInitialized Then
allPoolStats.Put(dbKey, connector.GetPoolStats)
Else
allPoolStats.Put(dbKey, CreateMap("Error": "Conector no inicializado"))
End If
End If
Next
' Libera el bloqueo para que otras partes del programa puedan usar los conectores.
Main.MainConnectorsLock.RunMethod("unlock", Null)
' Convierte el mapa de estadísticas a un formato de texto JSON.
Dim j As JSONGenerator
j.Initialize(allPoolStats)
Dim jsonStats As String = j.ToString
' Llama al "locutor" para enviar el JSON a todos los clientes conectados.
SSE.Broadcast("stats", "stats_update", jsonStats, 0)
Catch
' Captura y registra cualquier error que ocurra durante la recolección de datos.
Log($"[SSE] Error CRÍTICO durante la adquisición de estadísticas: ${LastException.Message}"$)
End Try
End Sub

35
TestHandler.bas Normal file
View File

@@ -0,0 +1,35 @@
B4J=true
Group=Default Group
ModulesStructureVersion=1
Type=Class
Version=4.19
@EndOfDesignText@
'Handler class
Sub Class_Globals
End Sub
Public Sub Initialize
End Sub
Sub Handle(req As ServletRequest, resp As ServletResponse)
Log("TEST")
resp.ContentType = "text/html"
resp.Write($"<a href="/test">Test</a> | <a href="/manager?command=reload">Reload</a> | <a href="/manager?command=rpm2">Reiniciar</a> | <a href="/manager?command=reviveBow">Revive Bow</a> | </br></br>"$)
resp.Write($"RemoteServer is running on port <strong>${Main.srvr.Port}</strong> ($DateTime{DateTime.Now})<br/>"$)
Try
' Dim con As SQL = Main.rdcConnectorDB1.GetConnection("")
Dim con As SQL = Main.Connectors.Get("DB1").As(RDCConnector).GetConnection("")
resp.Write("Connection successful.</br></br>")
Dim estaDB As String = ""
Log(Main.listaDeCP)
For i = 0 To Main.listaDeCP.Size - 1
If Main.listaDeCP.get(i) <> "" Then estaDB = "." & Main.listaDeCP.get(i)
resp.Write($"Using config${estaDB}.properties</br>"$)
Next
con.Close
Catch
resp.Write("Error fetching connection.")
End Try
End Sub

View File

@@ -0,0 +1,75 @@
#Lines starting with '#' are comments.
#Backslash character at the end of line means that the command continues in the next line.
DriverClass=oracle.jdbc.driver.OracleDriver
#JdbcUrl=jdbc:mysql://localhost/test?characterEncoding=utf8
#SQL Server
#DriverClass=net.sourceforge.jtds.jdbc.Driver
# este para produccion GHAN JdbcUrl=jdbc:oracle:thin:@//192.168.15.53:1521/DBKMT
#GOHAN ---> server
JdbcUrl=jdbc:oracle:thin:@//10.0.0.205:1521/DBKMT
#JdbcUrl=jdbc:oracle:thin:@//10.0.0.236:1521/DBKMT
# SVR-KEYMON-PRODUCCION--> Usuario
User=GUNA
Password=GUNAD2015M
#User=TORRADOCONAUTO
#Password=TORRADOCONAUTOD2016M
#--> Puertos
#SAC - DFR - MDA / GOHAN -->COBRANZA
#ServerPort=1783
#GUNA - SALMA - DURAKELO - DBC / SVR-KEYMON-PRODUCCION --> DISTRIBUIDORAS
ServerPort=1783
#CMG - TORRADO / TRUNKS -->COBRANZA/ GM
#ServerPort=1781
#If Debug is true then this file will be reloaded on every query.
#This is useful if you need to modify the queries.
Debug=true
#SQL COMMANDS
##################
#################
################ S O P O R T E
#################
##################
sql.select_soporte=select * from GUNA.soporte
sql.select_almacenes_KELL=select CAT_AG_ID, CAT_AG_NOMBRE from KELLOGGS.cat_agencias order by CAT_AG_NOMBRE
sql.select_almacenes_GUNA=select CAT_AG_ID, CAT_AG_NOMBRE from GUNA.cat_agencias order by CAT_AG_NOMBRE
sql.select_almacenes_SALMA=select CAT_AG_ID, CAT_AG_NOMBRE from SALMA.cat_agencias order by CAT_AG_NOMBRE
sql.select_almacenes_DANVIT=select CAT_AG_ID, CAT_AG_NOMBRE from DANVIT.cat_agencias order by CAT_AG_NOMBRE
sql.proc_QUITAR_VENTA_KELL=BEGIN EXECUTE IMMEDIATE ('DECLARE Cursor_SYS Sys_Refcursor; BEGIN KELLOGGS.SP_QUITAR_VENTA_X_TIPO( '''||(?)||''', '''||(?)||''', '''||(?)||''', '''||(?)||''', '''||(?)||''', Cursor_SYS); end;'); END;
sql.proc_QUITAR_VENTA_GUNA=BEGIN EXECUTE IMMEDIATE ('DECLARE Cursor_SYS Sys_Refcursor; BEGIN GUNA.SP_QUITAR_VENTA( '''||(?)||''', '''||(?)||''', '''||(?)||''', Cursor_SYS, '''||(?)||'''); end;'); END;
sql.proc_QUITAR_VENTA_SALMA=BEGIN EXECUTE IMMEDIATE ('DECLARE Cursor_SYS Sys_Refcursor; BEGIN SALMA.SP_QUITAR_VENTA( '''||(?)||''', '''||(?)||''', '''||(?)||''', Cursor_SYS); end;'); END;
sql.proc_QUITAR_VENTA_DANVIT=BEGIN EXECUTE IMMEDIATE ('DECLARE Cursor_SYS Sys_Refcursor; BEGIN DANVIT.SP_QUITAR_VENTA( '''||(?)||''', '''||(?)||''', '''||(?)||''', Cursor_SYS); end;'); END;
sql.proc_QUITAR_PAGOPAGARE_KELLOGGS=BEGIN EXECUTE IMMEDIATE ('DECLARE Cursor_SYS Sys_Refcursor; BEGIN KELLOGGS.SP_ELIMINAS_PAGOS_PAGARES_REP( '''||(?)||''', '''||(?)||''', '''||(?)||''', Cursor_SYS); end;'); END;
sql.proc_LIBERA_BANDERA_FACTURACION_KELLOGGS=BEGIN EXECUTE IMMEDIATE ('DECLARE Cursor_SYS Sys_Refcursor; BEGIN KELLOGGS.SP_LIBERA_FACTURACION(Cursor_SYS); end;'); END;
sql.proc_LIBERA_BANDERA_CARGAFORANEA_KELLOGGS=BEGIN EXECUTE IMMEDIATE ('DECLARE Cursor_SYS Sys_Refcursor; BEGIN KELLOGGS.SP_LLENAR_FILTROS ( '''||(?)||''', '''||(?)||''', Cursor_SYS); end;'); END;
sql.proc_QUITAR_TICKET_KELLOGGS=BEGIN EXECUTE IMMEDIATE ('DECLARE Cursor_SYS Sys_Refcursor; BEGIN KELLOGGS.SP_QUITAR_TICKET( '''||(?)||''', '''||(?)||''', '''||(?)||''', Cursor_SYS); end;'); END;
sql.revisa_liquidada_Guna=SELECT COUNT(*) as liquidada FROM GUNA.HIST_VENTAS_DETALLE WHERE trunc(HVD_DTESYNC) = trunc(sysdate) and hvd_almacen = (?) and hvd_ruta = (?) AND (HVD_DESCUENTO != 0 or HVD_FECHA_AVION IS NOT NULL)
sql.revisa_liquidada_Kell=SELECT COUNT(*) as liquidada FROM KELLOGGS.HIST_VENTAS_DETALLE WHERE trunc(HVD_DTESYNC) = trunc(sysdate) and hvd_almacen = (?) and hvd_ruta = (?) and HVD_TIPOVENTA = (?) AND HVD_ESTATUS = 'Liquidado'
sql.select_todos_soporte=select cat_lo_usuario, cat_lo_estatus, cat_lo_nombre, cat_lo_contrasena, cat_lo_agencia, cat_agencias.cat_ag_nombre, cat_lo_ruta from cat_logins left join cat_agencias on cat_lo_agencia = cat_ag_id where (cat_lo_usuario LIKE ('%'||(?)||'%') or cat_lo_nombre LIKE ('%'||(?)||'%')) and cat_ag_nombre LIKE ('%'||(?)||'%') and cat_lo_ruta LIKE ('%'||(?)||'%') and rownum <= 20
sql.select_todosGUNA_soporte=select cat_lo_usuario, cat_lo_estatus, cat_lo_nombre, cat_lo_contrasena, cat_lo_agencia, cat_agencias.cat_ag_nombre, cat_ru_ruta from cat_logins left join cat_agencias on cat_lo_agencia = cat_ag_id left join cat_rutas on cat_lo_usuario = cat_ru_vendedor where (cat_lo_usuario LIKE ('%'||(?)||'%') or cat_lo_nombre LIKE ('%'||(?)||'%')) and cat_ag_nombre LIKE ('%'||(?)||'%') and cat_ru_ruta LIKE ('%'||(?)||'%') and rownum <= 20
sql.select_todosKELLOGGS_soporte=select cat_lo_usuario, cat_lo_estatus, cat_lo_nombre, cat_lo_contrasena, cat_lo_agencia, cat_agencias.cat_ag_nombre, cat_ru_ruta from KELLOGGS.cat_logins left join KELLOGGS.cat_agencias on cat_lo_agencia = cat_ag_id left join KELLOGGS.cat_rutas on cat_lo_usuario = cat_ru_vendedor where (cat_lo_usuario LIKE ('%'||(?)||'%') or cat_lo_nombre LIKE ('%'||(?)||'%')) and cat_ag_nombre LIKE ('%'||(?)||'%') and cat_ru_ruta LIKE ('%'||(?)||'%') and rownum <= 20
sql.select_todosSALMA_soporte=select cat_lo_usuario, cat_lo_estatus, cat_lo_nombre, cat_lo_contrasena, cat_lo_agencia, cat_agencias.cat_ag_nombre, cat_lo_ruta as cat_ru_ruta from SALMA.cat_logins left join SALMA.cat_agencias on cat_lo_agencia = cat_ag_id where (cat_lo_usuario LIKE ('%'||(?)||'%') or cat_lo_nombre LIKE ('%'||(?)||'%')) and cat_ag_nombre LIKE ('%'||(?)||'%') and cat_lo_ruta LIKE ('%'||(?)||'%') and rownum <= 20
sql.select_todosDANVIT_soporte=select cat_lo_usuario, cat_lo_estatus, cat_lo_nombre, cat_lo_contrasena, cat_lo_agencia, cat_agencias.cat_ag_nombre, cat_ru_ruta from DANVIT.cat_logins left join DANVIT.cat_agencias on cat_lo_agencia = cat_ag_id left join DANVIT.cat_rutas on cat_lo_usuario = cat_ru_vendedor where (cat_lo_usuario LIKE ('%'||(?)||'%') or cat_lo_nombre LIKE ('%'||(?)||'%')) and cat_ag_nombre LIKE ('%'||(?)||'%') and cat_ru_ruta LIKE ('%'||(?)||'%') and rownum <= 20
sql.select_ventaXrutaGuna_soporte=select hvd_ruta, sum(hvd_costo_tot) as monto, hvd_tipoventa from hist_ventas_detalle where trunc(hvd_fecha)=trunc(sysdate) and hvd_ruta=(?) and hvd_almacen=(?) AND hvd_codpromo <> 'BASICA' group by hvd_ruta, hvd_tipoventa
sql.select_ventaXrutaKelloggs_soporte=select hvd_ruta, sum(hvd_costo_tot) as monto, hvd_tipoventa from KELLOGGS.hist_ventas_detalle where trunc(hvd_fecha)=trunc(sysdate) and hvd_ruta=(?) and hvd_almacen=(?) and hvd_tipoventa=(?) AND hvd_codpromo <> 'BASICA' group by hvd_ruta, hvd_tipoventa
sql.select_ventaXrutaSalma_soporte=select hvd_ruta, sum(hvd_costo_tot) as monto, hvd_tipoventa from SALMA.hist_ventas_detalle where trunc(hvd_fecha)=trunc(sysdate) and hvd_ruta=(?) and hvd_almacen=(?) AND hvd_codpromo <> 'BASICA' group by hvd_ruta, hvd_tipoventa
sql.select_ventaXrutaDanvit_soporte=select hvd_ruta, sum(hvd_costo_tot) as monto, hvd_tipoventa from DANVIT.hist_ventas_detalle where trunc(hvd_fecha)=trunc(sysdate) and hvd_ruta=(?) and hvd_almacen=(?) AND hvd_codpromo <> 'BASICA' group by hvd_ruta, hvd_tipoventa
sql.select_prodsTicket_Kelloggs=SELECT HVD_CLIENTE CLIENTE, HVD_PROID PRODUCTO_ID, HVD_PRONOMBRE NOMBRE_PRODUCTO, HVD_CANT CANTIDAD, HVD_COSTO_TOT COSTO_TOTAL, HVD_RUTA RUTA, HVD_CODPROMO CODPROMO,NVL(HVD_TIPOVENTA,' ') TIPOVENTA, NVL(HVD_ESTATUS,' ') ESTATUS, hvd_cedis FROM KELLOGGS.HIST_VENTAS_DETALLE WHERE TRUNC(HVD_FECHA) = TRUNC(SYSDATE) AND HVD_ALMACEN = (?) AND HVD_CLIENTE = (?) and hvd_rechazo is null ORDER BY HVD_CODPROMO, HVD_PRONOMBRE

View File

@@ -0,0 +1,80 @@
#Lines starting with '#' are comments.
#Backslash character at the end of line means that the command continues in the next line.
#DriverClass=oracle.jdbc.driver.OracleDriver
#JdbcUrl=jdbc:mysql://localhost/test?characterEncoding=utf8
DriverClass=org.postgresql.Driver
#SQL Server
#DriverClass=net.sourceforge.jtds.jdbc.Driver
# este para produccion GHAN JdbcUrl=jdbc:oracle:thin:@//192.168.15.53:1521/DBKMT
#GOHAN ---> server
#JdbcUrl=jdbc:oracle:thin:@//10.0.0.205:1521/DBKMT
#JdbcUrl=jdbc:oracle:thin:@//10.0.0.236:1521/DBKMT
JdbcUrl=jdbc:postgresql://localhost/pruebaKMT
# SVR-KEYMON-PRODUCCION--> Usuario
#User=GUNA
#Password=GUNAD2015M
User=postgres
Password=Demo1234
#User=TORRADOCONAUTO
#Password=TORRADOCONAUTOD2016M
#--> Puertos
#SAC - DFR - MDA / GOHAN -->COBRANZA
#ServerPort=1783
#GUNA - SALMA - DURAKELO - DBC / SVR-KEYMON-PRODUCCION --> DISTRIBUIDORAS
#ServerPort=1783
#CMG - TORRADO / TRUNKS -->COBRANZA/ GM
#ServerPort=1781
ServerPort=5432
#If Debug is true then this file will be reloaded on every query.
#This is useful if you need to modify the queries.
Debug=true
#SQL COMMANDS
##################
#################
################ S O P O R T E
#################
##################
sql.select_soporte=select * from GUNA.soporte
sql.select_almacenes_KELL=select CAT_AG_ID, CAT_AG_NOMBRE from KELLOGGS.cat_agencias order by CAT_AG_NOMBRE
sql.select_almacenes_GUNA=select CAT_AG_ID, CAT_AG_NOMBRE from GUNA.cat_agencias order by CAT_AG_NOMBRE
sql.select_almacenes_SALMA=select CAT_AG_ID, CAT_AG_NOMBRE from SALMA.cat_agencias order by CAT_AG_NOMBRE
sql.select_almacenes_DANVIT=select CAT_AG_ID, CAT_AG_NOMBRE from DANVIT.cat_agencias order by CAT_AG_NOMBRE
sql.proc_QUITAR_VENTA_KELL=BEGIN EXECUTE IMMEDIATE ('DECLARE Cursor_SYS Sys_Refcursor; BEGIN KELLOGGS.SP_QUITAR_VENTA_X_TIPO( '''||(?)||''', '''||(?)||''', '''||(?)||''', '''||(?)||''', '''||(?)||''', Cursor_SYS); end;'); END;
sql.proc_QUITAR_VENTA_GUNA=BEGIN EXECUTE IMMEDIATE ('DECLARE Cursor_SYS Sys_Refcursor; BEGIN GUNA.SP_QUITAR_VENTA( '''||(?)||''', '''||(?)||''', '''||(?)||''', Cursor_SYS, '''||(?)||'''); end;'); END;
sql.proc_QUITAR_VENTA_SALMA=BEGIN EXECUTE IMMEDIATE ('DECLARE Cursor_SYS Sys_Refcursor; BEGIN SALMA.SP_QUITAR_VENTA( '''||(?)||''', '''||(?)||''', '''||(?)||''', Cursor_SYS); end;'); END;
sql.proc_QUITAR_VENTA_DANVIT=BEGIN EXECUTE IMMEDIATE ('DECLARE Cursor_SYS Sys_Refcursor; BEGIN DANVIT.SP_QUITAR_VENTA( '''||(?)||''', '''||(?)||''', '''||(?)||''', Cursor_SYS); end;'); END;
sql.proc_QUITAR_PAGOPAGARE_KELLOGGS=BEGIN EXECUTE IMMEDIATE ('DECLARE Cursor_SYS Sys_Refcursor; BEGIN KELLOGGS.SP_ELIMINAS_PAGOS_PAGARES_REP( '''||(?)||''', '''||(?)||''', '''||(?)||''', Cursor_SYS); end;'); END;
sql.proc_LIBERA_BANDERA_FACTURACION_KELLOGGS=BEGIN EXECUTE IMMEDIATE ('DECLARE Cursor_SYS Sys_Refcursor; BEGIN KELLOGGS.SP_LIBERA_FACTURACION(Cursor_SYS); end;'); END;
sql.proc_LIBERA_BANDERA_CARGAFORANEA_KELLOGGS=BEGIN EXECUTE IMMEDIATE ('DECLARE Cursor_SYS Sys_Refcursor; BEGIN KELLOGGS.SP_LLENAR_FILTROS ( '''||(?)||''', '''||(?)||''', Cursor_SYS); end;'); END;
sql.proc_QUITAR_TICKET_KELLOGGS=BEGIN EXECUTE IMMEDIATE ('DECLARE Cursor_SYS Sys_Refcursor; BEGIN KELLOGGS.SP_QUITAR_TICKET( '''||(?)||''', '''||(?)||''', '''||(?)||''', Cursor_SYS); end;'); END;
sql.revisa_liquidada_Guna=SELECT COUNT(*) as liquidada FROM GUNA.HIST_VENTAS_DETALLE WHERE trunc(HVD_DTESYNC) = trunc(sysdate) and hvd_almacen = (?) and hvd_ruta = (?) AND (HVD_DESCUENTO != 0 or HVD_FECHA_AVION IS NOT NULL)
sql.revisa_liquidada_Kell=SELECT COUNT(*) as liquidada FROM KELLOGGS.HIST_VENTAS_DETALLE WHERE trunc(HVD_DTESYNC) = trunc(sysdate) and hvd_almacen = (?) and hvd_ruta = (?) and HVD_TIPOVENTA = (?) AND HVD_ESTATUS = 'Liquidado'
sql.select_todos_soporte=select cat_lo_usuario, cat_lo_estatus, cat_lo_nombre, cat_lo_contrasena, cat_lo_agencia, cat_agencias.cat_ag_nombre, cat_lo_ruta from cat_logins left join cat_agencias on cat_lo_agencia = cat_ag_id where (cat_lo_usuario LIKE ('%'||(?)||'%') or cat_lo_nombre LIKE ('%'||(?)||'%')) and cat_ag_nombre LIKE ('%'||(?)||'%') and cat_lo_ruta LIKE ('%'||(?)||'%') and rownum <= 20
sql.select_todosGUNA_soporte=select cat_lo_usuario, cat_lo_estatus, cat_lo_nombre, cat_lo_contrasena, cat_lo_agencia, cat_agencias.cat_ag_nombre, cat_ru_ruta from cat_logins left join cat_agencias on cat_lo_agencia = cat_ag_id left join cat_rutas on cat_lo_usuario = cat_ru_vendedor where (cat_lo_usuario LIKE ('%'||(?)||'%') or cat_lo_nombre LIKE ('%'||(?)||'%')) and cat_ag_nombre LIKE ('%'||(?)||'%') and cat_ru_ruta LIKE ('%'||(?)||'%') and rownum <= 20
sql.select_todosKELLOGGS_soporte=select cat_lo_usuario, cat_lo_estatus, cat_lo_nombre, cat_lo_contrasena, cat_lo_agencia, cat_agencias.cat_ag_nombre, cat_ru_ruta from KELLOGGS.cat_logins left join KELLOGGS.cat_agencias on cat_lo_agencia = cat_ag_id left join KELLOGGS.cat_rutas on cat_lo_usuario = cat_ru_vendedor where (cat_lo_usuario LIKE ('%'||(?)||'%') or cat_lo_nombre LIKE ('%'||(?)||'%')) and cat_ag_nombre LIKE ('%'||(?)||'%') and cat_ru_ruta LIKE ('%'||(?)||'%') and rownum <= 20
sql.select_todosSALMA_soporte=select cat_lo_usuario, cat_lo_estatus, cat_lo_nombre, cat_lo_contrasena, cat_lo_agencia, cat_agencias.cat_ag_nombre, cat_lo_ruta as cat_ru_ruta from SALMA.cat_logins left join SALMA.cat_agencias on cat_lo_agencia = cat_ag_id where (cat_lo_usuario LIKE ('%'||(?)||'%') or cat_lo_nombre LIKE ('%'||(?)||'%')) and cat_ag_nombre LIKE ('%'||(?)||'%') and cat_lo_ruta LIKE ('%'||(?)||'%') and rownum <= 20
sql.select_todosDANVIT_soporte=select cat_lo_usuario, cat_lo_estatus, cat_lo_nombre, cat_lo_contrasena, cat_lo_agencia, cat_agencias.cat_ag_nombre, cat_ru_ruta from DANVIT.cat_logins left join DANVIT.cat_agencias on cat_lo_agencia = cat_ag_id left join DANVIT.cat_rutas on cat_lo_usuario = cat_ru_vendedor where (cat_lo_usuario LIKE ('%'||(?)||'%') or cat_lo_nombre LIKE ('%'||(?)||'%')) and cat_ag_nombre LIKE ('%'||(?)||'%') and cat_ru_ruta LIKE ('%'||(?)||'%') and rownum <= 20
sql.select_ventaXrutaGuna_soporte=select hvd_ruta, sum(hvd_costo_tot) as monto, hvd_tipoventa from hist_ventas_detalle where trunc(hvd_fecha)=trunc(sysdate) and hvd_ruta=(?) and hvd_almacen=(?) AND hvd_codpromo <> 'BASICA' group by hvd_ruta, hvd_tipoventa
sql.select_ventaXrutaKelloggs_soporte=select hvd_ruta, sum(hvd_costo_tot) as monto, hvd_tipoventa from KELLOGGS.hist_ventas_detalle where trunc(hvd_fecha)=trunc(sysdate) and hvd_ruta=(?) and hvd_almacen=(?) and hvd_tipoventa=(?) AND hvd_codpromo <> 'BASICA' group by hvd_ruta, hvd_tipoventa
sql.select_ventaXrutaSalma_soporte=select hvd_ruta, sum(hvd_costo_tot) as monto, hvd_tipoventa from SALMA.hist_ventas_detalle where trunc(hvd_fecha)=trunc(sysdate) and hvd_ruta=(?) and hvd_almacen=(?) AND hvd_codpromo <> 'BASICA' group by hvd_ruta, hvd_tipoventa
sql.select_ventaXrutaDanvit_soporte=select hvd_ruta, sum(hvd_costo_tot) as monto, hvd_tipoventa from DANVIT.hist_ventas_detalle where trunc(hvd_fecha)=trunc(sysdate) and hvd_ruta=(?) and hvd_almacen=(?) AND hvd_codpromo <> 'BASICA' group by hvd_ruta, hvd_tipoventa
sql.select_prodsTicket_Kelloggs=SELECT HVD_CLIENTE CLIENTE, HVD_PROID PRODUCTO_ID, HVD_PRONOMBRE NOMBRE_PRODUCTO, HVD_CANT CANTIDAD, HVD_COSTO_TOT COSTO_TOTAL, HVD_RUTA RUTA, HVD_CODPROMO CODPROMO,NVL(HVD_TIPOVENTA,' ') TIPOVENTA, NVL(HVD_ESTATUS,' ') ESTATUS, hvd_cedis FROM KELLOGGS.HIST_VENTAS_DETALLE WHERE TRUNC(HVD_FECHA) = TRUNC(SYSDATE) AND HVD_ALMACEN = (?) AND HVD_CLIENTE = (?) and hvd_rechazo is null ORDER BY HVD_CODPROMO, HVD_PRONOMBRE

39
faviconHandler.bas Normal file
View File

@@ -0,0 +1,39 @@
B4J=true
Group=Default Group
ModulesStructureVersion=1
Type=Class
Version=10.3
@EndOfDesignText@
'Class module: FaviconHandler
' Manejador para la petición de /favicon.ico
' Simplemente devuelve un estado HTTP 204 (No Content)
' para indicar al navegador que no hay un favicon.
Sub Class_Globals
' No se necesitan variables globales para este manejador simple.
End Sub
Public Sub Initialize
' No se necesita inicialización específica para este manejador.
End Sub
Public Sub Handle(req As ServletRequest, resp As ServletResponse)
' Registra la petición en el Log (opcional, para depuración)
' Log("Petición de Favicon recibida: " & req.RequestURI)
'
' Establece el código de estado HTTP a 204 (No Content).
' Esto le dice al navegador que la petición fue exitosa, pero no hay contenido que devolver.
resp.Status = 204
' Es buena práctica cerrar el OutputStream, aunque para 204 no haya contenido explícito.
' Algunos servidores web podrían requerir cerrar el stream de respuesta.
Try
resp.OutputStream.Close
Catch
Log("Error al cerrar el OutputStream en FaviconHandler: " & LastException)
End Try
' El Return es fundamental para que el manejador termine su ejecución
' y no intente procesar la petición con otros manejadores o caiga en el "catch-all".
Return
End Sub

987
jRDC_Multi.b4j Normal file
View File

@@ -0,0 +1,987 @@
AppType=StandardJava
Build1=Default,b4j.JRDCMultiDB
Build2=New_1,b4j.JRDCMulti
File1=config.DB2.properties
File2=config.DB3.properties
File3=config.DB4.properties
File4=config.properties
File5=reiniciaProcesoBow.bat
File6=reiniciaProcesoPM2.bat
File7=start.bat
File8=start2.bat
File9=stop.bat
FileGroup1=Default Group
FileGroup2=Default Group
FileGroup3=Default Group
FileGroup4=Default Group
FileGroup5=Default Group
FileGroup6=Default Group
FileGroup7=Default Group
FileGroup8=Default Group
FileGroup9=Default Group
Group=Default Group
Library1=bcrypt
Library2=byteconverter
Library3=javaobject
Library4=jcore
Library5=jrandomaccessfile
Library6=jserver
Library7=jshell
Library8=json
Library9=jsql
Module1=Cambios
Module10=LoginHandler
Module11=LogoutHandler
Module12=Manager
Module13=ParameterValidationUtils
Module14=ping
Module15=RDCConnector
Module16=SSE
Module17=SSEHandler
Module18=TestHandler
Module2=ChangePassHandler
Module3=ConnectionPoolManager
Module4=DBHandlerB4X
Module5=DBHandlerJSON
Module6=DoLoginHandler
Module7=faviconHandler
Module8=GlobalParameters
Module9=HikariConnectionPool
NumberOfFiles=9
NumberOfLibraries=9
NumberOfModules=18
Version=10.3
@EndOfDesignText@
'Non-UI application (console / server application)
#Region Project Attributes
' Specify command line arguments if any
#CommandLineArgs:
' Merge all referenced libraries into the final JAR
#MergeLibraries: True
' VERSION 5.10.25
'###########################################################################################################
'###################### PULL #############################################################
' IDE helper link to perform a 'git pull'
'Ctrl + click ide://run?file=%WINDIR%\System32\cmd.exe&Args=/c&Args=git&Args=pull
'###########################################################################################################
'###################### PUSH #############################################################
' IDE helper link to perform a 'git push' (using a custom script/alias 'github')
'Ctrl + click ide://run?file=%WINDIR%\System32\WindowsPowerShell\v1.0\powershell.exe&Args=github&Args=..\..\
'###########################################################################################################
'###################### PUSH TORTOISE GIT #########################################################
' IDE helper link to open the TortoiseGit commit dialog
'Ctrl + click ide://run?file=%WINDIR%\System32\WindowsPowerShell\v1.0\powershell.exe&Args=TortoiseGitProc&Args=/command:commit&Args=/path:"../"&Args=/closeonend:2
'###########################################################################################################
#End Region
' --- JDBC Driver Selection ---
' Change based on the jdbc jar file
'#AdditionalJar: mysql-connector-java-5.1.27-bin
'#AdditionalJar: postgresql-42.7.0
' Using Oracle JDBC driver
#AdditionalJar: ojdbc11
' Using SQLite JDBC driver (for user/log database)
#AdditionalJar: sqlite-jdbc-3.7.2
' --- Critical Dependencies for HikariCP (Connection Pooling) and SLF4J (Logging) ---
#AdditionalJar: HikariCP-4.0.3
#AdditionalJar: slf4j-api-1.7.25
#AdditionalJar: slf4j-simple-1.7.25
' --- Global variables for the entire application ---
Sub Process_Globals
' The main B4J HTTP server object
Public srvr As Server
' The current version of this modified jRDC server
Public const VERSION As Float = 2.23
' Custom types for serializing/deserializing data
Type DBCommand (Name As String, Parameters() As Object)
Type DBResult (Tag As Object, Columns As Map, Rows As List)
' Holds a list of configured database identifiers (e.g., "DB1", "DB2")
Public listaDeCP As List
' A temporary list to store found configuration file names during startup
Private cpFiles As List
' Global maps to manage database connectors and loaded SQL commands
Public Connectors, commandsMap As Map
' SQL object for interacting with the local users and logs database (SQLite)
Public SQL1 As SQL
' Object for securely hashing and verifying passwords
Private bc As BCrypt
' A Java ReentrantLock object to protect Main.Connectors during Hot-Swapping (thread-safety)
Public MainConnectorsLock As JavaObject
' A Java ReentrantLock object to protect the log caches (QueryLogCache and ErrorLogCache)
Public LogCacheLock As JavaObject
' Timer for executing periodic tasks, such as log cleanup
Public timerLogs As Timer
' Map to store the SQLite logging status (True/False) for each DBKey (DB1, DB2, etc.)
Public SQLiteLoggingStatusByDB As Map
' Global flag indicating if AT LEAST one database has SQLite logging enabled
Public IsAnySQLiteLoggingEnabled As Boolean
' Type to encapsulate the result of parameter validation
Type ParameterValidationResult ( _
Success As Boolean, _
ErrorMessage As String, _
ParamsToExecute As List _ ' The final list of parameters to use in the SQL execution
)
' In-memory cache for performance logs (query_logs)
Public QueryLogCache As List
' In-memory cache for error and warning logs
Public ErrorLogCache As List
' Threshold of records to force a batch write to the DB
Public LOG_CACHE_THRESHOLD As Int = 400
' Flag to enable/disable verbose logging, set in AppStart
Dim logger As Boolean
' Thread-Safe Map to store the latest metrics for each connection pool
Public LatestPoolStats As Map
' Counter for timer ticks to control VACUUM frequency
Private TimerTickCount As Int = 0
' Run VACUUM every 48 cycles (48 * 30 minutes = 24 hours)
Private const VACUUM_CYCLES As Int = 48
' Granular control for TEXT file logging (CSV)
Public TextLoggingStatusByDB As Map
' Main object for managing all connection pools (RDCConnector instances)
Public ConnectionPoolManager1 As ConnectionPoolManager
End Sub
' --- Main application entry point ---
Sub AppStart (Args() As String)
' Initialize Server-Sent Events handler
SSE.Initialize
' Set logger flag based on build mode (DEBUG or RELEASE)
#if DEBUG
logger = True
' Use a small threshold in DEBUG mode for easier testing
LOG_CACHE_THRESHOLD = 10
#else
logger = False
#End If
Log("LOG_CACHE_THRESHOLD: " & LOG_CACHE_THRESHOLD)
' Copy web admin panel files if they don't exist
CopiarRecursoSiNoExiste("manager.html", "www")
CopiarRecursoSiNoExiste("login.html", "www")
' Copy root files (configs, start/stop scripts) if they don't exist
CopiarRecursoSiNoExiste("config.properties", "")
' CopiarRecursoSiNoExiste("config.DB2.properties", "")
' CopiarRecursoSiNoExiste("config.DB3.properties", "")
CopiarRecursoSiNoExiste("start.bat", "")
CopiarRecursoSiNoExiste("start2.bat", "")
CopiarRecursoSiNoExiste("stop.bat", "")
CopiarRecursoSiNoExiste("reiniciaProcesoBow.bat", "")
CopiarRecursoSiNoExiste("reiniciaProcesoPM2.bat", "")
' Initialize the BCrypt password hashing library
bc.Initialize("BC")
' Initialize in-memory log caches
QueryLogCache.Initialize
ErrorLogCache.Initialize
' === 1. Initialize the local user database (SQLite) and log tables ===
InitializeSQLiteDatabase
' === 2. Initialize global maps defined in GlobalParameters.bas ===
GlobalParameters.mpLogs.Initialize
GlobalParameters.mpTotalRequests.Initialize
GlobalParameters.mpTotalConnections.Initialize
GlobalParameters.mpBlockConnection.Initialize
' Ensure the active request counter map is thread-safe
GlobalParameters.ActiveRequestsCountByDB = srvr.CreateThreadSafeMap
' === 3. Initialize the main HTTP server structures ===
listaDeCP.Initialize
srvr.Initialize("")
' Connectors map must be thread-safe
Connectors = srvr.CreateThreadSafeMap
commandsMap.Initialize
' Initialize the stats map as Thread-Safe
LatestPoolStats = srvr.CreateThreadSafeMap
' Initialize the map for text logging status
TextLoggingStatusByDB.Initialize
' Initialize the granular SQLite logging status map
SQLiteLoggingStatusByDB.Initialize
' Create a ReentrantLock instance to protect Main.Connectors
MainConnectorsLock.InitializeNewInstance("java.util.concurrent.locks.ReentrantLock", Null)
' Initialize the lock for log caches
LogCacheLock.InitializeNewInstance("java.util.concurrent.locks.ReentrantLock", Null)
' Initialize the Manager, which in turn initializes all pool wrappers.
ConnectionPoolManager1.Initialize
' === 4. INITIALIZATION OF THE MAIN DATABASE CONNECTOR (DB1) ===
Try
' Initialize the main 'DB1' connector
Dim con1 As RDCConnector
con1.Initialize("DB1")
' Add it to the thread-safe map
Connectors.Put("DB1", con1)
' Set the server port from the DB1 config file
srvr.Port = con1.serverPort
' Add 'DB1' to the list of active database keys
listaDeCP.Add("DB1")
Log($"Main.AppStart: Connector 'DB1' initialized successfully on port: ${srvr.Port}"$)
' Read the 'enableSQLiteLogs' setting from config.properties (default to 0)
Dim enableLogsSetting As Int = con1.config.GetDefault("enableSQLiteLogs", 0).As(Int)
Dim isEnabled As Boolean = (enableLogsSetting = 1)
' Store the status in the granular map
SQLiteLoggingStatusByDB.Put("DB1", isEnabled)
' Read the 'enableTextLogging' setting
Dim enableTextLogsSetting As Int = con1.config.GetDefault("enableTextLogging", 0).As(Int)
Dim isTextEnabled As Boolean = (enableTextLogsSetting = 1)
' Store the text log status
TextLoggingStatusByDB.Put("DB1", isTextEnabled)
Catch
' This is a critical failure; the server cannot start without DB1
Dim ErrorMsg As String = $"Main.AppStart: CRITICAL ERROR initializing connector 'DB1': ${LastException.Message}"$
Log(ErrorMsg)
' Log the error to the SQLite DB (if it's already initialized)
LogServerError("ERROR", "Main.AppStart", ErrorMsg, "DB1", Null, Null)
' Stop the application
ExitApplication
End Try
' === 5. DETECTION AND INITIALIZATION OF ADDITIONAL DATABASES (DB2, DB3, DB4) ===
' Scan the application's root directory for configuration files
cpFiles = File.ListFiles("./")
If cpFiles.Size > 0 Then
For Each fileName As String In cpFiles
Dim keyPrefix As String = "config."
Dim keySuffix As String = ".properties"
' 1. Filter and exclude DB1 (which is already loaded)
' Find files matching "config.xxx.properties" but not "config.properties"
If fileName.StartsWith(keyPrefix) And fileName.EndsWith(keySuffix) And fileName <> "config.properties" Then
Try
' 2. Extract the key ("xxx" from config.xxx.properties)
Dim keyLength As Int = fileName.Length - keySuffix.Length
Dim dbKey As String = fileName.SubString2(keyPrefix.Length, keyLength)
' ROBUSTNESS: Ensure the key is UPPERCASE for consistency.
' Handlers normalize the key to uppercase, so we must match that.
dbKey = dbKey.ToUpperCase.Trim
Log($"Main.AppStart: Configuration file detected: '${fileName}'. Initializing connector '${dbKey}'."$)
Dim newCon As RDCConnector
' 3. Initialize the RDC Connector (which reads its own config.dbKey.properties file)
newCon.Initialize(dbKey)
' 4. Update global structures (Thread-Safe Maps)
Connectors.Put(dbKey, newCon)
listaDeCP.Add(dbKey)
' 5. Granular Logging Logic
' Capture the logging status for this new DB
Dim enableLogsSetting As Int = newCon.config.GetDefault("enableSQLiteLogs", 0).As(Int)
Dim isEnabled As Boolean = (enableLogsSetting = 1)
SQLiteLoggingStatusByDB.Put(dbKey, isEnabled)
' Capture text logging status for this new DB
Dim enableTextLogsSetting As Int = newCon.config.GetDefault("enableTextLogging", 0).As(Int)
Dim isTextEnabled As Boolean = (enableTextLogsSetting = 1)
TextLoggingStatusByDB.Put(dbKey, isTextEnabled)
Log("TEXT LOGGING STATUS BY DB: " & TextLoggingStatusByDB)
' Note: Global re-evaluation of IsAnySQLiteLoggingEnabled is done at the end of AppStart.
Catch
' 6. Error Handling: If a file is invalid (e.g., bad credentials, malformed URL),
' the server should log the error but continue trying with the next file.
Dim ErrorMsg As String = $"Main.AppStart: CRITICAL ERROR initializing connector '${dbKey}' from '${fileName}': ${LastException.Message}"$
Log(ErrorMsg)
LogServerError("ERROR", "Main.AppStart", ErrorMsg, dbKey, Null, Null)
End Try
End If
Next
End If
' Final log of all databases the server is managing.
Dim sbListaDeCP_Log As StringBuilder
sbListaDeCP_Log.Initialize
For Each item As String In listaDeCP
sbListaDeCP_Log.Append(item).Append(", ")
Next
If sbListaDeCP_Log.Length > 0 Then
sbListaDeCP_Log.Remove(sbListaDeCP_Log.Length - 2, sbListaDeCP_Log.Length)
End If
Log($"Main.AppStart: Configured and ready databases: [${sbListaDeCP_Log.ToString}]"$)
' <<<< Initialization block for the log cleanup Timer >>>>
' UNCONDITIONAL Initialization of the Timer (Ensures the object exists and prevents IllegalStateException)
timerLogs.Initialize("TimerLogs", 1800000) ' 30 minutes = 1800 * 1000 = 1800000 ms
' CONDITIONAL CONTROL BASED ON GRANULAR STATUS
IsAnySQLiteLoggingEnabled = False
For Each dbStatus As Boolean In SQLiteLoggingStatusByDB.Values
If dbStatus Then
IsAnySQLiteLoggingEnabled = True
Exit ' If one is active, it's enough to turn on the Timer
End If
Next
If IsAnySQLiteLoggingEnabled Then
timerLogs.Enabled = True
If logger Then Log("Main.AppStart: Log cleanup timer ACTIVATED (at least one DB requires logs).")
Else
timerLogs.Enabled = False
If logger Then Log("Main.AppStart: Log cleanup timer DISABLED (no DB requires logs).")
End If
' <<<< End of Timer block >>>>
' === 6. REGISTERING HTTP HANDLERS FOR THE SERVER ===
srvr.AddHandler("/ping", "ping", False)
srvr.AddHandler("/test", "TestHandler", False)
srvr.AddHandler("/login", "LoginHandler", False)
srvr.AddHandler("/dologin", "DoLoginHandler", False)
srvr.AddHandler("/logout", "LogoutHandler", False)
srvr.AddHandler("/changepass", "ChangePassHandler", False)
srvr.AddHandler("/manager", "Manager", False)
srvr.AddHandler("/DBJ", "DBHandlerJSON", False)
srvr.AddHandler("/dbrquery", "DBHandlerJSON", False)
srvr.AddHandler("/favicon.ico", "faviconHandler", False)
srvr.AddHandler("/stats-stream", "SSEHandler", False)
srvr.AddHandler("/*", "DBHandlerB4X", False)
' 7. Start the HTTP server.
srvr.Start
Log("===========================================================")
Log($"-=== jRDC is running on port: ${srvr.Port} (version = $1.2{VERSION}) ===-"$)
Log("===========================================================")
' 8. Start the B4J message loop.
StartMessageLoop
End Sub
' --- Subroutine to initialize the local user database (SQLite) ---
Sub InitializeSQLiteDatabase
Dim dbFileName As String = "users.db"
' Check if the database file already exists
If File.Exists(File.DirApp, dbFileName) = False Then
' --- Create a new database ---
Log("Creating new user database: " & dbFileName)
SQL1.InitializeSQLite(File.DirApp, dbFileName, True)
' Create 'users' table
Dim createUserTable As String = "CREATE TABLE users (username TEXT PRIMARY KEY, password_hash TEXT NOT NULL)"
SQL1.ExecNonQuery(createUserTable)
' Create 'query_logs' table
If logger Then Log("Creating 'query_logs' table with performance columns.")
Dim createQueryLogsTable As String = "CREATE TABLE query_logs (id INTEGER PRIMARY KEY AUTOINCREMENT, query_name TEXT, duration_ms INTEGER, timestamp INTEGER, db_key TEXT, client_ip TEXT, busy_connections INTEGER, handler_active_requests INTEGER)"
SQL1.ExecNonQuery(createQueryLogsTable)
' Set PRAGMA for better performance (Write-Ahead Logging)
SQL1.ExecNonQuery("PRAGMA journal_mode=WAL;")
SQL1.ExecNonQuery("PRAGMA synchronous=NORMAL;")
' Insert default user
Dim defaultUser As String = "admin"
Dim defaultPass As String = "admin"
Dim hashedPass As String = bc.hashpw(defaultPass, bc.gensalt)
SQL1.ExecNonQuery2("INSERT INTO users (username, password_hash) VALUES (?, ?)", Array As Object(defaultUser, hashedPass))
Log($"Default user created -> user: ${defaultUser}, pass: ${defaultPass}"$)
' Create 'errores' (errors) table
Log("Creating 'errores' table for event logging.")
Dim createErrorsTable As String = "CREATE TABLE errores (id INTEGER PRIMARY KEY AUTOINCREMENT, timestamp INTEGER, type TEXT, source TEXT, message TEXT, db_key TEXT, command_name TEXT, client_ip TEXT)"
SQL1.ExecNonQuery(createErrorsTable)
If logger Then Log("Creating performance indexes on log tables.")
' Index on timestamp for fast cleanup (DELETE/ORDER BY) in query_logs
SQL1.ExecNonQuery("CREATE INDEX idx_query_timestamp ON query_logs(timestamp)")
' Index on duration_ms for the 'slowqueries' query (ORDER BY)
SQL1.ExecNonQuery("CREATE INDEX idx_query_duration ON query_logs(duration_ms)")
' Index on timestamp for fast cleanup of the errors table
SQL1.ExecNonQuery("CREATE INDEX idx_error_timestamp ON errores(timestamp)")
Else
' --- Load existing database ---
SQL1.InitializeSQLite(File.DirApp, dbFileName, True)
Log("User database loaded.")
' Ensure WAL mode is set on existing DBs
SQL1.ExecNonQuery("PRAGMA journal_mode=WAL;")
SQL1.ExecNonQuery("PRAGMA synchronous=NORMAL;")
' >>> START: Migration logic (ALTER TABLE) if the DB already existed <<<
If logger Then Log("Verifying and migrating 'query_logs' table if necessary.")
' Check if 'query_logs' table exists
If SQL1.ExecQuerySingleResult("SELECT name FROM sqlite_master WHERE type='table' AND name='query_logs'") = Null Then
If logger Then Log("'query_logs' table not found, creating it with performance columns.")
Dim createQueryLogsTable As String = "CREATE TABLE query_logs (id INTEGER PRIMARY KEY AUTOINCREMENT, query_name TEXT, duration_ms INTEGER, timestamp INTEGER, db_key TEXT, client_ip TEXT, busy_connections INTEGER, handler_active_requests INTEGER)"
SQL1.ExecNonQuery(createQueryLogsTable)
Else
' If the query_logs table already exists, check and add missing columns
Dim columnExists As Boolean
Dim rs As ResultSet
' --- VERIFY AND ADD busy_connections ---
columnExists = False
rs = SQL1.ExecQuery("PRAGMA table_info(query_logs)")
Do While rs.NextRow
If rs.GetString("name").EqualsIgnoreCase("busy_connections") Then
columnExists = True
Exit ' Column already exists, exit loop
End If
Loop
rs.Close
If columnExists = False Then
If logger Then Log("Adding column 'busy_connections' to query_logs.")
SQL1.ExecNonQuery("ALTER TABLE query_logs ADD COLUMN busy_connections INTEGER DEFAULT 0")
End If
' --- VERIFY AND ADD handler_active_requests ---
columnExists = False
rs = SQL1.ExecQuery("PRAGMA table_info(query_logs)")
Do While rs.NextRow
If rs.GetString("name").EqualsIgnoreCase("handler_active_requests") Then
columnExists = True
Exit ' Column already exists, exit loop
End If
Loop
rs.Close
If columnExists = False Then
If logger Then Log("Adding column 'handler_active_requests' to query_logs.")
SQL1.ExecNonQuery("ALTER TABLE query_logs ADD COLUMN handler_active_requests INTEGER DEFAULT 0")
End If
' --- VERIFY AND ADD timestamp_text_local ---
columnExists = False
rs = SQL1.ExecQuery("PRAGMA table_info(query_logs)")
Do While rs.NextRow
If rs.GetString("name").EqualsIgnoreCase("timestamp_text_local") Then
columnExists = True
Exit ' Column already exists, exit loop
End If
Loop
rs.Close
If columnExists = False Then
If logger Then Log("Adding column 'timestamp_text_local' to query_logs.")
' Use 'TEXT' to store the formatted date/time string.
SQL1.ExecNonQuery("ALTER TABLE query_logs ADD COLUMN timestamp_text_local TEXT")
End If
' >>> START: Migration logic for 'errores' if DB already existed <<<
If logger Then Log("Verifying and migrating 'errores' table if necessary.")
If SQL1.ExecQuerySingleResult("SELECT name FROM sqlite_master WHERE type='table' AND name='errores'") = Null Then
If logger Then Log("'errores' table not found, creating it.")
Dim createErrorsTable As String = "CREATE TABLE errores (id INTEGER PRIMARY KEY AUTOINCREMENT, timestamp INTEGER, type TEXT, source TEXT, message TEXT, db_key TEXT, command_name TEXT, client_ip TEXT)"
SQL1.ExecNonQuery(createErrorsTable)
Else
If logger Then Log("'errores' table already exists.")
End If
' >>> END: Migration logic for 'errores' <<<
End If
' >>> END: Migration logic (ALTER TABLE) <<<
End If
End Sub
' Public subroutine to log query performance.
Public Sub LogQueryPerformance(QueryName As String, DurationMs As Long, DbKey As String, ClientIp As String, HandlerActiveRequests As Int, PoolBusyConnections As Int)
' Check if logging is enabled for this specific DBKey
Dim isEnabled As Boolean = SQLiteLoggingStatusByDB.GetDefault(DbKey, False)
If isEnabled Then
' Set date format for the new text timestamp column
DateTime.DateFormat = "yyyy-MM-dd HH:mm:ss.SSS"
Dim formattedTimestamp As String = DateTime.Date(DateTime.Now)
' 1. Create the data map (log entry)
Dim logEntry As Map = CreateMap("query_name": QueryName, "duration_ms": DurationMs, "timestamp": DateTime.Now, _
"db_key": DbKey, "client_ip": ClientIp, "busy_connections": PoolBusyConnections, _
"handler_active_requests": HandlerActiveRequests, "timestamp_text_local": formattedTimestamp)
' 2. Critical Zone: Add to cache and check threshold
Dim shouldWriteBatch As Boolean = False
' Use the *cache lock* to ensure adding and size-checking are atomic
LogCacheLock.RunMethod("lock", Null)
QueryLogCache.Add(logEntry)
If QueryLogCache.Size >= LOG_CACHE_THRESHOLD Then
shouldWriteBatch = True
End If
LogCacheLock.RunMethod("unlock", Null)
' 3. If the threshold was reached, trigger the write.
' This MUST be done OUTSIDE the lock.
If shouldWriteBatch Then
CallSub(Me, "WriteQueryLogsBatch")
End If
End If
End Sub
' --- Subroutine to log errors and warnings in the 'errores' table. ---
Public Sub LogServerError(Type0 As String, Source As String, Message As String, DBKey As String, CommandName As String, ClientIp As String)
If logger Then Log($">>>> LogServerError <<<<<${CRLF}tipo:${Type0}, source:${Source}, message:${Message}, dbkey:${DBKey}, commandanme:${CommandName}"$)
' Check if logging is enabled for this specific DBKey (or fallback if DBKey is null)
Dim isEnabled As Boolean = SQLiteLoggingStatusByDB.GetDefault(DBKey, False)
If isEnabled Then
' 1. Create the log entry map
Dim logEntry As Map = CreateMap("timestamp": DateTime.Now, "type": Type0, "source": Source, "message": Message, _
"db_key": DBKey, "command_name": CommandName, "client_ip": ClientIp)
Dim shouldWriteBatch As Boolean = False
' 2. Critical Zone: Add to cache and check threshold
' Use the *cache lock* for Thread Safety
LogCacheLock.RunMethod("lock", Null)
' Log(">>>>> Agregamos a errorLog")
ErrorLogCache.Add(logEntry)
If ErrorLogCache.Size >= LOG_CACHE_THRESHOLD Then
shouldWriteBatch = True
End If
LogCacheLock.RunMethod("unlock", Null)
' 3. If threshold was reached, trigger the write
If shouldWriteBatch Then
CallSub(Me, "WriteErrorLogsBatch")
End If
Else
' Logging is disabled for this DBKey, so the log is skipped.
End If
End Sub
' Writes the cached performance logs to the SQLite DB in a single transaction
Public Sub WriteQueryLogsBatch
Dim logsToWrite As List
logsToWrite.Initialize ' 1. Initialize the local list (CRITICAL)
' === STEP 1: Atomic Cache Swap (Protected by ReentrantLock) ===
LogCacheLock.RunMethod("lock", Null)
If QueryLogCache.Size = 0 Then
' Cache is empty, release lock and return
LogCacheLock.RunMethod("unlock", Null)
Return
End If
' *** CRITICAL FIX: Copy content (AddAll) instead of reference. ***
logsToWrite.AddAll(QueryLogCache)
Dim batchSize As Int = logsToWrite.Size
' Clear the global cache. logsToWrite now holds the copy of the items.
QueryLogCache.Initialize
LogCacheLock.RunMethod("unlock", Null)
' Check if text logging is enabled for any of these logs
If logsToWrite.Size > 0 Then
' Call the text archiving sub on a separate worker thread.
' This is NON-BLOCKING for the current thread, which will proceed to the SQLite transaction.
CallSubDelayed2(Me, "ArchiveQueryLogsToDailyFile", logsToWrite)
End If
' === STEP 2: Transactional Write to SQLite ===
Try
' 1. Begin the transaction: Everything that follows is a single disk operation.
SQL1.BeginTransaction
For Each logEntry As Map In logsToWrite
' Insert the log entry
SQL1.ExecNonQuery2("INSERT INTO query_logs (query_name, duration_ms, timestamp, db_key, client_ip, busy_connections, handler_active_requests, timestamp_text_local) VALUES (?, ?, ?, ?, ?, ?, ?, ?)", _
Array As Object(logEntry.Get("query_name"), logEntry.Get("duration_ms"), logEntry.Get("timestamp"), logEntry.Get("db_key"), _
logEntry.Get("client_ip"), logEntry.Get("busy_connections"), logEntry.Get("handler_active_requests"), _
logEntry.Get("timestamp_text_local")))
Next
' 2. Finalize the transaction: Efficient write to disk.
SQL1.TransactionSuccessful
If logger Then Log($"[LOG BATCH] Batch of ${batchSize} performance logs written successfully."$)
Catch
' If it fails, undo all logs in this batch and log the failure.
SQL1.Rollback
Dim ErrorMsg As String = "CRITICAL ERROR: Failed to write performance log batch to SQLite: " & LastException.Message
Log(ErrorMsg)
' Use LogServerError so the failure is recorded in the 'errores' table (if logging is enabled)
LogServerError("ERROR", "Main.WriteQueryLogsBatch", ErrorMsg, Null, "log_batch_write_performance", Null)
End Try
End Sub
' --- Event subroutine for the 'timerLogs' Timer. ---
' The 'Enabled' state of the Timer is already controlled by IsAnySQLiteLoggingEnabled in AppStart and Manager.
Sub TimerLogs_Tick
Try
' 1. Flush performance logs
WriteQueryLogsBatch
' 2. Flush error logs
WriteErrorLogsBatch
' 3. Clean up and VACUUM (this sub also checks IsAnySQLiteLoggingEnabled)
borraArribaDe30000Logs
Catch
Dim ErrorMsg As String = "ERROR in TimerLogs_Tick while trying to clear logs: " & LastException.Message
Log(ErrorMsg)
LogServerError("ERROR", "Main.TimerLogs_Tick", ErrorMsg, Null, "log_cleanup", Null)
End Try
End Sub
' Writes the cached error logs to the SQLite DB in a single transaction
Public Sub WriteErrorLogsBatch
Dim logsToWrite As List
logsToWrite.Initialize
' === STEP 1: Atomic Cache Swap (Protected by ReentrantLock) ===
' Bloqueamos el LogCacheLock para garantizar la atomicidad de la copia y limpieza.
LogCacheLock.RunMethod("lock", Null)
If ErrorLogCache.Size = 0 Then
' La caché está vacía, liberamos el lock inmediatamente y salimos.
LogCacheLock.RunMethod("unlock", Null)
Return
End If
' *** Copiar el contenido de la caché global de forma atómica. ***
logsToWrite.AddAll(ErrorLogCache)
' Usar el tamaño de la lista copiada para el procesamiento.
Dim batchSize As Int = logsToWrite.Size
' Log(logsToWrite)
' Limpiar la caché global. logsToWrite es ahora una lista independiente y poblada.
ErrorLogCache.Initialize
LogCacheLock.RunMethod("unlock", Null) ' Liberar el lock.
If logger Then Log($"[LOG BATCH] Starting transactional write of ${batchSize} ERROR logs to SQLite. Logs copied: ${batchSize}"$)
' === La corrección de Lógica ocurre aquí: La llamada a ArchiveErrorLogsToDailyFile
' y el proceso transaccional ocurren AHORA, después de asegurar que logsToWrite
' tiene contenido y que el lock fue liberado. ===
' 1. (Opcional, si el logging de texto CSV está habilitado)
If batchSize > 0 Then
' Delegar a una nueva subrutina para manejar la I/O de disco CSV (CallSubDelayed2)
CallSubDelayed2(Me, "ArchiveErrorLogsToDailyFile", logsToWrite)
End If
' === STEP 2: Escritura Transaccional a SQLite (Usa logsToWrite) ===
If batchSize = 0 Then
' Este caso no debería ocurrir con la lógica anterior, pero es un chequeo de seguridad.
Log("WARNING: Failed to copy list. logsToWrite is empty. Aborting write.")
Return
End If
Try
' 1. Iniciar la transacción.
SQL1.BeginTransaction
For Each logEntry As Map In logsToWrite
' Insertar la entrada de log
SQL1.ExecNonQuery2("INSERT INTO errores (timestamp, type, source, message, db_key, command_name, client_ip) VALUES (?, ?, ?, ?, ?, ?, ?)", _
Array As Object(logEntry.Get("timestamp"), logEntry.Get("type"), logEntry.Get("source"), logEntry.Get("message"), _
logEntry.Get("db_key"), logEntry.Get("command_name"), logEntry.Get("client_ip")))
Next
' 2. Confirmar la transacción.
SQL1.TransactionSuccessful
If logger Then Log($"[LOG BATCH] Batch of ${logsToWrite.Size} ERROR logs written successfully."$)
Catch
' 3. Rollback si falla.
SQL1.Rollback
Dim ErrorMsg As String = "CRITICAL ERROR: Failed to write ERROR log batch to SQLite: " & LastException.Message
Log(ErrorMsg)
End Try
End Sub
' Deletes the oldest records from 'query_logs' table and runs VACUUM.
Sub borraArribaDe30000Logs 'ignore
If IsAnySQLiteLoggingEnabled Then ' Only run if at least one DB requires logs.
' 1. Cleanup of Performance Logs (query_logs)
If logger Then Log("Trimming 'query_logs' table, limit of 30,000 records.")
Dim fechaCorte As Long ' (cutoff date/timestamp)
' First, try to find the timestamp of the 30,001st record.
Try ' OFFSET 30000 skips the 30,000 most recent.
fechaCorte = SQL1.ExecQuerySingleResult($"SELECT timestamp FROM query_logs ORDER BY timestamp DESC LIMIT 1 OFFSET 30000"$)
Catch ' If the table has fewer than 30,000 records, the result is NULL or throws an exception.
fechaCorte = 0 ' Force to 0 so it doesn't delete anything.
End Try
' If a cutoff time was found (i.e., there are more than 30,000 records)...
If fechaCorte > 0 Then ' Execute the simple DELETE, which is very fast using the idx_query_timestamp index.
SQL1.ExecNonQuery2("DELETE FROM query_logs WHERE timestamp < ?", Array As Object(fechaCorte))
End If
' 2. Cleanup of Error Logs (errores)
Dim fechaCorteError As Long
Try ' OFFSET 15000 skips the 15,000 most recent.
fechaCorteError = SQL1.ExecQuerySingleResult($"SELECT timestamp FROM errores ORDER BY timestamp DESC LIMIT 1 OFFSET 15000"$)
Catch ' If the table has fewer than 15,000 records, result is NULL.
fechaCorteError = 0
End Try
' If a cutoff time was found...
If fechaCorteError > 0 Then
SQL1.ExecNonQuery2("DELETE FROM errores WHERE timestamp < ?", Array As Object(fechaCorteError))
End If
' 3. Control and Conditional Execution of VACUUM
TimerTickCount = TimerTickCount + 1
If TimerTickCount >= VACUUM_CYCLES Then
If logger Then Log("EXECUTING VACUUM (24-hour cycle completed).")
SQL1.ExecNonQuery("vacuum;") ' Execute VACUUM.
TimerTickCount = 0 ' Reset the counter.
Else
' Show how many cycles are left, only if logger is active.
If logger Then Log($"VACUUM skipped. ${VACUUM_CYCLES - TimerTickCount} cycles remaining until daily execution."$)
End If
Else
' If IsAnySQLiteLoggingEnabled is False, the Timer should not be active.
If logger Then Log("NOTICE: Log cleanup task skipped. Global SQLite logging is disabled.")
End If
End Sub
'Copies resources from the jar to the app directory
Sub CopiarRecursoSiNoExiste(NombreArchivo As String, SubCarpeta As String)
Dim DirDestino As String = File.Combine(File.DirApp, SubCarpeta)
' If the subfolder is not empty and doesn't exist, create it
If SubCarpeta <> "" And File.Exists(DirDestino, "") = False Then
File.MakeDir(DirDestino, "")
End If
Dim ArchivoDestino As String = File.Combine(DirDestino, NombreArchivo)
' If the target file doesn't exist, copy it from resources
If File.Exists(DirDestino, NombreArchivo) = False Then
Dim RutaRecurso As String
If SubCarpeta <> "" Then
' Path inside the JAR (e.g., "Files/www/manager.html")
RutaRecurso = "Files/" & SubCarpeta & "/" & NombreArchivo
Else
' Path inside the JAR (e.g., "Files/config.properties")
RutaRecurso = "Files/" & NombreArchivo
End If
' Get the correct class loader to access JAR resources
Dim classLoader As JavaObject = GetThreadContextClassLoader
Dim InStream As InputStream = classLoader.RunMethod("getResourceAsStream", Array(RutaRecurso))
If InStream.IsInitialized Then
Log($"Copiando recurso: '${RutaRecurso}'..."$)
' Call our own manual copy function
Dim OutStream As OutputStream = File.OpenOutput(DirDestino, NombreArchivo, False)
CopiarStreamManualmente(InStream, OutStream)
Log($"'${ArchivoDestino}' copiado correctamente."$)
Else
Log($"ERROR: Could not find the resource with the internal path: '${RutaRecurso}'"$)
End If
End If
End Sub
' Does not depend on any external libraries (like File.Copy)
Sub CopiarStreamManualmente (InStream As InputStream, OutStream As OutputStream)
Try
Dim buffer(1024) As Byte
Dim len As Int
' Read bytes from input stream
len = InStream.ReadBytes(buffer, 0, buffer.Length)
Do While len > 0
' Write bytes to output stream
OutStream.WriteBytes(buffer, 0, len)
' Read next chunk
len = InStream.ReadBytes(buffer, 0, buffer.Length)
Loop
Catch
LogError(LastException)
End Try
' Always close streams
InStream.Close
OutStream.Close
End Sub
' Helper function to get the correct Class Loader
Sub GetThreadContextClassLoader As JavaObject
Dim thread As JavaObject
thread = thread.InitializeStatic("java.lang.Thread").RunMethod("currentThread", Null)
Return thread.RunMethod("getContextClassLoader", Null)
End Sub
' This runs on a separate worker thread (via CallSubDelayed2)
Private Sub ArchiveQueryLogsToDailyFile(logs As List)
' Set date format for the filename
DateTime.DateFormat = "yyyy-MM-dd"
Dim dateStr As String = DateTime.Date(DateTime.Now)
Dim fileBaseName As String = $"query_logs_${dateStr}.csv"$
' Fields based on the log structure:
Dim HEADER_LINE As String = $""timestamp_text_local","query_name","duration_ms","db_key","client_ip","busy_connections","handler_active_requests","timestamp_millis""$
Dim sbContent As StringBuilder
sbContent.Initialize
' === 1. CHECK AND WRITE HEADER (CRITICAL) ===
' Only write the header if the file does NOT exist OR if it exists but is empty.
Dim writeHeader As Boolean = False
If Not(File.Exists(File.DirApp, fileBaseName)) Or File.Size(File.DirApp, fileBaseName) = 0 Then
writeHeader = True
End If
If writeHeader Then
sbContent.Append(HEADER_LINE).Append(CRLF)
End If
' === 2. GENERATE CONTENT IN MEMORY ===
For Each logEntry As Map In logs
' Check if text logging is enabled for this specific DBKey
Dim dbKey As String = logEntry.Get("db_key").As(String)
If TextLoggingStatusByDB.GetDefault(dbKey, False) Then
' Format the log line (using double quotes for CSV)
Dim line As String = $""${logEntry.Get("timestamp_text_local")}","${logEntry.Get("query_name")}",${logEntry.Get("duration_ms")},"${dbKey}","${logEntry.Get("client_ip")}",${logEntry.Get("busy_connections")},${logEntry.Get("handler_active_requests")},${logEntry.Get("timestamp")}"$
sbContent.Append(line).Append(CRLF)
End If
Next
' === 3. CONSOLIDATED WRITE TO DISK ===
If sbContent.Length > 0 Then
Dim outStream As OutputStream
Try
' APPEND mode (True) ensures that both the header (if written) and the logs are added.
outStream = File.OpenOutput(File.DirApp, fileBaseName, True)
Dim bytes() As Byte = sbContent.ToString.GetBytes("UTF8")
outStream.WriteBytes(bytes, 0, bytes.Length)
Catch
' Log the error to the SQLite database
LogServerError("ADVERTENCIA", "ArchiveQueryLogsToDailyFile", $"Fallo al escribir lote + HEADER en ${fileBaseName}: ${LastException.Message}"$, "SYSTEM", "Log Batch Write", "N/A")
End Try
If outStream.IsInitialized Then
outStream.Close
End If
End If
End Sub
' This runs on a separate worker thread (via CallSubDelayed2)
Private Sub ArchiveErrorLogsToDailyFile(logs As List)
Log(">>>>>>> ArchiveErrorLogsToDailyFile <<<<<<<<<<<<< ")
' Log(CRLF & logs)
' Set date format for the filename
DateTime.DateFormat = "yyyy-MM-dd"
Dim dateStr As String = DateTime.Date(DateTime.Now)
Dim fileBaseName As String = $"error_logs_${dateStr}.csv"$ ' Different filename
' AVAILABLE ERROR FIELDS (See LogServerError):
' timestamp, type, source, message, db_key, command_name, client_ip
Dim HEADER_LINE As String = $""timestamp","type","source","message","db_key","command_name","client_ip""$
Dim sbContent As StringBuilder
sbContent.Initialize
' === 1. CONDITIONAL WRITE OF HEADER ===
' Check if file exists or is empty. Only write header if it's the first time today.
Dim writeHeader As Boolean = False
Try
If Not(File.Exists(File.DirApp, fileBaseName)) Or File.Size(File.DirApp, fileBaseName) = 0 Then
writeHeader = True
End If
Catch
' In case of an I/O error checking the file, assume we should try to write the header.
writeHeader = True
End Try
If writeHeader Then
sbContent.Append(HEADER_LINE).Append(CRLF)
End If
' === 2. GENERATE CONTENT IN MEMORY ===
For Each logEntry As Map In logs
' Log($"--- agregamos ${logEntry.Get("db_key").As(String).ToUpperCase}"$)
Dim dbKey As String = logEntry.Get("db_key").As(String).ToUpperCase
' Log($"==== ${dbKey} -> ${TextLoggingStatusByDB.GetDefault(dbKey, False)}"$)
' Log(TextLoggingStatusByDB)
If TextLoggingStatusByDB.GetDefault(dbKey, False) Then
' Format the log line for CSV.
' The 'message' field is CRITICAL, as it can contain multiline exceptions or special characters.
Dim line As String = $""${logEntry.Get("timestamp")}","${logEntry.Get("type")}","${logEntry.Get("source")}","${logEntry.Get("message")}","${dbKey}","${logEntry.Get("command_name")}","${logEntry.Get("client_ip")}""$
sbContent.Append(line).Append(CRLF)
End If
Next
' === 3. CONSOLIDATED WRITE TO DISK ===
If sbContent.Length > 0 Then
Dim outStream As OutputStream
Try
' Open the stream in APPEND mode (True) to add the batch
outStream = File.OpenOutput(File.DirApp, fileBaseName, True)
Dim bytes() As Byte = sbContent.ToString.GetBytes("UTF8")
outStream.WriteBytes(bytes, 0, bytes.Length)
Catch
' I/O failure in secondary thread. Use LogServerError so it's logged to SQLite (if active)
LogServerError("ERROR", "ArchiveErrorLogsToDailyFile", $"Fallo E/S al escribir logs de ERRORES en ${fileBaseName}: ${LastException.Message}"$, "SYSTEM", "Error Log Batch Write", "N/A")
End Try
If outStream.IsInitialized Then
outStream.Close
End If
End If
End Sub

60
jRDC_Multi.b4j.meta Normal file
View File

@@ -0,0 +1,60 @@
ModuleBookmarks0=
ModuleBookmarks1=
ModuleBookmarks10=
ModuleBookmarks11=
ModuleBookmarks12=
ModuleBookmarks13=
ModuleBookmarks14=
ModuleBookmarks15=
ModuleBookmarks16=
ModuleBookmarks17=
ModuleBookmarks18=
ModuleBookmarks2=
ModuleBookmarks3=
ModuleBookmarks4=
ModuleBookmarks5=
ModuleBookmarks6=
ModuleBookmarks7=
ModuleBookmarks8=
ModuleBookmarks9=
ModuleBreakpoints0=
ModuleBreakpoints1=
ModuleBreakpoints10=
ModuleBreakpoints11=
ModuleBreakpoints12=
ModuleBreakpoints13=
ModuleBreakpoints14=
ModuleBreakpoints15=
ModuleBreakpoints16=
ModuleBreakpoints17=
ModuleBreakpoints18=
ModuleBreakpoints2=
ModuleBreakpoints3=
ModuleBreakpoints4=
ModuleBreakpoints5=
ModuleBreakpoints6=
ModuleBreakpoints7=
ModuleBreakpoints8=
ModuleBreakpoints9=
ModuleClosedNodes0=
ModuleClosedNodes1=
ModuleClosedNodes10=
ModuleClosedNodes11=
ModuleClosedNodes12=
ModuleClosedNodes13=
ModuleClosedNodes14=
ModuleClosedNodes15=
ModuleClosedNodes16=5,6
ModuleClosedNodes17=2,3
ModuleClosedNodes18=
ModuleClosedNodes2=
ModuleClosedNodes3=
ModuleClosedNodes4=
ModuleClosedNodes5=
ModuleClosedNodes6=
ModuleClosedNodes7=
ModuleClosedNodes8=
ModuleClosedNodes9=
NavigationStack=DBHandlerB4X,Handle,103,6,DBHandlerB4X,CleanupAndLog,221,0,Main,ArchiveQueryLogsToDailyFile,839,0,RDCConnector,Class_Globals,6,0,DBHandlerJSON,Handle,219,0,Main,AppStart,255,6,Main,LogQueryPerformance,510,0,Main,WriteErrorLogsBatch,654,6,Main,LogServerError,539,6,Main,ArchiveErrorLogsToDailyFile,869,6
SelectedBuild=0
VisibleModules=4,5,15,1,12,9,3,13

47
ping.bas Normal file
View File

@@ -0,0 +1,47 @@
B4J=true
Group=Default Group
ModulesStructureVersion=1
Type=Class
Version=10.3
@EndOfDesignText@
'Handler class for JSON requests from Web Clients (JavaScript/axios)
'VERSION 14 (Validación de Parámetros): Chequea que el número de '?' coincida con los parámetros recibidos.
Sub Class_Globals
End Sub
Public Sub Initialize
End Sub
Sub Handle(req As ServletRequest, resp As ServletResponse)
' --- Headers CORS ---
resp.SetHeader("Access-Control-Allow-Origin", "*")
resp.SetHeader("Access-Control-Allow-Methods", "GET, POST, OPTIONS")
resp.SetHeader("Access-Control-Allow-Headers", "Content-Type")
Try
SendSuccessResponse(resp, CreateMap("message": $"Pong ${DateTime.now}"$))
Catch
Log(LastException)
SendErrorResponse(resp, 500, LastException.Message)
End Try
End Sub
' --- Subrutinas de ayuda para respuestas JSON (sin cambios) ---
Private Sub SendSuccessResponse(resp As ServletResponse, dataMap As Map)
dataMap.Put("success", True)
Dim jsonGenerator As JSONGenerator
jsonGenerator.Initialize(dataMap)
resp.ContentType = "application/json"
resp.Write(jsonGenerator.ToString)
End Sub
Private Sub SendErrorResponse(resp As ServletResponse, statusCode As Int, errorMessage As String)
Dim resMap As Map = CreateMap("success": False, "error": errorMessage)
Dim jsonGenerator As JSONGenerator
jsonGenerator.Initialize(resMap)
resp.Status = statusCode
resp.ContentType = "application/json"
resp.Write(jsonGenerator.ToString)
End Sub

9
reiniciaProcesoBow.bat Normal file
View File

@@ -0,0 +1,9 @@
@rem Este script reinicia el proceso en PM2 del servidor de jRDC2
@rem estas lineas sirven para que el archivo bat corra en modo administrador.
set "params=%*"
cd /d "%~dp0" && ( if exist "%temp%\getadmin.vbs" del "%temp%\getadmin.vbs" ) && fsutil dirty query %systemdrive% 1>nul 2>nul || ( echo Set UAC = CreateObject^("Shell.Application"^) : UAC.ShellExecute "cmd.exe", "/k cd ""%~sdp0"" && ""%~s0"" %params%", "", "runas", 1 >> "%temp%\getadmin.vbs" && "%temp%\getadmin.vbs" && exit /B )
pm2 restart BotSoporte_4.0
exit

9
reiniciaProcesoPM2.bat Normal file
View File

@@ -0,0 +1,9 @@
@rem Este script reinicia el proceso en PM2 del servidor de jRDC2
@rem estas lineas sirven para que el archivo bat corra en modo administrador.
set "params=%*"
cd /d "%~dp0" && ( if exist "%temp%\getadmin.vbs" del "%temp%\getadmin.vbs" ) && fsutil dirty query %systemdrive% 1>nul 2>nul || ( echo Set UAC = CreateObject^("Shell.Application"^) : UAC.ShellExecute "cmd.exe", "/k cd ""%~sdp0"" && ""%~s0"" %params%", "", "runas", 1 >> "%temp%\getadmin.vbs" && "%temp%\getadmin.vbs" && exit /B )
pm2 restart jRDC-Multi
exit

8
start.bat Normal file
View File

@@ -0,0 +1,8 @@
@rem Este script mata el proceso del servidor y despues lo reinicia, necesita los archivos stop.bat y start2.bat
start cmd.exe /c stop.bat
timeout 2
start cmd.exe /c start2.bat %1
exit

3
start2.bat Normal file
View File

@@ -0,0 +1,3 @@
@TITLE -== DBR Server %1 %2 ==-
"C:\Program Files (x86)\Java\jdk-14\bin\java.exe" -jar jRDC_Multi.jar

1
stop.bat Normal file
View File

@@ -0,0 +1 @@
wmic Path win32_process Where "CommandLine Like '%%jRDC_Multi.jar%%'" Call Terminate