mirror of
https://github.com/KeymonSoft/jRDC-MultiDB-Hikari.git
synced 2026-04-17 12:56:23 +00:00
Commit inicial
This commit is contained in:
16
.gitattributes
vendored
Normal file
16
.gitattributes
vendored
Normal 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
2
.gitignore
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
**/Objects
|
||||
**/AutoBackups
|
||||
235
Cambios.bas
Normal file
235
Cambios.bas
Normal 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
52
ChangePassHandler.bas
Normal 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
184
ConnectionPoolManager.bas
Normal 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
772
DBHandlerB4X.bas
Normal 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
311
DBHandlerJSON.bas
Normal 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
43
DoLoginHandler.bas
Normal 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
|
||||
29
Files ejemplo/config.DB2.properties
Normal file
29
Files ejemplo/config.DB2.properties
Normal 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 ejemplo/config.DB3.properties
Normal file
29
Files ejemplo/config.DB3.properties
Normal 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 ejemplo/config.DB4.properties
Normal file
29
Files ejemplo/config.DB4.properties
Normal 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 ejemplo/config.properties
Normal file
29
Files ejemplo/config.properties
Normal 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(?, ?)
|
||||
9
Files ejemplo/reiniciaProcesoBow.bat
Normal file
9
Files ejemplo/reiniciaProcesoBow.bat
Normal 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
Files ejemplo/reiniciaProcesoPM2.bat
Normal file
9
Files ejemplo/reiniciaProcesoPM2.bat
Normal 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
8
Files ejemplo/start.bat
Normal 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
3
Files ejemplo/start2.bat
Normal 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
1
Files ejemplo/stop.bat
Normal file
@@ -0,0 +1 @@
|
||||
wmic Path win32_process Where "CommandLine Like '%%jRDC_Multi.jar%%'" Call Terminate
|
||||
21
Files ejemplo/www/login.html
Normal file
21
Files ejemplo/www/login.html
Normal 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 ejemplo/www/manager.html
Normal file
392
Files ejemplo/www/manager.html
Normal 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>
|
||||
29
Files/config.DB2.properties
Normal file
29
Files/config.DB2.properties
Normal 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.DB3.properties
Normal file
29
Files/config.DB3.properties
Normal 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.DB4.properties
Normal file
29
Files/config.DB4.properties
Normal 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
29
Files/config.properties
Normal 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(?, ?)
|
||||
9
Files/reiniciaProcesoBow.bat
Normal file
9
Files/reiniciaProcesoBow.bat
Normal 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
Files/reiniciaProcesoPM2.bat
Normal file
9
Files/reiniciaProcesoPM2.bat
Normal 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
8
Files/start.bat
Normal 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
3
Files/start2.bat
Normal 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
1
Files/stop.bat
Normal file
@@ -0,0 +1 @@
|
||||
wmic Path win32_process Where "CommandLine Like '%%jRDC_Multi.jar%%'" Call Terminate
|
||||
21
Files/www/login.html
Normal file
21
Files/www/login.html
Normal 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
392
Files/www/manager.html
Normal 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
18
GlobalParameters.bas
Normal 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
347
HikariConnectionPool.bas
Normal 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
23
LoginHandler.bas
Normal 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
17
LogoutHandler.bas
Normal 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
568
Manager.bas
Normal 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
|
||||
70
ParameterValidationUtils.bas
Normal file
70
ParameterValidationUtils.bas
Normal 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
253
RDCConnector.bas
Normal 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
161
README0.md
Normal 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
195
SSE.bas
Normal 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
131
SSEHandler.bas
Normal 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
35
TestHandler.bas
Normal 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
|
||||
75
config.properties.original
Normal file
75
config.properties.original
Normal 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
|
||||
|
||||
80
config.properties.postgres
Normal file
80
config.properties.postgres
Normal 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
39
faviconHandler.bas
Normal 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
987
jRDC_Multi.b4j
Normal 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
60
jRDC_Multi.b4j.meta
Normal 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
47
ping.bas
Normal 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
9
reiniciaProcesoBow.bat
Normal 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
9
reiniciaProcesoPM2.bat
Normal 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
8
start.bat
Normal 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
3
start2.bat
Normal file
@@ -0,0 +1,3 @@
|
||||
@TITLE -== DBR Server %1 %2 ==-
|
||||
|
||||
"C:\Program Files (x86)\Java\jdk-14\bin\java.exe" -jar jRDC_Multi.jar
|
||||
Reference in New Issue
Block a user