4 Commits

Author SHA1 Message Date
8b876e5095 - 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.

- 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`).

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.
2025-09-27 20:34:12 -06:00
616013f0fb - 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.
2025-09-27 14:14:38 -06:00
820fe9fc2b - 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`.
2025-09-23 00:16:03 -06:00
884cb96f9d - 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 [1, 2].

- Cambios principales:
		1. Refactorización de LogQueryPerformance y LogServerError para que solo almacenen logs en las cachés globales (QueryLogCache y ErrorLogCache) [3].
		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` [4-7].
		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.
2025-09-19 18:43:55 -06:00
16 changed files with 1972 additions and 718 deletions

View File

@@ -21,11 +21,53 @@ Sub Process_Globals
' los dominios permitidos. ' los dominios permitidos.
' - Ej: token:1224abcd5678fghi, dominio:"keymon.net" ' - Ej: token:1224abcd5678fghi, dominio:"keymon.net"
' - Ej: token:4321abcd8765fghi, dominio:"*" ' - Ej: token:4321abcd8765fghi, dominio:"*"
' - Que los logs, en lugar de guardar de uno en uno en la BD Sqlte, se guarden en memoria, se junten ... por ejemplo 100 y ya que haya 100, se guarden
' en una solo query a la BD Sqlite.
' - Que en el reporte de "Queries lentos" se pueda especificar de cuanto tiempo, ahorita esta de la ultima hora, pero que se pueda seleccionar desde una ' - Que en el reporte de "Queries lentos" se pueda especificar de cuanto tiempo, ahorita esta de la ultima hora, pero que se pueda seleccionar desde una
' lista, por ejemplo 15, 30, 45 y 60 minutos antes. ' lista, por ejemplo 15, 30, 45 y 60 minutos antes.
' - 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.
'
' - 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`).
'
' 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 [1, 2].
'
' - Cambios principales:
' 1. Refactorización de LogQueryPerformance y LogServerError para que solo almacenen logs en las cachés globales (QueryLogCache y ErrorLogCache) [3].
' 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` [4-7].
' 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
' 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.
' 2. 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 ' - Versión: 5.09.16
' - feat: Implementa control de logs de SQLite granular por DBKey y corrige la concurrencia del Timer en Hot-Swap. ' - 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.). ' - 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.).

View File

@@ -17,15 +17,15 @@ Sub Class_Globals
' La siguiente sección de constantes y utilidades se compila condicionalmente ' La siguiente sección de constantes y utilidades se compila condicionalmente
' solo si la directiva #if VERSION1 está activa. Esto es para dar soporte ' solo si la directiva #if VERSION1 está activa. Esto es para dar soporte
' a una versión antigua del protocolo de comunicación de DBRequestManager. ' a una versión antigua del protocolo de comunicación de DBRequestManager.
' #if VERSION1 ' #if VERSION1
' Constantes para identificar los tipos de datos en la serialización personalizada (protocolo V1). ' Constantes para identificar los tipos de datos en la serialización personalizada (protocolo V1).
Private const T_NULL = 0, T_STRING = 1, T_SHORT = 2, T_INT = 3, T_LONG = 4, T_FLOAT = 5 _ 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 ,T_DOUBLE = 6, T_BOOLEAN = 7, T_BLOB = 8 As Byte
' Utilidades para convertir entre tipos de datos y arrays de bytes. ' Utilidades para convertir entre tipos de datos y arrays de bytes.
Private bc As ByteConverter Private bc As ByteConverter
' Utilidad para comprimir/descomprimir streams de datos (usado en V1). ' Utilidad para comprimir/descomprimir streams de datos (usado en V1).
Private cs As CompressedStreams Private cs As CompressedStreams
' #end if ' #end if
' Mapa para convertir tipos de columna JDBC de fecha/hora a los nombres de métodos de Java ' Mapa para convertir tipos de columna JDBC de fecha/hora a los nombres de métodos de Java
' para obtener los valores correctos de ResultSet. ' para obtener los valores correctos de ResultSet.
@@ -105,17 +105,34 @@ Sub Handle(req As ServletRequest, resp As ServletResponse)
con = Connector.GetConnection(dbKey) ' ¡La conexión a la BD se obtiene aquí del pool de conexiones! con = Connector.GetConnection(dbKey) ' ¡La conexión a la BD se obtiene aquí del pool de conexiones!
' <<<< ¡BUSY_CONNECTIONS YA SE CAPTURABA BIEN! >>>>
' Este bloque captura el número de conexiones actualmente ocupadas en el pool ' Este bloque captura el número de conexiones actualmente ocupadas en el pool
' *después* de que esta petición ha obtenido la suya. ' *después* de que esta petición ha obtenido la suya.
If Connector.IsInitialized Then If Connector.IsInitialized Then
Dim poolStats As Map = Connector.GetPoolStats Dim poolStats As Map = Connector.GetPoolStats
If poolStats.ContainsKey("BusyConnections") Then If poolStats.ContainsKey("BusyConnections") Then
' <<<< ¡CORRECCIÓN CLAVE: Aseguramos que el valor sea Int! >>>>
poolBusyConnectionsForLog = poolStats.Get("BusyConnections").As(Int) ' Capturamos el valor. poolBusyConnectionsForLog = poolStats.Get("BusyConnections").As(Int) ' Capturamos el valor.
Log($">>>>>>>>>> ${poolStats.Get("BusyConnections")} "$)
End If End If
End If End If
' <<<< ¡FIN DE CAPTURA! >>>>
Dim cachedStatsB4X As Map = Main.LatestPoolStats.Get(dbKey).As(Map)
If cachedStatsB4X.IsInitialized Then
' 1. Actualizar Busy Connections y Active Requests
cachedStatsB4X.Put("BusyConnections", poolBusyConnectionsForLog)
cachedStatsB4X.Put("HandlerActiveRequests", requestsBeforeDecrement)
' 2. Capturar TotalConnections y IdleConnections (ya disponibles en 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-escribir el mapa en el cache global (es Thread-Safe)
Main.LatestPoolStats.Put(dbKey, cachedStatsB4X)
End If
' Log("Metodo: " & method) ' Log de depuración para identificar el método de la petición. ' Log("Metodo: " & method) ' Log de depuración para identificar el método de la petición.
@@ -128,25 +145,25 @@ Sub Handle(req As ServletRequest, resp As ServletResponse)
CleanupAndLog(dbKey, "error_in_" & method, duration, req.RemoteAddress, requestsBeforeDecrement, poolBusyConnectionsForLog, con) CleanupAndLog(dbKey, "error_in_" & method, duration, req.RemoteAddress, requestsBeforeDecrement, poolBusyConnectionsForLog, con)
Return ' Salida temprana si hay un error. Return ' Salida temprana si hay un error.
End If End If
' #if VERSION1 ' #if VERSION1
' Estas ramas se compilan solo si #if VERSION1 está activo (para protocolo antiguo). ' Estas ramas se compilan solo si #if VERSION1 está activo (para protocolo antiguo).
Else if method = "query" Then Else if method = "query" Then
in = cs.WrapInputStream(in, "gzip") ' Descomprime el stream de entrada si es protocolo V1. in = cs.WrapInputStream(in, "gzip") ' Descomprime el stream de entrada si es protocolo V1.
q = ExecuteQuery(dbKey, con, in, resp) q = ExecuteQuery(dbKey, con, in, resp)
If q = "error" Then If q = "error" Then
duration = DateTime.Now - start duration = DateTime.Now - start
CleanupAndLog(dbKey, "error_in_" & method, duration, req.RemoteAddress, requestsBeforeDecrement, poolBusyConnectionsForLog, con) CleanupAndLog(dbKey, "error_in_" & method, duration, req.RemoteAddress, requestsBeforeDecrement, poolBusyConnectionsForLog, con)
Return Return
End If End If
Else if method = "batch" Then Else if method = "batch" Then
in = cs.WrapInputStream(in, "gzip") ' Descomprime el stream de entrada si es protocolo V1. in = cs.WrapInputStream(in, "gzip") ' Descomprime el stream de entrada si es protocolo V1.
q = ExecuteBatch(dbKey, con, in, resp) q = ExecuteBatch(dbKey, con, in, resp)
If q = "error" Then If q = "error" Then
duration = DateTime.Now - start duration = DateTime.Now - start
CleanupAndLog(dbKey, "error_in_" & method, duration, req.RemoteAddress, requestsBeforeDecrement, poolBusyConnectionsForLog, con) CleanupAndLog(dbKey, "error_in_" & method, duration, req.RemoteAddress, requestsBeforeDecrement, poolBusyConnectionsForLog, con)
Return Return
End If End If
' #end if ' #end if
Else if method = "batch2" Then Else if method = "batch2" Then
' Ejecuta un lote de comandos (INSERT, UPDATE, DELETE) utilizando el protocolo V2. ' Ejecuta un lote de comandos (INSERT, UPDATE, DELETE) utilizando el protocolo V2.
q = ExecuteBatch2(dbKey, con, in, resp) q = ExecuteBatch2(dbKey, con, in, resp)
@@ -167,9 +184,16 @@ Sub Handle(req As ServletRequest, resp As ServletResponse)
End If End If
Catch ' --- CATCH: Maneja errores generales de ejecución o de SQL --- Catch ' --- CATCH: Maneja errores generales de ejecución o de SQL ---
Log(LastException) ' Registra la excepción completa en el log. Dim errorMessage As String = LastException.Message
Main.LogServerError("ERROR", "DBHandlerB4X.Handle", LastException.Message, dbKey, q, req.RemoteAddress) ' <-- Nuevo Log If errorMessage.Contains("ORA-01002") Or errorMessage.Contains("recuperación fuera de secuencia") Then
SendPlainTextError(resp, 500, LastException.Message) ' Envía un error 500 al cliente. errorMessage = "SE USA EXECUTEQUERY EN LUGAR DE EXECUTECOMMAND: " & errorMessage
else If errorMessage.Contains("ORA-17003") Or errorMessage.Contains("Índice de columnas no válido") Then
errorMessage = "NUMERO DE PARAMETROS EQUIVOCADO: " & errorMessage
End If
Log(errorMessage) ' Registra la excepción completa en el log.
Main.LogServerError("ERROR", "DBHandlerB4X.Handle", errorMessage, dbKey, q, req.RemoteAddress) ' <-- Nuevo Log
SendPlainTextError(resp, 500, errorMessage) ' Envía un error 500 al cliente.
q = "error_in_b4x_handler" ' Aseguramos un valor para 'q' en caso de excepción. q = "error_in_b4x_handler" ' Aseguramos un valor para 'q' en caso de excepción.
End Try ' --- FIN: Bloque Try principal --- End Try ' --- FIN: Bloque Try principal ---
@@ -344,11 +368,12 @@ Private Sub ExecuteBatch2(DB As String, con As SQL, in As InputStream, resp As S
Dim m As Map = ser.ConvertBytesToObject(Bit.InputStreamToBytes(in)) Dim m As Map = ser.ConvertBytesToObject(Bit.InputStreamToBytes(in))
' Obtiene la lista de objetos DBCommand. ' Obtiene la lista de objetos DBCommand.
Dim commands As List = m.Get("commands") Dim commands As List = m.Get("commands")
Dim totalAffectedRows As Int = 0 ' Contador para acumular el total de filas afectadas.
' Prepara un objeto DBResult para la respuesta (aunque para batch no devuelve datos, solo confirmación). ' Prepara un objeto DBResult para la respuesta (aunque para batch no devuelve datos, solo confirmación).
Dim res As DBResult Dim res As DBResult
res.Initialize res.Initialize
res.columns = CreateMap("AffectedRows (N/A)": 0) ' Columna simbólica. res.columns = CreateMap("AffectedRows": 0) ' Columna simbólica.
res.Rows.Initialize res.Rows.Initialize
res.Tag = Null res.Tag = Null
@@ -390,10 +415,14 @@ Private Sub ExecuteBatch2(DB As String, con As SQL, in As InputStream, resp As S
End If End If
con.ExecNonQuery2(sqlCommand, validationResult.ParamsToExecute) ' Ejecuta el comando con la lista de parámetros validada. con.ExecNonQuery2(sqlCommand, validationResult.ParamsToExecute) ' Ejecuta el comando con la lista de parámetros validada.
totalAffectedRows = totalAffectedRows + 1 ' Acumulamos 1 por cada comando ejecutado sin error.
' <<< FIN VALIDACIÓN DE PARÁMETROS CENTRALIZADA DENTRO DEL BATCH >>> ' <<< FIN VALIDACIÓN DE PARÁMETROS CENTRALIZADA DENTRO DEL BATCH >>>
Next Next
res.Rows.Add(Array As Object(0)) ' Añade una fila simbólica al resultado para indicar éxito. res.Rows.Add(Array As Object(totalAffectedRows)) ' Añade una fila simbólica al resultado para indicar éxito.
con.TransactionSuccessful ' Si todos los comandos se ejecutaron sin error, confirma la transacción. con.TransactionSuccessful ' Si todos los comandos se ejecutaron sin error, confirma la transacción.
Catch Catch
' Si cualquier comando falla, se captura el error. ' Si cualquier comando falla, se captura el error.
@@ -408,7 +437,17 @@ Private Sub ExecuteBatch2(DB As String, con As SQL, in As InputStream, resp As S
resp.OutputStream.WriteBytes(data, 0, data.Length) resp.OutputStream.WriteBytes(data, 0, data.Length)
' Devuelve un resumen para el log. ' Devuelve un resumen para el log.
Return $"batch (size=${commands.Size})"$ ' Return $"batch (size=${commands.Size})"$
' Devuelve un resumen para el log, incluyendo el nombre de la query si es un lote de tamaño 1.
If commands.Size = 1 Then
' Obtenemos el único comando en el lote.
Dim cmd As DBCommand = commands.Get(0)
Return $"batch (size=1) - query: ${cmd.Name}"$
Else
' Si el lote es de tamaño > 1, mantenemos el resumen por tamaño.
Return $"batch (size=${commands.Size})"$
End If
End Sub End Sub
' --- Subrutinas para manejar la ejecución de queries y batches (Protocolo V1 - Compilación Condicional) --- ' --- Subrutinas para manejar la ejecución de queries y batches (Protocolo V1 - Compilación Condicional) ---
@@ -417,30 +456,41 @@ End Sub
' Ejecuta un lote de comandos usando el protocolo V1. ' Ejecuta un lote de comandos usando el protocolo V1.
Private Sub ExecuteBatch(DB As String, con As SQL, in As InputStream, resp As ServletResponse) As String Private Sub ExecuteBatch(DB As String, con As SQL, in As InputStream, resp As ServletResponse) As String
' Lee y descarta la versión del cliente. ' Log($"ExecuteBatch ${DB}"$)
Dim clientVersion As Float = ReadObject(in) 'ignore ' Lee y descarta la versión del cliente.
' Lee cuántos comandos vienen en el lote. Dim clientVersion As Float = ReadObject(in) 'ignore
Dim numberOfStatements As Int = ReadInt(in) ' Lee cuántos comandos vienen en el lote.
Dim res(numberOfStatements) As Int ' Array para resultados (aunque no se usa). Dim numberOfStatements As Int = ReadInt(in)
Dim res(numberOfStatements) As Int ' Array para resultados (aunque no se usa).
Dim singleQueryName As String = ""
Dim affectedCounts As List
Dim totalAffectedRows As Int
affectedCounts.Initialize
Try Try
con.BeginTransaction con.BeginTransaction
' Itera para procesar cada comando del lote. ' Itera para procesar cada comando del lote.
For i = 0 To numberOfStatements - 1 ' Log(numberOfStatements)
' Lee el nombre del comando y la lista de parámetros usando el deserializador V1. For i = 0 To numberOfStatements - 1
Dim queryName As String = ReadObject(in) ' Log($"i: ${i}"$)
Dim params As List = ReadList(in) ' Lee el nombre del comando y la lista de parámetros usando el deserializador V1.
Dim sqlCommand As String = Connector.GetCommand(DB, queryName) Dim queryName As String = ReadObject(in)
Dim params As List = ReadList(in)
' <<< INICIO NUEVA VALIDACIÓN: VERIFICAR SI EL COMANDO EXISTE (V1) >>> ' Log(params)
If sqlCommand = Null Or sqlCommand = "null" Or sqlCommand.Trim = "" Then If numberOfStatements = 1 Then
con.Rollback ' Deshace la transacción si un comando es inválido. singleQueryName = queryName 'Capturamos el nombre del query.
Dim errorMessage As String = $"El comando '${queryName}' no fue encontrado en el config.properties de '${DB}'."$ End If
Log(errorMessage) Dim sqlCommand As String = Connector.GetCommand(DB, queryName)
' Log(sqlCommand)
' <<< INICIO NUEVA VALIDACIÓN: VERIFICAR SI EL COMANDO EXISTE (V1) >>>
If sqlCommand = Null Or sqlCommand = "null" Or sqlCommand.Trim = "" Then
con.Rollback ' Deshace la transacción si un comando es inválido.
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) Main.LogServerError("ERROR", "DBHandlerB4X.ExecuteBatch (V1)", errorMessage, DB, queryName, Null)
SendPlainTextError(resp, 400, errorMessage) SendPlainTextError(resp, 400, errorMessage)
Return "error" Return "error"
End If End If
' <<< FIN NUEVA VALIDACIÓN >>> ' <<< FIN NUEVA VALIDACIÓN >>>
' <<< INICIO VALIDACIÓN DE PARÁMETROS CENTRALIZADA DENTRO DEL BATCH (V1) >>> ' <<< INICIO VALIDACIÓN DE PARÁMETROS CENTRALIZADA DENTRO DEL BATCH (V1) >>>
@@ -451,53 +501,67 @@ Private Sub ExecuteBatch(DB As String, con As SQL, in As InputStream, resp As Se
SendPlainTextError(resp, 400, validationResult.ErrorMessage) SendPlainTextError(resp, 400, validationResult.ErrorMessage)
Return "error" ' Salida temprana si la validación falla. Return "error" ' Salida temprana si la validación falla.
End If End If
' Log(validationResult.ParamsToExecute)
Dim affectedCount As Int = 1 ' Asumimos éxito (1) ya que la llamada directa es la única que ejecuta el SQL sin fallar en runtime.
con.ExecNonQuery2(sqlCommand, validationResult.ParamsToExecute) ' Ejecuta el comando con la lista de parámetros validada. con.ExecNonQuery2(sqlCommand, validationResult.ParamsToExecute) ' Ejecuta el comando con la lista de parámetros validada.
' <<< FIN VALIDACIÓN DE PARÁMETROS CENTRALIZADA DENTRO DEL BATCH (V1) >>> ' <<< FIN VALIDACIÓN DE PARÁMETROS CENTRALIZADA DENTRO DEL BATCH (V1) >>>
Next affectedCounts.Add(affectedCount) ' Añadimos el resultado (1) a la lista de respuesta V1
totalAffectedRows = totalAffectedRows + affectedCount ' Acumulamos el total para el log (aunque sea 1 simbólico)
Next
con.TransactionSuccessful ' Confirma la transacción. con.TransactionSuccessful ' Confirma la transacción.
' Log("Transaction succesfull")
Dim out As OutputStream = cs.WrapOutputStream(resp.OutputStream, "gzip") ' Comprime la salida antes de enviarla. Dim out As OutputStream = cs.WrapOutputStream(resp.OutputStream, "gzip") ' Comprime la salida antes de enviarla.
' Escribe la respuesta usando el serializador V1. ' Escribe la respuesta usando el serializador V1.
WriteObject(Main.VERSION, out) WriteObject(Main.VERSION, out)
WriteObject("batch", out) WriteObject("batch", out)
WriteInt(res.Length, out) WriteInt(res.Length, out)
For Each r As Int In res For Each r As Int In affectedCounts
WriteInt(r, out) WriteInt(r, out)
Next Next
out.Close out.Close
Catch Catch
con.Rollback con.Rollback
Log(LastException) Log(LastException)
Main.LogServerError("ERROR", "DBHandlerB4X.ExecuteBatch (V1)", LastException.Message, DB, "batch_execution_error_v1", Null) Main.LogServerError("ERROR", "DBHandlerB4X.ExecuteBatch (V1)", LastException.Message, DB, "batch_execution_error_v1", Null)
SendPlainTextError(resp, 500, LastException.Message) SendPlainTextError(resp, 500, LastException.Message)
End Try End Try
Return $"batch (size=${numberOfStatements})"$ ' 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 End Sub
' Ejecuta una consulta única usando el protocolo V1. ' Ejecuta una consulta única usando el protocolo V1.
Private Sub ExecuteQuery(DB As String, con As SQL, in As InputStream, resp As ServletResponse) As String Private Sub ExecuteQuery(DB As String, con As SQL, in As InputStream, resp As ServletResponse) As String
' Log("====================== ExecuteQuery =====================") ' Log("====================== ExecuteQuery =====================")
' Deserializa los datos de la petición usando el protocolo V1. ' Deserializa los datos de la petición usando el protocolo V1.
Dim clientVersion As Float = ReadObject(in) 'ignore Dim clientVersion As Float = ReadObject(in) 'ignore
Dim queryName As String = ReadObject(in) Dim queryName As String = ReadObject(in)
Dim limit As Int = ReadInt(in) Dim limit As Int = ReadInt(in)
Dim params As List = ReadList(in) Dim params As List = ReadList(in)
' Obtiene la sentencia SQL. ' Obtiene la sentencia SQL.
Dim theSql As String = Connector.GetCommand(DB, queryName) Dim theSql As String = Connector.GetCommand(DB, queryName)
' <<< INICIO NUEVA VALIDACIÓN: VERIFICAR SI EL COMANDO EXISTE (V1) >>> ' <<< INICIO NUEVA VALIDACIÓN: VERIFICAR SI EL COMANDO EXISTE (V1) >>>
If theSql = Null Or theSql ="null" Or theSql.Trim = "" Then 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}'."$ Dim errorMessage As String = $"El comando '${queryName}' no fue encontrado en el config.properties de '${DB}'."$
Log(errorMessage) Log(errorMessage)
Main.LogServerError("ERROR", "DBHandlerB4X.ExecuteQuery (V1)", errorMessage, DB, queryName, Null) Main.LogServerError("ERROR", "DBHandlerB4X.ExecuteQuery (V1)", errorMessage, DB, queryName, Null)
SendPlainTextError(resp, 400, errorMessage) SendPlainTextError(resp, 400, errorMessage)
Return "error" Return "error"
End If End If
' <<< FIN NUEVA VALIDACIÓN >>> ' <<< FIN NUEVA VALIDACIÓN >>>
' <<< INICIO VALIDACIÓN DE PARÁMETROS CENTRALIZADA (V1) >>> ' <<< INICIO VALIDACIÓN DE PARÁMETROS CENTRALIZADA (V1) >>>
@@ -512,177 +576,177 @@ Private Sub ExecuteQuery(DB As String, con As SQL, in As InputStream, resp As Se
Dim rs As ResultSet = con.ExecQuery2(theSql, validationResult.ParamsToExecute) Dim rs As ResultSet = con.ExecQuery2(theSql, validationResult.ParamsToExecute)
' <<< FIN VALIDACIÓN DE PARÁMETROS CENTRALIZADA (V1) >>> ' <<< FIN VALIDACIÓN DE PARÁMETROS CENTRALIZADA (V1) >>>
If limit <= 0 Then limit = 0x7fffffff 'max int If limit <= 0 Then limit = 0x7fffffff 'max int
Dim jrs As JavaObject = rs Dim jrs As JavaObject = rs
Dim rsmd As JavaObject = jrs.RunMethod("getMetaData", Null) Dim rsmd As JavaObject = jrs.RunMethod("getMetaData", Null)
Dim cols As Int = rs.ColumnCount Dim cols As Int = rs.ColumnCount
Dim out As OutputStream = cs.WrapOutputStream(resp.OutputStream, "gzip") ' Comprime el stream de salida. Dim out As OutputStream = cs.WrapOutputStream(resp.OutputStream, "gzip") ' Comprime el stream de salida.
' Escribe la cabecera de la respuesta V1. ' Escribe la cabecera de la respuesta V1.
WriteObject(Main.VERSION, out) WriteObject(Main.VERSION, out)
WriteObject("query", out) WriteObject("query", out)
WriteInt(rs.ColumnCount, out) WriteInt(rs.ColumnCount, out)
' Escribe los nombres de las columnas. ' Escribe los nombres de las columnas.
For i = 0 To cols - 1 For i = 0 To cols - 1
WriteObject(rs.GetColumnName(i), out) WriteObject(rs.GetColumnName(i), out)
Next Next
' Itera sobre las filas del resultado. ' Itera sobre las filas del resultado.
Do While rs.NextRow And limit > 0 Do While rs.NextRow And limit > 0
WriteByte(1, out) ' Escribe un byte '1' para indicar que viene una fila. WriteByte(1, out) ' Escribe un byte '1' para indicar que viene una fila.
' Itera sobre las columnas de la fila. ' Itera sobre las columnas de la fila.
For i = 0 To cols - 1 For i = 0 To cols - 1
Dim ct As Int = rsmd.RunMethod("getColumnType", Array(i + 1)) Dim ct As Int = rsmd.RunMethod("getColumnType", Array(i + 1))
' Maneja los tipos de datos binarios de forma especial. ' Maneja los tipos de datos binarios de forma especial.
If ct = -2 Or ct = 2004 Or ct = -3 Or ct = -4 Then If ct = -2 Or ct = 2004 Or ct = -3 Or ct = -4 Then
WriteObject(rs.GetBlob2(i), out) WriteObject(rs.GetBlob2(i), out)
Else Else
' Escribe el valor de la columna. ' Escribe el valor de la columna.
WriteObject(jrs.RunMethod("getObject", Array(i + 1)), out) WriteObject(jrs.RunMethod("getObject", Array(i + 1)), out)
End If End If
Next Next
limit = limit - 1 limit = limit - 1
Loop Loop
' Escribe un byte '0' para indicar el fin de las filas. ' Escribe un byte '0' para indicar el fin de las filas.
WriteByte(0, out) WriteByte(0, out)
out.Close out.Close
rs.Close rs.Close
Return "query: " & queryName Return "query: " & queryName
End Sub End Sub
' Escribe un único byte en el stream de salida. ' Escribe un único byte en el stream de salida.
Private Sub WriteByte(value As Byte, out As OutputStream) Private Sub WriteByte(value As Byte, out As OutputStream)
out.WriteBytes(Array As Byte(value), 0, 1) out.WriteBytes(Array As Byte(value), 0, 1)
End Sub End Sub
' Serializador principal para el protocolo V1. Escribe un objeto al stream. ' Serializador principal para el protocolo V1. Escribe un objeto al stream.
Private Sub WriteObject(o As Object, out As OutputStream) Private Sub WriteObject(o As Object, out As OutputStream)
Dim data() As Byte Dim data() As Byte
' Escribe un byte de tipo seguido de los datos. ' Escribe un byte de tipo seguido de los datos.
If o = Null Then If o = Null Then
out.WriteBytes(Array As Byte(T_NULL), 0, 1) out.WriteBytes(Array As Byte(T_NULL), 0, 1)
Else If o Is Short Then Else If o Is Short Then
out.WriteBytes(Array As Byte(T_SHORT), 0, 1) out.WriteBytes(Array As Byte(T_SHORT), 0, 1)
data = bc.ShortsToBytes(Array As Short(o)) data = bc.ShortsToBytes(Array As Short(o))
Else If o Is Int Then Else If o Is Int Then
out.WriteBytes(Array As Byte(T_INT), 0, 1) out.WriteBytes(Array As Byte(T_INT), 0, 1)
data = bc.IntsToBytes(Array As Int(o)) data = bc.IntsToBytes(Array As Int(o))
Else If o Is Float Then Else If o Is Float Then
out.WriteBytes(Array As Byte(T_FLOAT), 0, 1) out.WriteBytes(Array As Byte(T_FLOAT), 0, 1)
data = bc.FloatsToBytes(Array As Float(o)) data = bc.FloatsToBytes(Array As Float(o))
Else If o Is Double Then Else If o Is Double Then
out.WriteBytes(Array As Byte(T_DOUBLE), 0, 1) out.WriteBytes(Array As Byte(T_DOUBLE), 0, 1)
data = bc.DoublesToBytes(Array As Double(o)) data = bc.DoublesToBytes(Array As Double(o))
Else If o Is Long Then Else If o Is Long Then
out.WriteBytes(Array As Byte(T_LONG), 0, 1) out.WriteBytes(Array As Byte(T_LONG), 0, 1)
data = bc.LongsToBytes(Array As Long(o)) data = bc.LongsToBytes(Array As Long(o))
Else If o Is Boolean Then Else If o Is Boolean Then
out.WriteBytes(Array As Byte(T_BOOLEAN), 0, 1) out.WriteBytes(Array As Byte(T_BOOLEAN), 0, 1)
Dim b As Boolean = o Dim b As Boolean = o
Dim data(1) As Byte Dim data(1) As Byte
If b Then data(0) = 1 Else data(0) = 0 If b Then data(0) = 1 Else data(0) = 0
Else If GetType(o) = "[B" Then ' Si el objeto es un array de bytes (BLOB) Else If GetType(o) = "[B" Then ' Si el objeto es un array de bytes (BLOB)
data = o data = o
out.WriteBytes(Array As Byte(T_BLOB), 0, 1) out.WriteBytes(Array As Byte(T_BLOB), 0, 1)
' Escribe la longitud de los datos antes de los datos mismos. ' Escribe la longitud de los datos antes de los datos mismos.
WriteInt(data.Length, out) WriteInt(data.Length, out)
Else ' Trata todo lo demás como un String Else ' Trata todo lo demás como un String
out.WriteBytes(Array As Byte(T_STRING), 0, 1) out.WriteBytes(Array As Byte(T_STRING), 0, 1)
data = bc.StringToBytes(o, "UTF8") data = bc.StringToBytes(o, "UTF8")
' Escribe la longitud del string antes del string. ' Escribe la longitud del string antes del string.
WriteInt(data.Length, out) WriteInt(data.Length, out)
End If End If
' Escribe los bytes del dato. ' Escribe los bytes del dato.
If data.Length > 0 Then out.WriteBytes(data, 0, data.Length) If data.Length > 0 Then out.WriteBytes(data, 0, data.Length)
End Sub End Sub
' Deserializador principal para el protocolo V1. Lee un objeto del stream. ' Deserializador principal para el protocolo V1. Lee un objeto del stream.
Private Sub ReadObject(In As InputStream) As Object Private Sub ReadObject(In As InputStream) As Object
' Lee el primer byte para determinar el tipo de dato. ' Lee el primer byte para determinar el tipo de dato.
Dim data(1) As Byte Dim data(1) As Byte
In.ReadBytes(data, 0, 1) In.ReadBytes(data, 0, 1)
Select data(0) Select data(0)
Case T_NULL Case T_NULL
Return Null Return Null
Case T_SHORT Case T_SHORT
Dim data(2) As Byte Dim data(2) As Byte
Return bc.ShortsFromBytes(ReadBytesFully(In, data, data.Length))(0) Return bc.ShortsFromBytes(ReadBytesFully(In, data, data.Length))(0)
Case T_INT Case T_INT
Dim data(4) As Byte Dim data(4) As Byte
Return bc.IntsFromBytes(ReadBytesFully(In, data, data.Length))(0) Return bc.IntsFromBytes(ReadBytesFully(In, data, data.Length))(0)
Case T_LONG Case T_LONG
Dim data(8) As Byte Dim data(8) As Byte
Return bc.LongsFromBytes(ReadBytesFully(In, data, data.Length))(0) Return bc.LongsFromBytes(ReadBytesFully(In, data, data.Length))(0)
Case T_FLOAT Case T_FLOAT
Dim data(4) As Byte Dim data(4) As Byte
Return bc.FloatsFromBytes(ReadBytesFully(In, data, data.Length))(0) Return bc.FloatsFromBytes(ReadBytesFully(In, data, data.Length))(0)
Case T_DOUBLE Case T_DOUBLE
Dim data(8) As Byte Dim data(8) As Byte
Return bc.DoublesFromBytes(ReadBytesFully(In, data, data.Length))(0) Return bc.DoublesFromBytes(ReadBytesFully(In, data, data.Length))(0)
Case T_BOOLEAN Case T_BOOLEAN
Dim b As Byte = ReadByte(In) Dim b As Byte = ReadByte(In)
Return b = 1 Return b = 1
Case T_BLOB Case T_BLOB
' Lee la longitud, luego lee esa cantidad de bytes. ' Lee la longitud, luego lee esa cantidad de bytes.
Dim len As Int = ReadInt(In) Dim len As Int = ReadInt(In)
Dim data(len) As Byte Dim data(len) As Byte
Return ReadBytesFully(In, data, data.Length) Return ReadBytesFully(In, data, data.Length)
Case Else ' T_STRING Case Else ' T_STRING
' Lee la longitud, luego lee esa cantidad de bytes y los convierte a string. ' Lee la longitud, luego lee esa cantidad de bytes y los convierte a string.
Dim len As Int = ReadInt(In) Dim len As Int = ReadInt(In)
Dim data(len) As Byte Dim data(len) As Byte
ReadBytesFully(In, data, data.Length) ReadBytesFully(In, data, data.Length)
Return BytesToString(data, 0, data.Length, "UTF8") Return BytesToString(data, 0, data.Length, "UTF8")
End Select End Select
End Sub End Sub
' Se asegura de leer exactamente la cantidad de bytes solicitada del stream. ' Se asegura de leer exactamente la cantidad de bytes solicitada del stream.
Private Sub ReadBytesFully(In As InputStream, Data() As Byte, Len As Int) As Byte() Private Sub ReadBytesFully(In As InputStream, Data() As Byte, Len As Int) As Byte()
Dim count = 0, Read As Int Dim count = 0, Read As Int
' Sigue leyendo en un bucle hasta llenar el buffer, por si los datos llegan en partes. ' Sigue leyendo en un bucle hasta llenar el buffer, por si los datos llegan en partes.
Do While count < Len And Read > -1 Do While count < Len And Read > -1
Read = In.ReadBytes(Data, count, Len - count) Read = In.ReadBytes(Data, count, Len - count)
count = count + Read count = count + Read
Loop Loop
Return Data Return Data
End Sub End Sub
' Escribe un entero (4 bytes) en el stream. ' Escribe un entero (4 bytes) en el stream.
Private Sub WriteInt(i As Int, out As OutputStream) Private Sub WriteInt(i As Int, out As OutputStream)
Dim data() As Byte Dim data() As Byte
data = bc.IntsToBytes(Array As Int(i)) data = bc.IntsToBytes(Array As Int(i))
out.WriteBytes(data, 0, data.Length) out.WriteBytes(data, 0, data.Length)
End Sub End Sub
' Lee un entero (4 bytes) del stream. ' Lee un entero (4 bytes) del stream.
Private Sub ReadInt(In As InputStream) As Int Private Sub ReadInt(In As InputStream) As Int
Dim data(4) As Byte Dim data(4) As Byte
Return bc.IntsFromBytes(ReadBytesFully(In, data, data.Length))(0) Return bc.IntsFromBytes(ReadBytesFully(In, data, data.Length))(0)
End Sub End Sub
' Lee un solo byte del stream. ' Lee un solo byte del stream.
Private Sub ReadByte(In As InputStream) As Byte Private Sub ReadByte(In As InputStream) As Byte
Dim data(1) As Byte Dim data(1) As Byte
In.ReadBytes(data, 0, 1) In.ReadBytes(data, 0, 1)
Return data(0) Return data(0)
End Sub End Sub
' Lee una lista de objetos del stream (protocolo V1). ' Lee una lista de objetos del stream (protocolo V1).
Private Sub ReadList(in As InputStream) As List Private Sub ReadList(in As InputStream) As List
' Primero lee la cantidad de elementos en la lista. ' Primero lee la cantidad de elementos en la lista.
Dim len As Int = ReadInt(in) Dim len As Int = ReadInt(in)
Dim l1 As List Dim l1 As List
l1.Initialize l1.Initialize
' Luego lee cada objeto uno por uno y lo añade a la lista. ' Luego lee cada objeto uno por uno y lo añade a la lista.
For i = 0 To len - 1 For i = 0 To len - 1
l1.Add(ReadObject(in)) l1.Add(ReadObject(in))
Next Next
Return l1 Return l1
End Sub End Sub
'#end If ' Fin del bloque de compilación condicional para VERSION1 '#end If ' Fin del bloque de compilación condicional para VERSION1

View File

@@ -44,6 +44,7 @@ Sub Handle(req As ServletRequest, resp As ServletResponse)
Dim poolBusyConnectionsForLog As Int = 0 ' Contiene el número de conexiones ocupadas del pool. 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 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 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 --- Try ' --- INICIO: Bloque Try que envuelve la lógica principal del Handler ---
@@ -127,9 +128,29 @@ Sub Handle(req As ServletRequest, resp As ServletResponse)
If poolStats.ContainsKey("BusyConnections") Then If poolStats.ContainsKey("BusyConnections") Then
' <<<< ¡CORRECCIÓN CLAVE: Aseguramos que el valor sea Int! >>>> ' <<<< ¡CORRECCIÓN CLAVE: Aseguramos que el valor sea Int! >>>>
poolBusyConnectionsForLog = poolStats.Get("BusyConnections").As(Int) ' Capturamos el valor. poolBusyConnectionsForLog = poolStats.Get("BusyConnections").As(Int) ' Capturamos el valor.
' Log($">>>>>>>>>> ${poolStats.Get("BusyConnections")} "$)
End If End If
End If End If
' <<<< ¡FIN DE CAPTURA! >>>> ' <<<< ¡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. ' Obtiene la sentencia SQL correspondiente al nombre del comando desde config.properties.
Dim sqlCommand As String = Connector.GetCommand(finalDbKey, queryNameForLog) Dim sqlCommand As String = Connector.GetCommand(finalDbKey, queryNameForLog)
@@ -191,9 +212,9 @@ Sub Handle(req As ServletRequest, resp As ServletResponse)
CleanupAndLog(finalDbKey, queryNameForLog, duration, req.RemoteAddress, requestsBeforeDecrement, poolBusyConnectionsForLog, con) CleanupAndLog(finalDbKey, queryNameForLog, duration, req.RemoteAddress, requestsBeforeDecrement, poolBusyConnectionsForLog, con)
Return ' Salida temprana. Return ' Salida temprana.
End If 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. con.ExecNonQuery2(sqlCommand, validationResult.ParamsToExecute) ' Ejecuta un comando con la lista de parámetros validada.
SendSuccessResponse(resp, CreateMap("message": "Command executed successfully")) ' Envía confirmación de éxito. SendSuccessResponse(resp, CreateMap("affectedRows": affectedCount, "message": "Command executed successfully")) ' Envía confirmación de éxito.
' --- FIN VALIDACIÓN DE PARÁMETROS CENTRALIZADA --- ' --- FIN VALIDACIÓN DE PARÁMETROS CENTRALIZADA ---
Else Else
Dim ErrorMsg As String = "Parámetro 'exec' inválido. '" & execType & "' no es un valor permitido." Dim ErrorMsg As String = "Parámetro 'exec' inválido. '" & execType & "' no es un valor permitido."
@@ -201,7 +222,6 @@ Sub Handle(req As ServletRequest, resp As ServletResponse)
Main.LogServerError("ERROR", "DBHandlerJSON.Handle", ErrorMsg, finalDbKey, queryNameForLog, req.RemoteAddress) ' <-- Nuevo Log 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. ' El flujo continúa hasta la limpieza final si no hay un Return explícito.
End If End If
Catch ' --- CATCH: Maneja errores generales de ejecución o de SQL/JSON --- Catch ' --- CATCH: Maneja errores generales de ejecución o de SQL/JSON ---
Log(LastException) ' Registra la excepción completa en el log. Log(LastException) ' Registra la excepción completa en el log.
Main.LogServerError("ERROR", "DBHandlerJSON.Handle", LastException.Message, finalDbKey, queryNameForLog, req.RemoteAddress) ' <-- Nuevo Log Main.LogServerError("ERROR", "DBHandlerJSON.Handle", LastException.Message, finalDbKey, queryNameForLog, req.RemoteAddress) ' <-- Nuevo Log
@@ -215,7 +235,6 @@ Sub Handle(req As ServletRequest, resp As ServletResponse)
duration = DateTime.Now - start ' Calcula la duración total de la petición. 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. ' Llama a la subrutina centralizada para registrar el rendimiento y limpiar recursos.
CleanupAndLog(finalDbKey, queryNameForLog, duration, req.RemoteAddress, requestsBeforeDecrement, poolBusyConnectionsForLog, con) CleanupAndLog(finalDbKey, queryNameForLog, duration, req.RemoteAddress, requestsBeforeDecrement, poolBusyConnectionsForLog, con)
End Sub End Sub
' --- NUEVA SUBRUTINA: Centraliza el logging de rendimiento y la limpieza de recursos --- ' --- NUEVA SUBRUTINA: Centraliza el logging de rendimiento y la limpieza de recursos ---

View File

@@ -11,13 +11,18 @@ DriverClass=oracle.jdbc.driver.OracleDriver
#GOHAN ---> server #GOHAN ---> server
#JdbcUrl=jdbc:oracle:thin:@//10.0.0.205:1521/DBKMT #JdbcUrl=jdbc:oracle:thin:@//10.0.0.205:1521/DBKMT
#JdbcUrl=jdbc:oracle:thin:@//10.0.0.236:1521/DBKMT #JdbcUrl=jdbc:oracle:thin:@//10.0.0.236:1521/DBKMT
JdbcUrl=jdbc:oracle:thin:@//192.168.101.13:1521/DBKMT?v$session.program=jRDC_MultiSALMA JdbcUrl=jdbc:oracle:thin:@//192.168.101.13:1521/DBKMT?v$session.program=jRDC_Pruebas_Salma2
# Configuración del pool de conexiones # Define el número de conexiones que se intentarán crear al iniciar el pool.
InitialPoolSize=3 InitialPoolSize=1
MinPoolSize=2 # Fija el número mínimo de conexiones que el pool mantendrá abiertas.
MaxPoolSize=15 MinPoolSize=1
AcquireIncrement=2 # Define el número máximo de conexiones simultáneas.
MaxPoolSize=2
# Cuántas conexiones nuevas se añaden en lote si el pool se queda sin disponibles.
AcquireIncrement=1
# Tiempo máximo de inactividad de una conexión antes de cerrarse (segundos).
MaxConnectionAge=60
# Configuración de tolerancia de parámetros: # Configuración de tolerancia de parámetros:
# 1 = Habilita la tolerancia a parámetros de más (se recortarán los excesivos). # 1 = Habilita la tolerancia a parámetros de más (se recortarán los excesivos).
@@ -25,6 +30,12 @@ AcquireIncrement=2
# Por defecto, si no se especifica o el valor es diferente de 1, la tolerancia estará DESHABILITADA (modo estricto). # Por defecto, si no se especifica o el valor es diferente de 1, la tolerancia estará DESHABILITADA (modo estricto).
parameterTolerance=1 parameterTolerance=1
# Configuración de los logs de SQLite:
# 1 = Habilita el registro de logs de queries y errores en la base de datos SQLite (users.db).
# 0 = Deshabilita el registro de logs de queries y errores en SQLite para optimizar el rendimiento.
# Por defecto, si no se especifica o el valor es diferente de 1, los logs estarán DESHABILITADOS.
enableSQLiteLogs=1
# SVR-KEYMON-PRODUCCION--> Usuario # SVR-KEYMON-PRODUCCION--> Usuario
User=SALMA User=SALMA
Password=SALMAD2016M Password=SALMAD2016M

View File

@@ -11,13 +11,30 @@ DriverClass=oracle.jdbc.driver.OracleDriver
#GOHAN ---> server #GOHAN ---> server
#JdbcUrl=jdbc:oracle:thin:@//10.0.0.205:1521/DBKMT #JdbcUrl=jdbc:oracle:thin:@//10.0.0.205:1521/DBKMT
#JdbcUrl=jdbc:oracle:thin:@//10.0.0.236:1521/DBKMT #JdbcUrl=jdbc:oracle:thin:@//10.0.0.236:1521/DBKMT
JdbcUrl=jdbc:oracle:thin:@//192.168.101.13:1521/DBKMT?v$session.program=jRDC_MultiSALMA JdbcUrl=jdbc:oracle:thin:@//192.168.101.13:1521/DBKMT?v$session.program=jRDC_Pruebas_Salma3
# Configuración del pool de conexiones # Define el número de conexiones que se intentarán crear al iniciar el pool.
InitialPoolSize=3 InitialPoolSize=1
MinPoolSize=2 # Fija el número mínimo de conexiones que el pool mantendrá abiertas.
MaxPoolSize=15 MinPoolSize=1
AcquireIncrement=2 # Define el número máximo de conexiones simultáneas.
MaxPoolSize=2
# Cuántas conexiones nuevas se añaden en lote si el pool se queda sin disponibles.
AcquireIncrement=1
# Tiempo máximo de inactividad de una conexión antes de cerrarse (segundos).
MaxConnectionAge=60
# Configuración de tolerancia de parámetros:
# 1 = Habilita la tolerancia a parámetros de más (se recortarán los excesivos).
# 0 = Deshabilita la tolerancia (el servidor será estricto y lanzará un error si hay parámetros de más).
# Por defecto, si no se especifica o el valor es diferente de 1, la tolerancia estará DESHABILITADA (modo estricto).
parameterTolerance=1
# Configuración de los logs de SQLite:
# 1 = Habilita el registro de logs de queries y errores en la base de datos SQLite (users.db).
# 0 = Deshabilita el registro de logs de queries y errores en SQLite para optimizar el rendimiento.
# Por defecto, si no se especifica o el valor es diferente de 1, los logs estarán DESHABILITADOS.
enableSQLiteLogs=0
# SVR-KEYMON-PRODUCCION--> Usuario # SVR-KEYMON-PRODUCCION--> Usuario
User=SALMA User=SALMA

View File

@@ -11,13 +11,30 @@ DriverClass=oracle.jdbc.driver.OracleDriver
#GOHAN ---> server #GOHAN ---> server
#JdbcUrl=jdbc:oracle:thin:@//10.0.0.205:1521/DBKMT #JdbcUrl=jdbc:oracle:thin:@//10.0.0.205:1521/DBKMT
#JdbcUrl=jdbc:oracle:thin:@//10.0.0.236:1521/DBKMT #JdbcUrl=jdbc:oracle:thin:@//10.0.0.236:1521/DBKMT
JdbcUrl=jdbc:oracle:thin:@//192.168.101.13:1521/DBKMT?v$session.program=jRDC_MultiSALMA JdbcUrl=jdbc:oracle:thin:@//192.168.101.13:1521/DBKMT?v$session.program=jRDC_Pruebas_Salma4
# Configuración del pool de conexiones # Define el número de conexiones que se intentarán crear al iniciar el pool.
InitialPoolSize=3 InitialPoolSize=1
MinPoolSize=2 # Fija el número mínimo de conexiones que el pool mantendrá abiertas.
MaxPoolSize=15 MinPoolSize=1
AcquireIncrement=2 # Define el número máximo de conexiones simultáneas.
MaxPoolSize=2
# Cuántas conexiones nuevas se añaden en lote si el pool se queda sin disponibles.
AcquireIncrement=1
# Tiempo máximo de inactividad de una conexión antes de cerrarse (segundos).
MaxConnectionAge=60
# Configuración de tolerancia de parámetros:
# 1 = Habilita la tolerancia a parámetros de más (se recortarán los excesivos).
# 0 = Deshabilita la tolerancia (el servidor será estricto y lanzará un error si hay parámetros de más).
# Por defecto, si no se especifica o el valor es diferente de 1, la tolerancia estará DESHABILITADA (modo estricto).
parameterTolerance=1
# Configuración de los logs de SQLite:
# 1 = Habilita el registro de logs de queries y errores en la base de datos SQLite (users.db).
# 0 = Deshabilita el registro de logs de queries y errores en SQLite para optimizar el rendimiento.
# Por defecto, si no se especifica o el valor es diferente de 1, los logs estarán DESHABILITADOS.
enableSQLiteLogs=0
# SVR-KEYMON-PRODUCCION--> Usuario # SVR-KEYMON-PRODUCCION--> Usuario
User=SALMA User=SALMA

View File

@@ -11,20 +11,31 @@ DriverClass=oracle.jdbc.driver.OracleDriver
#GOHAN ---> server #GOHAN ---> server
#JdbcUrl=jdbc:oracle:thin:@//10.0.0.205:1521/DBKMT #JdbcUrl=jdbc:oracle:thin:@//10.0.0.205:1521/DBKMT
#JdbcUrl=jdbc:oracle:thin:@//10.0.0.236:1521/DBKMT #JdbcUrl=jdbc:oracle:thin:@//10.0.0.236:1521/DBKMT
JdbcUrl=jdbc:oracle:thin:@//192.168.101.10:1521/DBKMT?v$session.program=jRDC_Multi JdbcUrl=jdbc:oracle:thin:@//192.168.101.10:1521/DBKMT?v$session.program=jRDC_Pruebas_Guna1
# Configuración del pool de conexiones # Define el número de conexiones que se intentarán crear al iniciar el pool.
InitialPoolSize=3 InitialPoolSize=1
MinPoolSize=2 # Fija el número mínimo de conexiones que el pool mantendrá abiertas.
MaxPoolSize=15 MinPoolSize=1
AcquireIncrement=2 # Define el número máximo de conexiones simultáneas.
MaxPoolSize=2
# Cuántas conexiones nuevas se añaden en lote si el pool se queda sin disponibles.
AcquireIncrement=1
# Tiempo máximo de inactividad de una conexión antes de cerrarse (segundos).
MaxConnectionAge=60
# Configuración de tolerancia de parámetros: # Configuración de tolerancia de parámetros:
# 1 = Habilita la tolerancia a parámetros de más (se recortarán los excesivos). # 1 = Habilita la tolerancia a parámetros de más (se ignoran los extras).
# 0 = Deshabilita la tolerancia (el servidor será estricto y lanzará un error si hay parámetros de más). # 0 = Deshabilita la tolerancia (el servidor será estricto y lanzará un error si hay parámetros de más).
# Por defecto, si no se especifica o el valor es diferente de 1, la tolerancia estará DESHABILITADA (modo estricto). # Por defecto, si no se especifica o el valor es diferente de 1, la tolerancia estará DESHABILITADA (modo estricto).
parameterTolerance=1 parameterTolerance=1
# Configuración de los logs de SQLite:
# 1 = Habilita el registro de logs de queries y errores en la base de datos SQLite (users.db).
# 0 = Deshabilita el registro de logs de queries y errores en SQLite para optimizar el rendimiento.
# Por defecto, si no se especifica o el valor es diferente de 1, los logs estarán DESHABILITADOS.
enableSQLiteLogs=0
# SVR-KEYMON-PRODUCCION--> Usuario # SVR-KEYMON-PRODUCCION--> Usuario
User=GUNA User=GUNA
Password=GUNAD2015M Password=GUNAD2015M

View File

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

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

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

View File

@@ -2,7 +2,7 @@
Group=Default Group Group=Default Group
ModulesStructureVersion=1 ModulesStructureVersion=1
Type=Class Type=Class
Version=8.8 Version=10.3
@EndOfDesignText@ @EndOfDesignText@
' Módulo de clase: Manager ' Módulo de clase: Manager
' Este handler proporciona un panel de administración web para el servidor jRDC2-Multi. ' Este handler proporciona un panel de administración web para el servidor jRDC2-Multi.
@@ -24,398 +24,452 @@ End Sub
' Método principal que maneja las peticiones HTTP para el panel de administración. ' 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. ' 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. ' 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) Sub Handle(req As ServletRequest, resp As ServletResponse)
' --- 1. Bloque de Seguridad: Autenticación de Usuario ---
' Verifica si el usuario actual ha iniciado sesión y está autorizado. ' --- 1. Bloque de Seguridad ---
' Si no está autorizado, se le redirige a la página de login.
If req.GetSession.GetAttribute2("user_is_authorized", False) = False Then If req.GetSession.GetAttribute2("user_is_authorized", False) = False Then
resp.SendRedirect("/login") resp.SendRedirect("/login")
Return ' Termina la ejecución si no está autorizado. Return
End If End If
' Obtiene el comando solicitado de los parámetros de la URL (ej. "?command=reload").
Dim Command As String = req.GetParameter("command") Dim Command As String = req.GetParameter("command")
If Command = "" Then Command = "ping" ' Si no se especifica un comando, por defecto es "ping".
Log($"Command: ${Command}"$) ' --- 2. Servidor de la Página Principal ---
If Command = "" Then
' --- MANEJO ESPECIAL PARA SNAPSHOT --- Try
' El comando "snapshot" no devuelve HTML, sino una imagen. Lo manejamos por separado al principio. resp.ContentType = "text/html; charset=utf-8"
If Command = "snapshot" Then resp.Write(File.ReadString(File.DirApp, "www/manager.html"))
' Try Catch
' resp.ContentType = "image/png" resp.SendError(500, "Error: No se pudo encontrar el archivo principal del panel (www/manager.html). " & LastException.Message)
' Dim robot, toolkit As JavaObject End Try
' robot.InitializeNewInstance("java.awt.Robot", Null) Return
' toolkit.InitializeStatic("java.awt.Toolkit")
' Dim screenRect As JavaObject
' screenRect.InitializeNewInstance("java.awt.Rectangle", Array As Object( _
' toolkit.RunMethodJO("getDefaultToolkit", Null).RunMethod("getScreenSize", Null)))
' Dim image As JavaObject = robot.RunMethod("createScreenCapture", Array As Object(screenRect))
' Dim ImageIO As JavaObject
' ImageIO.InitializeStatic("javax.imageio.ImageIO").RunMethod("write", Array As Object(image, "png", resp.OutputStream))
' Catch
' resp.SendError(500, LastException.Message)
' End Try
' Return ' Detenemos la ejecución aquí para no enviar más HTML.
End If End If
' --- FIN DE MANEJO ESPECIAL ---
' Para todos los demás comandos, construimos la página HTML de respuesta. ' --- 3. Manejo de Comandos como API ---
resp.ContentType = "text/html" ' Establece el tipo de contenido como HTML. Select Command.ToLowerCase
Dim sb As StringBuilder ' Usamos StringBuilder para construir eficientemente el HTML.
sb.Initialize
' --- Estilos y JavaScript (igual que antes) ---
sb.Append("<html><head><style>")
sb.Append("body {font-family: sans-serif; margin: 2em; background-color: #f9f9f9;} ")
sb.Append("h1, h2 {color: #333;} hr {margin: 2em 0; border: 0; border-top: 1px solid #ddd;} ")
sb.Append("input {display: block; width: 95%; padding: 8px; margin-bottom: 10px; border: 1px solid #ccc; border-radius: 4px;} ")
sb.Append("button {padding: 10px 15px; border: none; background-color: #007bff; color: white; cursor: pointer; border-radius: 4px; margin-right: 1em;} ")
sb.Append(".nav a, .logout a {text-decoration: none; margin-right: 10px; color: #007bff;} ")
sb.Append(".output {background: #fff; padding: 1em; border: 1px solid #eee; border-radius: 8px; font-family: monospace; white-space: pre-wrap; word-wrap: break-word;} ")
sb.Append("#changePassForm {background: #f0f0f0; padding: 1.5em; border-radius: 8px; max-width: 400px; margin-top: 1em;} ")
sb.Append("</style>")
sb.Append("<script>function toggleForm() {var form = document.getElementById('changePassForm'); if (form.style.display === 'none') {form.style.display = 'block';} else {form.style.display = 'none';}}</script>")
sb.Append("</head><body>")
' --- Cabecera de la Página y Mensaje de Bienvenida ---
sb.Append("<h1>Panel de Administración jRDC</h1>")
sb.Append($"<p>Bienvenido, <strong>${req.GetSession.GetAttribute("username")}</strong></p>"$)
' --- Menú de Navegación del Manager ---
' Este menú incluye las opciones para interactuar con el servidor.
sb.Append("<div class='menu'>")
sb.Append("<a href='/manager?command=test'>Test</a> | ")
sb.Append("<a href='/manager?command=ping'>Ping</a> | ")
sb.Append("<a href='/manager?command=reload'>Reload</a> | ")
sb.Append("<a href='/manager?command=slowqueries'>Queries Lentas</a> | ") ' Nuevo enlace para queries lentas.
sb.Append("<a href='/manager?command=totalcon'>Estadísticas Pool</a> | ") ' Nuevo enlace para estadísticas del pool.
sb.Append("<a href='/manager?command=rpm2'>Reiniciar (pm2)</a> | ")
sb.Append("<a href='/manager?command=reviveBow'>Revive Bow</a>")
sb.Append("</div>")
sb.Append("<hr>")
sb.Append("<div id='changePassForm' style='display:none;'>")
sb.Append("<h2>Cambiar Contraseña</h2><form action='/changepass' method='post'>")
sb.Append("Contraseña Actual: <input type='password' name='current_password' required><br>")
sb.Append("Nueva Contraseña: <input type='password' name='new_password' required><br>")
sb.Append("Confirmar Nueva Contraseña: <input type='password' name='confirm_password' required><br>")
sb.Append("<button type='submit'>Actualizar Contraseña</button> <button onclick='toggleForm()'>Cancelar</button></form></div>")
' --- Resultado del Comando ---
sb.Append("<h2>Resultado del Comando: '" & Command & "'</h2>")
sb.Append("<div class='output'>")
' =========================================================================
' ### INICIO DE TU LÓGICA DE COMANDOS INTEGRADA ###
' =========================================================================
If Command = "reload" Then
Dim sbTemp As StringBuilder
sbTemp.Initialize
sbTemp.Append($"Iniciando recarga de configuración (Hot-Swap) ($DateTime{DateTime.Now})"$).Append(" " & CRLF)
' <<< PASO CLAVE 1: DETENER TIMER DE LOGS (ZONA SEGURA DE SQLite) >>>
' Detenemos el timer incondicionalmente al inicio para evitar conflictos de bloqueo con SQLite
' durante la inicialización de conectores.
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
' 1. Crear un nuevo mapa temporal para almacenar los conectores recién inicializados.
Dim newConnectors As Map
newConnectors.Initialize
Dim oldConnectors As Map
Dim reloadSuccessful As Boolean = True
' *** INICIO DEL BLOQUE CRÍTICO 1: Obtener oldConnectors con ReentrantLock ***
Dim lock1Acquired As Boolean = False
Try
Main.MainConnectorsLock.RunMethod("lock", Null)
lock1Acquired = True
oldConnectors = Main.Connectors
Catch
sbTemp.Append($" -> ERROR CRÍTICO: No se pudo adquirir el bloqueo para leer conectores antiguos: ${LastException.Message}"$).Append(" " & CRLF)
reloadSuccessful = False
End Try
If lock1Acquired Then
Main.MainConnectorsLock.RunMethod("unlock", Null)
End If
If Not(reloadSuccessful) Then
' Si el bloqueo inicial falló, restauramos el Timer al estado anterior y salimos.
If oldTimerState Then Main.timerLogs.Enabled = True
sb.Append(sbTemp.ToString)
sb.Append($"¡ERROR: La recarga de configuración falló en la fase de bloqueo inicial! Los conectores antiguos siguen activos."$).Append(" " & CRLF)
Return
End If
' 2. Iterar sobre las bases de datos configuradas y crear *nuevas* instancias de RDCConnector.
For Each dbKey As String In Main.listaDeCP
Try
Dim newRDC As RDCConnector
newRDC.Initialize(dbKey) ' Inicializa la nueva instancia con la configuración fresca.
' <<< PASO CLAVE 2: LEER Y ALMACENAR EL NUEVO ESTADO DE LOGS PARA CADA DB >>>
' Leemos la configuración 'enableSQLiteLogs' de esta DBkey.
Dim enableLogsSetting As Int = newRDC.config.GetDefault("enableSQLiteLogs", 0).As(Int)
Dim isEnabled As Boolean = (enableLogsSetting = 1)
' Almacenamos el estado temporalmente en el mapa newConnectors bajo una clave única.
newConnectors.Put(dbKey & "_LOG_STATE", isEnabled)
sbTemp.Append($" -> Logs de ${dbKey} activados: ${isEnabled}"$).Append(" " & CRLF)
' <<< FIN PASO CLAVE 2 >>>
newConnectors.Put(dbKey, newRDC)
Dim newPoolStats As Map = newRDC.GetPoolStats
sbTemp.Append($" -> ${dbKey}: Nuevo conector inicializado. Conexiones: ${newPoolStats.Get("TotalConnections")}"$).Append(" " & CRLF)
Catch
sbTemp.Append($" -> ERROR CRÍTICO al inicializar nuevo conector para ${dbKey}: ${LastException.Message}"$).Append(" " & CRLF)
reloadSuccessful = False
Exit ' Si uno falla, abortamos la recarga.
End Try
Next
sb.Append(sbTemp.ToString)
If reloadSuccessful Then
' 3. Si todo fue exitoso, hacemos el Hot-Swap atómico. ' --- Comandos que devuelven JSON (Métricas del Pool) ---
' *** INICIO DEL BLOQUE CRÍTICO 2: Reemplazar Main.Connectors con ReentrantLock *** Case "getstatsold"
resp.ContentType = "application/json; charset=utf-8"
Dim lock2Acquired As Boolean = False
Try
Main.MainConnectorsLock.RunMethod("lock", Null)
lock2Acquired = True
Main.Connectors = newConnectors ' Reemplazamos el mapa de conectores completo por el nuevo.
Catch
sb.Append($" -> ERROR CRÍTICO: No se pudo adquirir el bloqueo para reemplazar conectores: ${LastException.Message}"$).Append(" " & CRLF)
reloadSuccessful = False
End Try
If lock2Acquired Then
Main.MainConnectorsLock.RunMethod("unlock", Null)
End If
If reloadSuccessful Then ' Si el swap fue exitoso
' <<< PASO CLAVE 3: APLICAR EL NUEVO ESTADO GLOBAL GRANULAR Y REINICIAR TIMER >>>
' 3a. Reemplazar el mapa de estados de logging granular
Main.SQLiteLoggingStatusByDB.Clear ' Limpiamos el mapa global
Dim isAnyEnabled As Boolean = False
For Each dbKey As String In Main.listaDeCP
' Recuperamos el estado logueado temporalmente.
Dim isEnabled As Boolean = newConnectors.Get(dbKey & "_LOG_STATE").As(Boolean)
Main.SQLiteLoggingStatusByDB.Put(dbKey, isEnabled) ' Aplicamos el estado al mapa global
If isEnabled Then isAnyEnabled = True ' Calculamos el flag general
Next
' 3b. Controlar el Timer y el flag global
Main.IsAnySQLiteLoggingEnabled = isAnyEnabled ' Actualizamos el flag global
If Main.IsAnySQLiteLoggingEnabled Then
Main.timerLogs.Enabled = True
sb.Append($" -> Logs de SQLite HABILITADOS (Granular). Timer de limpieza ACTIVADO."$).Append(" " & CRLF)
Else
Main.timerLogs.Enabled = False
sb.Append($" -> Logs de SQLite DESHABILITADOS (Total). Timer de limpieza PERMANECERÁ DETENIDO."$).Append(" " & CRLF)
End If
' <<< FIN PASO CLAVE 3 >>>
sb.Append($"¡Recarga de configuración completada con éxito (Hot-Swap)!"$).Append(" " & CRLF)
' ... (Resto del código: Mostrar estado de pools y Cierre explícito de oldConnectors) ...
Else
sb.Append($"¡ERROR: La recarga de configuración falló en la fase de reemplazo de conectores! Los conectores antiguos siguen activos."$).Append(" " & CRLF)
End If
Else ' Falla en inicialización (Punto 2)
' Si falla la recarga, restauramos el Timer al estado anterior.
If oldTimerState Then
Main.timerLogs.Enabled = True
sb.Append(" -> Restaurando Timer de limpieza de logs (SQLite) al estado ACTIVO debido a fallo en recarga.").Append(" " & CRLF)
End If
sb.Append($"¡ERROR: La recarga de configuración falló durante la inicialización de nuevos conectores! Los conectores antiguos siguen activos."$).Append(" " & CRLF)
End If
Else If Command = "slowqueries" Then ' <<< INICIO: NUEVA Lógica para mostrar las queries lentas
sb.Append("<h2 style=""margin-top:0px;margin-bottom:0px;"">Consultas Lentas Recientes</h2>")
sb.Append("(Este registro depende de que los logs estén habilitados con del parámetro ""enableSQLiteLogs=1"" en config properties)<br><br>")
Try
' 1. Calcular el límite de tiempo: el tiempo actual (en milisegundos) menos 1 hora (3,600,000 ms).
Dim oneHourAgoMs As Long = DateTime.Now - 3600000
' Ajusta la consulta SQL para obtener las 20 queries más lentas.
' Utilizamos datetime con 'unixepoch' y 'localtime' para una visualización legible del timestamp.
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"$)
sb.Append("<table border='1' style='width:100%; text-align:left; border-collapse: collapse;'>")
sb.Append("<thead><tr><th>Query</th><th>Duración (ms)</th><th>Fecha/Hora Local</th><th>DB Key</th><th>Cliente IP</th><th>Conex. Ocupadas</th><th>Peticiones Activas</th></tr></thead>")
sb.Append("<tbody>")
Do While rs.NextRow
sb.Append("<tr>")
sb.Append($"<td>${rs.GetString("query_name")}</td>"$)
sb.Append($"<td>${rs.GetLong("duration_ms")}</td>"$)
sb.Append($"<td>${rs.GetString("timestamp_local")}</td>"$)
sb.Append($"<td>${rs.GetString("db_key")}</td>"$)
sb.Append($"<td>${rs.GetString("client_ip")}</td>"$)
sb.Append($"<td>${rs.GetInt("busy_connections")}</td>"$)
sb.Append($"<td>${rs.GetInt("handler_active_requests")}</td>"$)
sb.Append("</tr>")
Loop
sb.Append("</tbody>")
sb.Append("</table>")
rs.Close
Catch
Log("Error al obtener queries lentas en Manager: " & LastException.Message)
sb.Append($"<p style='color:red;'>Error al cargar queries lentas: ${LastException.Message}</p>"$)
End Try
Else If Command = "test" Then
Try
Dim con As SQL = Main.Connectors.Get("DB1").As(RDCConnector).GetConnection("")
sb.Append("Connection successful.</br></br>")
Private 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)
sb.Append($"Using config${estaDB}.properties<br/>"$)
Next
con.Close
Catch
resp.Write("Error fetching connection.")
End Try
Else If Command = "stop" Then
' Public shl As Shell...
Else If Command = "rsx" Then
Log($"Ejecutamos ${File.DirApp}\start.bat"$)
sb.Append($"Ejecutamos ${File.DirApp}\start.bat"$)
Public shl As Shell
shl.Initialize("shl","cmd",Array("/c",File.DirApp & "\start.bat " & Main.srvr.Port))
shl.WorkingDirectory = File.DirApp
shl.Run(-1)
Else If Command = "rpm2" Then
Log($"Ejecutamos ${File.DirApp}\reiniciaProcesoPM2.bat"$)
sb.Append($"Ejecutamos ${File.DirApp}\reiniciaProcesoPM2.bat"$)
Public shl As Shell
shl.Initialize("shl","cmd",Array("/c",File.DirApp & "\reiniciaProcesoPM2.bat " & Main.srvr.Port))
shl.WorkingDirectory = File.DirApp
shl.Run(-1)
Else If Command = "reviveBow" Then
Log($"Ejecutamos ${File.DirApp}\reiniciaProcesoBow.bat"$)
sb.Append($"Ejecutamos ${File.DirApp}\reiniciaProcesoBow.bat<br><br>"$)
sb.Append($"!!!BOW REINICIANDO!!!"$)
Public shl As Shell
shl.Initialize("shl","cmd",Array("/c",File.DirApp & "\reiniciaProcesoBow.bat " & Main.srvr.Port))
shl.WorkingDirectory = File.DirApp
shl.Run(-1)
Else If Command = "paused" Then
GlobalParameters.IsPaused = 1
sb.Append("Servidor pausado.")
Else If Command = "continue" Then
GlobalParameters.IsPaused = 0
sb.Append("Servidor reanudado.")
Else If Command = "logs" Then
If GlobalParameters.mpLogs.IsInitialized Then
j.Initialize(GlobalParameters.mpLogs)
sb.Append(j.ToString)
End If
Else If Command = "block" Then
Dim BlockedConIP As String = req.GetParameter("IP")
If GlobalParameters.mpBlockConnection.IsInitialized Then
GlobalParameters.mpBlockConnection.Put(BlockedConIP, BlockedConIP)
sb.Append("IP bloqueada: " & BlockedConIP)
End If
Else If Command = "unblock" Then
Dim UnBlockedConIP As String = req.GetParameter("IP")
If GlobalParameters.mpBlockConnection.IsInitialized Then
GlobalParameters.mpBlockConnection.Remove(UnBlockedConIP)
sb.Append("IP desbloqueada: " & UnBlockedConIP)
End If
Else If Command = "restartserver" Then
Log($"Ejecutamos ${File.DirApp}/restarServer.bat"$)
sb.Append("Reiniciando servidor...")
Else If Command = "runatstartup" Then
File.Copy("C:\jrdcinterface", "startup.bat", "C:\ProgramData\Microsoft\Windows\Start Menu\Programs\StartUp", "startup.bat")
sb.Append("Script de inicio añadido.")
Else If Command = "stoprunatstartup" Then
File.Delete("C:\ProgramData\Microsoft\Windows\Start Menu\Programs\StartUp", "startup.bat")
sb.Append("Script de inicio eliminado.")
Else If Command = "totalrequests" Then
If GlobalParameters.mpTotalRequests.IsInitialized Then
j.Initialize(GlobalParameters.mpTotalRequests)
sb.Append(j.ToString)
End If
Else If Command = "totalblocked" Then
If GlobalParameters.mpBlockConnection.IsInitialized Then
' j.Initialize(Global.mpBlockConnection)
sb.Append(j.ToString)
End If
Else If Command = "ping" Then
sb.Append($"Pong ($DateTime{DateTime.Now})"$)
Else If Command = "totalcon" Then ' <<< Modificado: Ahora usa GetPoolStats para cada pool
' Verificamos que el mapa global de conexiones esté inicializado.
' Aunque no lo poblamos directamente, es un buen chequeo de estado.
If GlobalParameters.mpTotalConnections.IsInitialized Then
sb.Append("<h2>Estadísticas del Pool de Conexiones por DB:</h2>")
' Creamos un mapa LOCAL para almacenar las estadísticas de TODOS los pools de conexiones.
Dim allPoolStats As Map Dim allPoolStats As Map
allPoolStats.Initialize allPoolStats.Initialize
' Iteramos sobre cada clave de base de datos que tenemos configurada (DB1, DB2, etc.).
For Each dbKey As String In Main.listaDeCP For Each dbKey As String In Main.listaDeCP
' Obtenemos el conector RDC para la base de datos actual. Dim connector As RDCConnector = Main.Connectors.Get(dbKey)
Dim connector As RDCConnector = Main.Connectors.Get(dbKey).As(RDCConnector) If connector.IsInitialized Then
allPoolStats.Put(dbKey, connector.GetPoolStats)
' Si el conector no está inicializado (lo cual no debería ocurrir si Main.AppStart funcionó), Else
' registramos un error y pasamos al siguiente. allPoolStats.Put(dbKey, CreateMap("Error": "Conector no inicializado"))
If connector.IsInitialized = False Then
Log($"Manager: ADVERTENCIA: El conector para ${dbKey} no está inicializado."$)
Dim errorMap As Map = CreateMap("Error": "Conector no inicializado o no cargado correctamente")
allPoolStats.Put(dbKey, errorMap)
Continue ' Salta a la siguiente iteración del bucle.
End If End If
' Llamamos al método GetPoolStats del conector para obtener las métricas de su pool.
Dim poolStats As Map = connector.GetPoolStats
' Añadimos las estadísticas de este pool (poolStats) al mapa general (allPoolStats),
' usando la clave de la base de datos (dbKey) como su identificador.
allPoolStats.Put(dbKey, poolStats)
Next Next
' Inicializamos el generador JSON con el mapa 'allPoolStats' (que ahora sí debería contener datos).
' (La variable 'j' ya está declarada en Class_Globals de Manager.bas, no la declares de nuevo aquí).
j.Initialize(allPoolStats) j.Initialize(allPoolStats)
resp.Write(j.ToString)
' Añadimos la representación JSON de las estadísticas al StringBuilder para la respuesta HTML. Return
sb.Append(j.ToString)
Else Case "getstats"
sb.Append("El mapa de conexiones GlobalParameters.mpTotalConnections no está inicializado.") resp.ContentType = "application/json; charset=utf-8"
End If Dim allPoolStats As Map
End If
' ========================================================================= ' Leemos del caché global actualizado por el Timer SSE
' ### FIN DE TU LÓGICA DE COMANDOS ### allPoolStats = Main.LatestPoolStats
' =========================================================================
For Each dbKey As String In Main.listaDeCP
' --- Cerramos la página y la enviamos --- If allPoolStats.ContainsKey(dbKey) = False Then
sb.Append("</div><p class='logout'><a href='/logout'>Cerrar Sesión</a> | <a href=# onclick='toggleForm()'>Cambiar Contraseña</a></p></body></html>") allPoolStats.Put(dbKey, CreateMap("Error": "Métricas no disponibles/Pool no inicializado"))
resp.Write(sb.ToString) 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
If GlobalParameters.mpLogs.IsInitialized Then GlobalParameters.mpLogs.Put(Command, "Manager : " & Command & " - Time : " & DateTime.Time(DateTime.Now)) ' 4. Actualizar el estado de logs (Granular)
End Sub 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
' sbInfo.Append($"--- CONFIGURACIÓN ACTUAL DEL SERVIDOR jRDC2-Multi ($DateTime{DateTime.Now}) ---"$).Append(CRLF).Append(CRLF)
Dim allKeys As List
allKeys.Initialize
allKeys.AddAll(Main.listaDeCP) ' DB1, DB2, ...
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 ###").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)
sbInfo.Append("InitialPoolSize: Conexiones que el pool establece al iniciar (c3p0).").Append(CRLF)
sbInfo.Append("MinPoolSize: Mínimo de conexiones inactivas que se mantendrán.").Append(CRLF)
sbInfo.Append("MaxPoolSize: Máximo de conexiones simultáneas permitido.").Append(CRLF)
sbInfo.Append("AcquireIncrement: Número de conexiones nuevas que se adquieren en lote al necesitar más.").Append(CRLF)
sbInfo.Append("MaxIdleTime: Tiempo máximo (segundos) de inactividad antes de cerrar una conexión.").Append(CRLF)
sbInfo.Append("MaxConnectionAge: Tiempo máximo de vida (segundos) de una conexión.").Append(CRLF)
sbInfo.Append("CheckoutTimeout: Tiempo máximo de espera (milisegundos) por una conexión disponible.").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)
sbInfo.Append("--------------------------------------------------").Append(CRLF).Append(CRLF)
If connector.IsInitialized Then
Dim configMap As Map = connector.config
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 (C3P0) ---").Append(CRLF)
sbInfo.Append($"InitialPoolSize: ${configMap.GetDefault("InitialPoolSize", 3)}"$).Append(CRLF)
sbInfo.Append($"MinPoolSize: ${configMap.GetDefault("MinPoolSize", 2)}"$).Append(CRLF)
sbInfo.Append($"MaxPoolSize: ${configMap.GetDefault("MaxPoolSize", 5)}"$).Append(CRLF)
sbInfo.Append($"AcquireIncrement: ${configMap.GetDefault("AcquireIncrement", 5)}"$).Append(CRLF)
sbInfo.Append($"MaxIdleTime (s): ${configMap.GetDefault("MaxIdleTime", 300)}"$).Append(CRLF)
sbInfo.Append($"MaxConnectionAge (s): ${configMap.GetDefault("MaxConnectionAge", 900)}"$).Append(CRLF)
sbInfo.Append($"CheckoutTimeout (ms): ${configMap.GetDefault("CheckoutTimeout", 60000)}"$).Append(CRLF).Append(CRLF)
sbInfo.Append("--- COMPORTAMIENTO ---").Append(CRLF)
sbInfo.Append($"Debug (Recarga Queries - DESHABILITADO): ${configMap.GetDefault("Debug", "false")}"$).Append(CRLF)
' Lectura explícita de las nuevas propiedades, asegurando un Int.
Dim tolerance As Int = configMap.GetDefault("parameterTolerance", 0).As(Int)
sbInfo.Append($"ParameterTolerance: ${tolerance} (0=Estricto, 1=Habilitado)"$).Append(CRLF)
Dim logsEnabled As Int = configMap.GetDefault("enableSQLiteLogs", 1).As(Int)
sbInfo.Append($"EnableSQLiteLogs: ${logsEnabled} (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 Else
resp.ContentType = "text/plain; charset=utf-8"
resp.SendError(404, $"Comando desconocido: '{Command}'"$)
Return
End Select
End Sub

View File

@@ -50,6 +50,7 @@ Public Sub ValidateAndAdjustParameters (CommandName As String, DBKey As String,
res.Success = True 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."$ 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(WarningMsg)
Log("Cache: " & Main.LOG_CACHE_THRESHOLD & "|" & Main.ErrorLogCache.Size)
Main.LogServerError("ADVERTENCIA", "ParameterValidationUtils.ValidateAndAdjustParameters", WarningMsg, DBKey, CommandName, Null) ' <-- Nuevo Log [6] Main.LogServerError("ADVERTENCIA", "ParameterValidationUtils.ValidateAndAdjustParameters", WarningMsg, DBKey, CommandName, Null) ' <-- Nuevo Log [6]
Else Else
' Si la tolerancia NO está habilitada, esto es un error crítico. ' Si la tolerancia NO está habilitada, esto es un error crítico.

View File

@@ -78,15 +78,18 @@ Public Sub Initialize(DB As String)
Dim minPoolSize As Int = config.GetDefault("MinPoolSize", 2) Dim minPoolSize As Int = config.GetDefault("MinPoolSize", 2)
Dim maxPoolSize As Int = config.GetDefault("MaxPoolSize", 5) Dim maxPoolSize As Int = config.GetDefault("MaxPoolSize", 5)
Dim acquireIncrement As Int = config.GetDefault("AcquireIncrement", 5) Dim acquireIncrement As Int = config.GetDefault("AcquireIncrement", 5)
Dim maxIdleTime As Int = config.GetDefault("MaxIdleTime", 300)
Dim maxConnectionAge As Int = config.GetDefault("MaxConnectionAge", 900)
Dim checkoutTimeout As Int = config.GetDefault("CheckoutTimeout", 60000)
' Configuración de los parámetros del pool de conexiones C3P0: ' Configuración de los parámetros del pool de conexiones C3P0:
jo.RunMethod("setInitialPoolSize", Array(initialPoolSize)) ' Define el número de conexiones que se intentarán crear al iniciar el pool. jo.RunMethod("setInitialPoolSize", Array(initialPoolSize)) ' Define el número de conexiones que se intentarán crear al iniciar el pool.
jo.RunMethod("setMinPoolSize", Array(minPoolSize)) ' Fija el número mínimo de conexiones que el pool mantendrá abiertas. jo.RunMethod("setMinPoolSize", Array(minPoolSize)) ' Fija el número mínimo de conexiones que el pool mantendrá abiertas.
jo.RunMethod("setMaxPoolSize", Array(maxPoolSize)) ' Define el número máximo de conexiones simultáneas. jo.RunMethod("setMaxPoolSize", Array(maxPoolSize)) ' Define el número máximo de conexiones simultáneas.
jo.RunMethod("setAcquireIncrement", Array(acquireIncrement)) ' Cuántas conexiones nuevas se añaden en lote si el pool se queda sin disponibles. jo.RunMethod("setAcquireIncrement", Array(acquireIncrement)) ' Cuántas conexiones nuevas se añaden en lote si el pool se queda sin disponibles.
jo.RunMethod("setMaxIdleTime", Array As Object(config.GetDefault("MaxIdleTime", 300))) ' Tiempo máximo de inactividad de una conexión antes de cerrarse (segundos). jo.RunMethod("setMaxIdleTime", Array As Object(maxIdleTime)) ' Es el tiempo máximo (en segundos) que una conexión puede permanecer inactiva en el pool antes de ser cerrada para ahorrar recursos.
jo.RunMethod("setMaxConnectionAge", Array As Object(config.GetDefault("MaxConnectionAge", 900))) ' Tiempo máximo de vida de una conexión (segundos). jo.RunMethod("setMaxConnectionAge", Array As Object(maxConnectionAge)) ' Tiempo máximo de vida de una conexión (segundos), previene conexiones viciadas.
jo.RunMethod("setCheckoutTimeout", Array As Object(config.GetDefault("CheckoutTimeout", 60000))) ' Tiempo máximo de espera por una conexión del pool (milisegundos). jo.RunMethod("setCheckoutTimeout", Array As Object(checkoutTimeout)) ' Tiempo máximo de espera por una conexión del pool (milisegundos).
' LÍNEAS CRÍTICAS PARA FORZAR UN COMPORTAMIENTO NO SILENCIOSO DE C3P0: ' LÍNEAS CRÍTICAS PARA FORZAR UN COMPORTAMIENTO NO SILENCIOSO DE C3P0:
' Por defecto, C3P0 puede reintentar muchas veces y no lanzar una excepción si las conexiones iniciales fallan. ' Por defecto, C3P0 puede reintentar muchas veces y no lanzar una excepción si las conexiones iniciales fallan.
@@ -103,6 +106,16 @@ Public Sub Initialize(DB As String)
tempCon.Close ' Devolvemos la conexión inmediatamente al pool para que esté disponible. tempCon.Close ' Devolvemos la conexión inmediatamente al pool para que esté disponible.
End If End If
' Cargar configuración estática en el cache global
Dim dbKeyToStore As String = DB
If dbKeyToStore = "" Then dbKeyToStore = "DB1" ' Aseguramos la llave si era DB1
Dim initialPoolStats As Map = GetPoolStats ' Llama a la función que usa JavaObject
' PASO C: Almacenamos el mapa completo (estático + dinámico inicial) en el cache global.
Main.LatestPoolStats.Put(dbKeyToStore, initialPoolStats)
' Log(Main.LatestPoolStats)
' com.mchange.v2.c3p0.ComboPooledDataSource [ ' com.mchange.v2.c3p0.ComboPooledDataSource [
' acquireIncrement -> 3, ' acquireIncrement -> 3,
' acquireRetryAttempts -> 30, ' acquireRetryAttempts -> 30,

187
SSE.bas Normal file
View File

@@ -0,0 +1,187 @@
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
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.
Broadcast("stats", "stats_update", jsonStats, 0)
Catch
' Captura y registra cualquier error que ocurra durante la recolección de datos.
Log($"[SSE] Error CRÍTICO durante la adquisición de estadísticas: ${LastException.Message}"$)
End Try
End Sub

131
SSEHandler.bas Normal file
View File

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

View File

@@ -1,17 +1,15 @@
AppType=StandardJava AppType=StandardJava
Build1=Default,b4j.JRDCMulti Build1=Default,b4j.JRDCMulti
File1=config.DB2.properties File1=config.DB2.properties
File10=stop.bat
File2=config.DB3.properties File2=config.DB3.properties
File3=config.DB4.properties File3=config.DB4.properties
File4=config.properties File4=config.properties
File5=login.html File5=reiniciaProcesoBow.bat
File6=reiniciaProcesoBow.bat File6=reiniciaProcesoPM2.bat
File7=reiniciaProcesoPM2.bat File7=start.bat
File8=start.bat File8=start2.bat
File9=start2.bat File9=stop.bat
FileGroup1=Default Group FileGroup1=Default Group
FileGroup10=Default Group
FileGroup2=Default Group FileGroup2=Default Group
FileGroup3=Default Group FileGroup3=Default Group
FileGroup4=Default Group FileGroup4=Default Group
@@ -21,21 +19,24 @@ FileGroup7=Default Group
FileGroup8=Default Group FileGroup8=Default Group
FileGroup9=Default Group FileGroup9=Default Group
Group=Default Group Group=Default Group
Library1=byteconverter Library1=bcrypt
Library2=javaobject Library2=byteconverter
Library3=jcore Library3=javaobject
Library4=jrandomaccessfile Library4=jcore
Library5=jserver Library5=jrandomaccessfile
Library6=jshell Library6=jserver
Library7=json Library7=jshell
Library8=jsql Library8=json
Library9=bcrypt Library9=jsql
Module1=Cambios Module1=Cambios
Module10=Manager Module10=Manager
Module11=ParameterValidationUtils Module11=Manager0
Module12=ping Module12=ParameterValidationUtils
Module13=RDCConnector Module13=ping
Module14=TestHandler Module14=RDCConnector
Module15=SSE
Module16=SSEHandler
Module17=TestHandler
Module2=ChangePassHandler Module2=ChangePassHandler
Module3=DBHandlerB4X Module3=DBHandlerB4X
Module4=DBHandlerJSON Module4=DBHandlerJSON
@@ -44,9 +45,9 @@ Module6=faviconHandler
Module7=GlobalParameters Module7=GlobalParameters
Module8=LoginHandler Module8=LoginHandler
Module9=LogoutHandler Module9=LogoutHandler
NumberOfFiles=10 NumberOfFiles=9
NumberOfLibraries=9 NumberOfLibraries=9
NumberOfModules=14 NumberOfModules=17
Version=10.3 Version=10.3
@EndOfDesignText@ @EndOfDesignText@
'Non-UI application (console / server application) 'Non-UI application (console / server application)
@@ -55,7 +56,7 @@ Version=10.3
#CommandLineArgs: #CommandLineArgs:
#MergeLibraries: True #MergeLibraries: True
' VERSION 5.09.16 ' VERSION 5.09.19
'########################################################################################################### '###########################################################################################################
'###################### PULL ############################################################# '###################### PULL #############################################################
'Ctrl + click ide://run?file=%WINDIR%\System32\cmd.exe&Args=/c&Args=git&Args=pull 'Ctrl + click ide://run?file=%WINDIR%\System32\cmd.exe&Args=/c&Args=git&Args=pull
@@ -112,6 +113,7 @@ Sub Process_Globals
' NUEVAS VARIABLES para control granular de logs ' NUEVAS VARIABLES para control granular de logs
' Mapa para almacenar el estado de logging (True/False) por cada DBKey (DB1, DB2, etc.). ' Mapa para almacenar el estado de logging (True/False) por cada DBKey (DB1, DB2, etc.).
Public SQLiteLoggingStatusByDB As Map Public SQLiteLoggingStatusByDB As Map
' Bandera global que indica si AL MENOS una base de datos tiene los logs habilitados. ' Bandera global que indica si AL MENOS una base de datos tiene los logs habilitados.
Public IsAnySQLiteLoggingEnabled As Boolean Public IsAnySQLiteLoggingEnabled As Boolean
@@ -121,12 +123,47 @@ Sub Process_Globals
ErrorMessage As String, _ ErrorMessage As String, _
ParamsToExecute As List _ ' La lista de parámetros final a usar en la ejecución SQL ParamsToExecute As List _ ' La lista de parámetros final a usar en la ejecución SQL
) )
Public QueryLogCache As List ' Cache para los logs de rendimiento (query_logs)
Public ErrorLogCache As List ' Cache para los logs de errores y advertencias
Public LOG_CACHE_THRESHOLD As Int = 350 ' Umbral de registros para forzar la escritura
Dim logger As Boolean
Public LatestPoolStats As Map ' Mapa Thread-Safe para almacenar las últimas métricas de cada pool.
End Sub End Sub
Sub AppStart (Args() As String) Sub AppStart (Args() As String)
SSE.Initialize
#if DEBUG
logger = True
LOG_CACHE_THRESHOLD = 10
#else
logger = False
#End If
' --- Subrutina principal que se ejecuta al iniciar la aplicación --- ' --- Subrutina principal que se ejecuta al iniciar la aplicación ---
' La subcarpeta es "www"
CopiarRecursoSiNoExiste("manager.html", "www")
CopiarRecursoSiNoExiste("login.html", "www")
' --- Copiar los archivos .bat de la raíz ---
' La subcarpeta es "" (vacía) porque están en la raíz de "Files"
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", "")
'
' Log("Verificación de archivos completada.")
bc.Initialize("BC") bc.Initialize("BC")
QueryLogCache.Initialize
ErrorLogCache.Initialize
' 1. Inicializa la base de datos local de usuarios (SQLite) y la tabla de logs. ' 1. Inicializa la base de datos local de usuarios (SQLite) y la tabla de logs.
InitializeSQLiteDatabase InitializeSQLiteDatabase
@@ -145,6 +182,7 @@ Sub AppStart (Args() As String)
srvr.Initialize("") srvr.Initialize("")
Connectors = srvr.CreateThreadSafeMap Connectors = srvr.CreateThreadSafeMap
commandsMap.Initialize commandsMap.Initialize
LatestPoolStats = srvr.CreateThreadSafeMap ' Inicializar el mapa de estadísticas como Thread-Safe
' NUEVO: Inicializar el mapa de estado de logs granular ' NUEVO: Inicializar el mapa de estado de logs granular
SQLiteLoggingStatusByDB.Initialize SQLiteLoggingStatusByDB.Initialize
@@ -164,7 +202,7 @@ Sub AppStart (Args() As String)
Log($"Main.AppStart: Conector 'DB1' inicializado exitosamente en puerto: ${srvr.Port}"$) Log($"Main.AppStart: Conector 'DB1' inicializado exitosamente en puerto: ${srvr.Port}"$)
' Lógica de Logs para DB1 (Fuente principal de configuración) ' Lógica de Logs para DB1 (Fuente principal de configuración)
Dim enableLogsSetting As Int = con1.config.GetDefault("enableSQLiteLogs", 1).As(Int) Dim enableLogsSetting As Int = con1.config.GetDefault("enableSQLiteLogs", 0).As(Int)
Dim isEnabled As Boolean = (enableLogsSetting = 1) Dim isEnabled As Boolean = (enableLogsSetting = 1)
SQLiteLoggingStatusByDB.Put("DB1", isEnabled) ' Guardar el estado SQLiteLoggingStatusByDB.Put("DB1", isEnabled) ' Guardar el estado
@@ -276,10 +314,10 @@ Sub AppStart (Args() As String)
If IsAnySQLiteLoggingEnabled Then If IsAnySQLiteLoggingEnabled Then
timerLogs.Enabled = True timerLogs.Enabled = True
Log("Main.AppStart: Timer de limpieza de logs ACTIVADO (al menos una DB requiere logs).") If logger Then Log("Main.AppStart: Timer de limpieza de logs ACTIVADO (al menos una DB requiere logs).")
Else Else
timerLogs.Enabled = False timerLogs.Enabled = False
Log("Main.AppStart: Timer de limpieza de logs DESHABILITADO (ninguna DB requiere logs).") If logger Then Log("Main.AppStart: Timer de limpieza de logs DESHABILITADO (ninguna DB requiere logs).")
End If End If
' <<<< Fin del bloque del Timer >>>> ' <<<< Fin del bloque del Timer >>>>
@@ -295,6 +333,7 @@ Sub AppStart (Args() As String)
srvr.AddHandler("/DBJ", "DBHandlerJSON", False) srvr.AddHandler("/DBJ", "DBHandlerJSON", False)
srvr.AddHandler("/dbrquery", "DBHandlerJSON", False) srvr.AddHandler("/dbrquery", "DBHandlerJSON", False)
srvr.AddHandler("/favicon.ico", "faviconHandler", False) srvr.AddHandler("/favicon.ico", "faviconHandler", False)
srvr.AddHandler("/stats-stream", "SSEHandler", False)
srvr.AddHandler("/*", "DBHandlerB4X", False) srvr.AddHandler("/*", "DBHandlerB4X", False)
' 7. Inicia el servidor HTTP. ' 7. Inicia el servidor HTTP.
@@ -315,16 +354,18 @@ Sub InitializeSQLiteDatabase
If File.Exists(File.DirApp, dbFileName) = False Then If File.Exists(File.DirApp, dbFileName) = False Then
Log("Creando nueva base de datos de usuarios: " & dbFileName) Log("Creando nueva base de datos de usuarios: " & dbFileName)
SQL1.InitializeSQLite(File.DirApp, dbFileName, True) SQL1.InitializeSQLite(File.DirApp, dbFileName, True)
' Crear tabla 'users' ' Crear tabla 'users'
Dim createUserTable As String = "CREATE TABLE users (username TEXT PRIMARY KEY, password_hash TEXT NOT NULL)" Dim createUserTable As String = "CREATE TABLE users (username TEXT PRIMARY KEY, password_hash TEXT NOT NULL)"
SQL1.ExecNonQuery(createUserTable) SQL1.ExecNonQuery(createUserTable)
' Crear tabla 'query_logs' ' Crear tabla 'query_logs'
Log("Creando tabla 'query_logs' con columnas de rendimiento.") If logger Then Log("Creando tabla 'query_logs' con columnas de rendimiento.")
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)" 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) SQL1.ExecNonQuery(createQueryLogsTable)
SQL1.ExecNonQuery("PRAGMA journal_mode=WAL;")
SQL1.ExecNonQuery("PRAGMA synchronous=NORMAL;")
' Insertar usuario por defecto ' Insertar usuario por defecto
Dim defaultUser As String = "admin" Dim defaultUser As String = "admin"
Dim defaultPass As String = "12345" Dim defaultPass As String = "12345"
@@ -336,17 +377,30 @@ Sub InitializeSQLiteDatabase
Log("Creando tabla 'errores' para registrar eventos.") Log("Creando tabla 'errores' para registrar eventos.")
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)" 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) SQL1.ExecNonQuery(createErrorsTable)
If logger Then Log("Creando índices de rendimiento en tablas de logs.")
' Índice en timestamp para limpieza rápida (DELETE/ORDER BY) en query_logs
SQL1.ExecNonQuery("CREATE INDEX idx_query_timestamp ON query_logs(timestamp)")
' Índice en duration_ms para la consulta 'slowqueries' (ORDER BY)
SQL1.ExecNonQuery("CREATE INDEX idx_query_duration ON query_logs(duration_ms)")
' Índice en timestamp para limpieza rápida de la tabla de errores
SQL1.ExecNonQuery("CREATE INDEX idx_error_timestamp ON errores(timestamp)")
Else Else
SQL1.InitializeSQLite(File.DirApp, dbFileName, True) SQL1.InitializeSQLite(File.DirApp, dbFileName, True)
Log("Base de datos de usuarios cargada.") Log("Base de datos de usuarios cargada.")
SQL1.ExecNonQuery("PRAGMA journal_mode=WAL;")
SQL1.ExecNonQuery("PRAGMA synchronous=NORMAL;")
' >>> INICIO: Lógica de migración (ALTER TABLE) si la DB ya existía <<< ' >>> INICIO: Lógica de migración (ALTER TABLE) si la DB ya existía <<<
Log("Verificando y migrando tabla 'query_logs' si es necesario.") If logger Then Log("Verificando y migrando tabla 'query_logs' si es necesario.")
If SQL1.ExecQuerySingleResult("SELECT name FROM sqlite_master WHERE type='table' AND name='query_logs'") = Null Then If SQL1.ExecQuerySingleResult("SELECT name FROM sqlite_master WHERE type='table' AND name='query_logs'") = Null Then
Log("Tabla 'query_logs' no encontrada, creándola con columnas de rendimiento.") If logger Then Log("Tabla 'query_logs' no encontrada, creándola con columnas de rendimiento.")
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)" 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)"
@@ -371,7 +425,7 @@ Sub InitializeSQLiteDatabase
rs.Close rs.Close
If columnExists = False Then If columnExists = False Then
Log("Añadiendo columna 'busy_connections' a query_logs.") If logger Then Log("Añadiendo columna 'busy_connections' a query_logs.")
SQL1.ExecNonQuery("ALTER TABLE query_logs ADD COLUMN busy_connections INTEGER DEFAULT 0") SQL1.ExecNonQuery("ALTER TABLE query_logs ADD COLUMN busy_connections INTEGER DEFAULT 0")
End If End If
@@ -388,20 +442,34 @@ Sub InitializeSQLiteDatabase
rs.Close rs.Close
If columnExists = False Then If columnExists = False Then
Log("Añadiendo columna 'handler_active_requests' a query_logs.") If logger Then Log("Añadiendo columna 'handler_active_requests' a query_logs.")
SQL1.ExecNonQuery("ALTER TABLE query_logs ADD COLUMN handler_active_requests INTEGER DEFAULT 0") SQL1.ExecNonQuery("ALTER TABLE query_logs ADD COLUMN handler_active_requests INTEGER DEFAULT 0")
End If End If
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 ' La columna ya existe, salimos del bucle.
End If
Loop
rs.Close
If columnExists = False Then
If logger Then Log("Añadiendo columna 'timestamp_text_local' a query_logs.")
' Usamos 'TEXT' para almacenar la cadena de fecha/hora formateada.
SQL1.ExecNonQuery("ALTER TABLE query_logs ADD COLUMN timestamp_text_local TEXT")
End If
' >>> INICIO: Lógica de migración para 'errores' si la DB ya existía <<< ' >>> INICIO: Lógica de migración para 'errores' si la DB ya existía <<<
Log("Verificando y migrando tabla 'errores' si es necesario.") If logger Then Log("Verificando y migrando tabla 'errores' si es necesario.")
If SQL1.ExecQuerySingleResult("SELECT name FROM sqlite_master WHERE type='table' AND name='errores'") = Null Then If SQL1.ExecQuerySingleResult("SELECT name FROM sqlite_master WHERE type='table' AND name='errores'") = Null Then
Log("Tabla 'errores' no encontrada, creándola.") If logger Then Log("Tabla 'errores' no encontrada, creándola.")
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)" 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) SQL1.ExecNonQuery(createErrorsTable)
Else Else
Log("Tabla 'errores' ya existe.") If logger Then Log("Tabla 'errores' ya existe.")
End If End If
' >>> FIN: Lógica de migración para 'errores' <<< ' >>> FIN: Lógica de migración para 'errores' <<<
@@ -411,45 +479,155 @@ Sub InitializeSQLiteDatabase
End If End If
End Sub End Sub
' --- Subrutina para registrar las métricas de rendimiento de las queries en la tabla 'query_logs'. ---
' ¡MODIFICADA PARA USAR FILTRADO GRANULAR POR DBKEY!
Public Sub LogQueryPerformance(QueryName As String, DurationMs As Long, DbKey As String, ClientIp As String, HandlerActiveRequests As Int, PoolBusyConnections As Int) Public Sub LogQueryPerformance(QueryName As String, DurationMs As Long, DbKey As String, ClientIp As String, HandlerActiveRequests As Int, PoolBusyConnections As Int)
' Obtener el estado de logging para esta DBKey. Usar False si la DBKey no existe en el mapa.
Dim isEnabled As Boolean = SQLiteLoggingStatusByDB.GetDefault(DbKey, False) Dim isEnabled As Boolean = SQLiteLoggingStatusByDB.GetDefault(DbKey, False)
If isEnabled Then If isEnabled Then
Try
SQL1.ExecNonQuery2("INSERT INTO query_logs (query_name, duration_ms, timestamp, db_key, client_ip, busy_connections, handler_active_requests) VALUES (?, ?, ?, ?, ?, ?, ?)", _ ' Formato de tiempo necesario para la columna timestamp_text_local
Array As Object(QueryName, DurationMs, DateTime.Now, DbKey, ClientIp, PoolBusyConnections, HandlerActiveRequests)) DateTime.DateFormat = "yyyy-MM-dd HH:mm:ss.SSS"
Catch Dim formattedTimestamp As String = DateTime.Date(DateTime.Now)
Log("Error al guardar log de query en SQLite (Main.LogQueryPerformance): " & LastException.Message)
End Try ' 1. Crear el mapa de datos (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. Zona Crítica: Añadir a la caché y verificar el umbral
Dim shouldWriteBatch As Boolean = False
' Usamos el lock global para garantizar que la adición y la verificación del tamaño sean atómicas.
MainConnectorsLock.RunMethod("lock", Null)
QueryLogCache.Add(logEntry)
If QueryLogCache.Size >= LOG_CACHE_THRESHOLD Then
shouldWriteBatch = True
End If
MainConnectorsLock.RunMethod("unlock", Null)
' 3. Si se alcanzó el umbral, disparamos la escritura.
' NO DEBE HACERSE CON EL LOCK PUESTO.
If shouldWriteBatch Then
CallSub(Me, "WriteQueryLogsBatch")
End If
End If End If
End Sub End Sub
' --- Subrutina para registrar errores y advertencias en la tabla 'errores'. --- ' --- Subrutina para registrar errores y advertencias en la tabla 'errores'. ---
' ¡MODIFICADA PARA USAR FILTRADO GRANULAR POR DBKEY!
Public Sub LogServerError(Type0 As String, Source As String, Message As String, DBKey As String, CommandName As String, ClientIp As String) Public Sub LogServerError(Type0 As String, Source As String, Message As String, DBKey As String, CommandName As String, ClientIp As String)
' Obtener el estado de logging para esta DBKey. Usar False si la DBKey es Null o no está en el mapa.
Dim isEnabled As Boolean = SQLiteLoggingStatusByDB.GetDefault(DBKey, False) Dim isEnabled As Boolean = SQLiteLoggingStatusByDB.GetDefault(DBKey, False)
If isEnabled Then If isEnabled Then
Try
SQL1.ExecNonQuery2("INSERT INTO errores (timestamp, type, source, message, db_key, command_name, client_ip) VALUES (?, ?, ?, ?, ?, ?, ?)", _ ' Log($"[DEBUG CACHE] Se recibió log de error/advertencia para: ${CommandName}"$) '<--- Nuevo Log 1
Array As Object(DateTime.Now, Type0, Source, Message, DBKey, CommandName, ClientIp))
Catch Dim logEntry As Map = CreateMap("timestamp": DateTime.Now, "type": Type0, "source": Source, "message": Message, _
Log("ERROR CRÍTICO: Fallo al guardar el log de error/advertencia en SQLite (Main.LogServerError): " & LastException.Message) "db_key": DBKey, "command_name": CommandName, "client_ip": ClientIp)
End Try
Dim shouldWriteBatch As Boolean = False
' 1. Zona Crítica: Añadir a la caché y verificar el umbral
' Usamos el lock para Thread Safety
MainConnectorsLock.RunMethod("lock", Null)
' Log($"[DEBUG CACHE] Lock adquirido. Tamaño actual de ErrorLogCache: ${ErrorLogCache.Size}"$) '<--- Nuevo Log 2
ErrorLogCache.Add(logEntry)
' Log($"[DEBUG CACHE] Log añadido. Nuevo tamaño: ${ErrorLogCache.Size}. Umbral: ${LOG_CACHE_THRESHOLD}"$) '<--- Nuevo Log 3
If ErrorLogCache.Size >= LOG_CACHE_THRESHOLD Then
shouldWriteBatch = True
' Log(">>> [DEBUG CACHE] UMBRAL ALCANZADO. DISPARANDO ESCRITURA BATCH. <<<") '<--- Nuevo Log 4
End If
MainConnectorsLock.RunMethod("unlock", Null)
' Log($"[DEBUG CACHE] Lock liberado."$) '<--- Nuevo Log 5
' 2. Si se alcanzó el umbral (o si el timer lo llama), disparamos la escritura.
If shouldWriteBatch Then
CallSub(Me, "WriteErrorLogsBatch")
End If
Else
' Log($"[DEBUG CACHE] Logging deshabilitado para DBKey: ${DBKey}. Log de error omitido."$)
End If End If
End Sub End Sub
Public Sub WriteQueryLogsBatch
Dim logsToWrite As List
logsToWrite.Initialize ' 1. Inicializar la lista local (CRÍTICO)
' === PASO 1: Intercambio Atómico de Caché (Protegido por ReentrantLock) ===
MainConnectorsLock.RunMethod("lock", Null)
If QueryLogCache.Size = 0 Then
MainConnectorsLock.RunMethod("unlock", Null)
' Log("[DEBUG BATCH-Q] Saliendo: Caché de rendimiento vacía.")
Return
End If
' *** CORRECCIÓN CRÍTICA: Copia de contenido (AddAll) en lugar de referencia. ***
logsToWrite.AddAll(QueryLogCache)
Dim batchSize As Int = logsToWrite.Size
' Vaciamos la caché global. logsToWrite ahora contiene la copia de los elementos.
QueryLogCache.Initialize
MainConnectorsLock.RunMethod("unlock", Null)
' If logger Then Log($"[LOG BATCH] Iniciando escritura transaccional de ${batchSize} logs de rendimiento. Logs copiados: ${logsToWrite.Size}"$)
' === PASO 2: Escritura Transaccional a SQLite ===
Try
' 1. Iniciar la transacción: Todo lo que siga es una única operación de disco.
SQL1.BeginTransaction
For Each logEntry As Map In logsToWrite
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. Finalizar la transacción: Escritura eficiente a disco.
SQL1.TransactionSuccessful
If logger Then Log($"[LOG BATCH] Lote de ${batchSize} logs de rendimiento escrito exitosamente."$)
Catch
' Si falla, deshacemos todos los logs del lote y registramos el fallo.
SQL1.Rollback
Dim ErrorMsg As String = "ERROR CRÍTICO: Fallo al escribir lote de logs de rendimiento en SQLite: " & LastException.Message
Log(ErrorMsg)
' Usamos LogServerError para que el fallo quede registrado en la tabla 'errores' si el logging está habilitado.
LogServerError("ERROR", "Main.WriteQueryLogsBatch", ErrorMsg, Null, "log_batch_write_performance", Null)
End Try
End Sub
' --- Subrutina de evento para el Timer 'timerLogs'. --- ' --- Subrutina de evento para el Timer 'timerLogs'. ---
' El estado 'Enabled' del Timer ya está controlado por IsAnySQLiteLoggingEnabled en AppStart y Manager. ' El estado 'Enabled' del Timer ya está controlado por IsAnySQLiteLoggingEnabled en AppStart y Manager.
Sub TimerLogs_Tick Sub TimerLogs_Tick
Try Try
' 1. Vaciado de logs de rendimiento (asumiendo que WriteQueryLogsBatch también fue implementado)
WriteQueryLogsBatch
' 2. Vaciado de logs de errores
WriteErrorLogsBatch
' 3. Limpieza y VACUUM (esto ya verifica IsAnySQLiteLoggingEnabled [8])
borraArribaDe15000Logs borraArribaDe15000Logs
Catch Catch
Dim ErrorMsg As String = "ERROR en TimerLogs_Tick al intentar borrar logs: " & LastException.Message Dim ErrorMsg As String = "ERROR en TimerLogs_Tick al intentar borrar logs: " & LastException.Message
Log(ErrorMsg) Log(ErrorMsg)
@@ -457,16 +635,145 @@ Sub TimerLogs_Tick
End Try End Try
End Sub End Sub
Public Sub WriteErrorLogsBatch
Dim logsToWrite As List
logsToWrite.Initialize ' *** Aseguramos que logsToWrite sea una LISTA NUEVA y no dependa de la referencia.
' === PASO 1: Intercambio Atómico de Caché (Protegido por ReentrantLock) ===
MainConnectorsLock.RunMethod("lock", Null) ' Adquirimos el bloqueo.
' Log($"[DEBUG BATCH] Lock adquirido en WriteErrorLogsBatch. Caché Size: ${ErrorLogCache.Size}"$)
If ErrorLogCache.Size = 0 Then
MainConnectorsLock.RunMethod("unlock", Null)
' Log("[DEBUG BATCH] Saliendo: Caché vacía.")
Return
End If
' *** CORRECCIÓN CRÍTICA: Copiamos el CONTENIDO de forma atómica. ***
logsToWrite.AddAll(ErrorLogCache) ' <--- ESTO PASA LOS 10 REGISTROS A LA NUEVA LISTA
' Vaciamos la caché global. logsToWrite AHORA ES INDEPENDIENTE.
ErrorLogCache.Initialize
MainConnectorsLock.RunMethod("unlock", Null) ' Liberamos el bloqueo.
' Usamos el tamaño de la lista *copiada*.
Dim batchSize As Int = logsToWrite.Size
If logger Then Log($"[LOG BATCH] Iniciando escritura transaccional de ${batchSize} logs de ERRORES a SQLite. Logs copiados: ${logsToWrite.Size}"$)
' === PASO 2: Escritura Transaccional a SQLite (Usa logsToWrite) ===
If batchSize = 0 Then
Log("ADVERTENCIA: Fallo en la copia de la lista. logsToWrite está vacía. Abortando escritura.")
Return
End If
Try
' 1. Iniciar la transacción.
SQL1.BeginTransaction
For Each logEntry As Map In logsToWrite
' ... (Tu lógica de SQL1.ExecNonQuery2 aquí) ...
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] Lote de ${logsToWrite.Size} logs de ERRORES escrito exitosamente."$)
Catch
' 3. Rollback si falla.
SQL1.Rollback
Dim ErrorMsg As String = "ERROR CRÍTICO: Fallo al escribir lote de logs de ERRORES en SQLite: " & LastException.Message
Log(ErrorMsg)
End Try
End Sub
' --- Borra los registros más antiguos de la tabla 'query_logs' y hace VACUUM. --- ' --- Borra los registros más antiguos de la tabla 'query_logs' y hace VACUUM. ---
' ¡MODIFICADA PARA USAR FILTRADO GLOBAL!
Sub borraArribaDe15000Logs 'ignore Sub borraArribaDe15000Logs 'ignore
If IsAnySQLiteLoggingEnabled Then ' Solo ejecutar si al menos una DB requiere logs. If IsAnySQLiteLoggingEnabled Then ' Solo ejecutar si al menos una DB requiere logs.
Log("Recortando la tabla de 'query_logs', límite de 15,000 registros.") If logger Then Log("Recortando la tabla de 'query_logs', límite de 15,000 registros.")
' 1. Limpieza de Logs de Rendimiento (query_logs)
If logger Then Log("Recortando la tabla de 'query_logs', límite de 15,000 registros.")
SQL1.ExecNonQuery("DELETE FROM query_logs WHERE timestamp NOT in (SELECT timestamp FROM query_logs ORDER BY timestamp desc LIMIT 15000 )") SQL1.ExecNonQuery("DELETE FROM query_logs WHERE timestamp NOT in (SELECT timestamp FROM query_logs ORDER BY timestamp desc LIMIT 15000 )")
' 2. Limpieza de Logs de Errores (errores)
If logger Then Log("Recortando la tabla de 'errores', límite de 15,000 registros.")
SQL1.ExecNonQuery("DELETE FROM errores WHERE timestamp NOT in (SELECT timestamp FROM errores ORDER BY timestamp desc LIMIT 15000 )")
' 3. Optimización de disco
SQL1.ExecNonQuery("vacuum;") SQL1.ExecNonQuery("vacuum;")
Else Else
' Si IsAnySQLiteLoggingEnabled es False, el Timer no debería estar activo. ' Si IsAnySQLiteLoggingEnabled es False, el Timer no debería estar activo.
Log("AVISO: Tarea de limpieza de logs omitida. El logging global de SQLite está deshabilitado.") If logger Then Log("AVISO: Tarea de limpieza de logs omitida. El logging global de SQLite está deshabilitado.")
End If End If
End Sub End Sub
'Copiamos recursos del jar al directorio de la app
Sub CopiarRecursoSiNoExiste(NombreArchivo As String, SubCarpeta As String)
Dim DirDestino As String = File.Combine(File.DirApp, SubCarpeta)
If SubCarpeta <> "" And File.Exists(DirDestino, "") = False Then
File.MakeDir(DirDestino, "")
End If
Dim ArchivoDestino As String = File.Combine(DirDestino, NombreArchivo)
If File.Exists(DirDestino, NombreArchivo) = False Then
Dim RutaRecurso As String
If SubCarpeta <> "" Then
RutaRecurso = "Files/" & SubCarpeta & "/" & NombreArchivo
Else
RutaRecurso = "Files/" & NombreArchivo
End If
Dim classLoader As JavaObject = GetThreadContextClassLoader
Dim InStream As InputStream = classLoader.RunMethod("getResourceAsStream", Array(RutaRecurso))
If InStream.IsInitialized Then
Log($"Copiando recurso: '${RutaRecurso}'..."$)
' Llamamos a nuestra propia función de copiado manual
Dim OutStream As OutputStream = File.OpenOutput(DirDestino, NombreArchivo, False)
CopiarStreamManualmente(InStream, OutStream)
Log($"'${ArchivoDestino}' copiado correctamente."$)
Else
Log($"ERROR: No se pudo encontrar el recurso con la ruta interna: '${RutaRecurso}'"$)
End If
End If
End Sub
' No depende de ninguna librería extraña.
Sub CopiarStreamManualmente (InStream As InputStream, OutStream As OutputStream)
Try
Dim buffer(1024) As Byte
Dim len As Int
len = InStream.ReadBytes(buffer, 0, buffer.Length)
Do While len > 0
OutStream.WriteBytes(buffer, 0, len)
len = InStream.ReadBytes(buffer, 0, buffer.Length)
Loop
Catch
LogError(LastException)
End Try
InStream.Close
OutStream.Close
End Sub
' Función ayudante para obtener el Class Loader correcto.
Sub GetThreadContextClassLoader As JavaObject
Dim thread As JavaObject
thread = thread.InitializeStatic("java.lang.Thread").RunMethod("currentThread", Null)
Return thread.RunMethod("getContextClassLoader", Null)
End Sub

View File

@@ -5,6 +5,9 @@ ModuleBookmarks11=
ModuleBookmarks12= ModuleBookmarks12=
ModuleBookmarks13= ModuleBookmarks13=
ModuleBookmarks14= ModuleBookmarks14=
ModuleBookmarks15=
ModuleBookmarks16=
ModuleBookmarks17=
ModuleBookmarks2= ModuleBookmarks2=
ModuleBookmarks3= ModuleBookmarks3=
ModuleBookmarks4= ModuleBookmarks4=
@@ -20,6 +23,9 @@ ModuleBreakpoints11=
ModuleBreakpoints12= ModuleBreakpoints12=
ModuleBreakpoints13= ModuleBreakpoints13=
ModuleBreakpoints14= ModuleBreakpoints14=
ModuleBreakpoints15=
ModuleBreakpoints16=
ModuleBreakpoints17=
ModuleBreakpoints2= ModuleBreakpoints2=
ModuleBreakpoints3= ModuleBreakpoints3=
ModuleBreakpoints4= ModuleBreakpoints4=
@@ -28,21 +34,24 @@ ModuleBreakpoints6=
ModuleBreakpoints7= ModuleBreakpoints7=
ModuleBreakpoints8= ModuleBreakpoints8=
ModuleBreakpoints9= ModuleBreakpoints9=
ModuleClosedNodes0= ModuleClosedNodes0=5,6,7,8,9,10,12,13
ModuleClosedNodes1= ModuleClosedNodes1=
ModuleClosedNodes10= ModuleClosedNodes10=
ModuleClosedNodes11= ModuleClosedNodes11=
ModuleClosedNodes12= ModuleClosedNodes12=
ModuleClosedNodes13= ModuleClosedNodes13=
ModuleClosedNodes14= ModuleClosedNodes14=
ModuleClosedNodes15=3,5,6
ModuleClosedNodes16=2,3
ModuleClosedNodes17=
ModuleClosedNodes2= ModuleClosedNodes2=
ModuleClosedNodes3=9,10,11,12,13,14,15,16 ModuleClosedNodes3=
ModuleClosedNodes4= ModuleClosedNodes4=
ModuleClosedNodes5= ModuleClosedNodes5=
ModuleClosedNodes6= ModuleClosedNodes6=
ModuleClosedNodes7= ModuleClosedNodes7=
ModuleClosedNodes8= ModuleClosedNodes8=
ModuleClosedNodes9= ModuleClosedNodes9=
NavigationStack=DBHandlerB4X,CleanupAndLog,198,0,DBHandlerJSON,CleanupAndLog,223,0,ParameterValidationUtils,ValidateAndAdjustParameters,45,0,Main,Process_Globals,53,0,Main,AppStart,186,0,Main,LogQueryPerformance,367,0,Main,LogServerError,384,6,Manager,Handle,164,6,Main,borraArribaDe15000Logs,412,0,Cambios,Process_Globals,25,6 NavigationStack=Main,CopiarStreamManualmente,714,0,Main,GetThreadContextClassLoader,716,0,Main,borraArribaDe15000Logs,653,0,Main,WriteErrorLogsBatch,631,0,Main,InitializeSQLiteDatabase,332,0,Main,CopiarRecursoSiNoExiste,698,6,RDCConnector,Initialize,100,0,Manager,Handle,301,0,Cambios,Process_Globals,20,1,Main,AppStart,88,4
SelectedBuild=0 SelectedBuild=0
VisibleModules=3,4,13,1,10,11,14,2 VisibleModules=3,4,14,1,10,15,16,17,13