mirror of
https://github.com/KeymonSoft/jRDC-Multi.git
synced 2026-04-18 21:29:29 +00:00
Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 8b876e5095 | |||
| 616013f0fb | |||
| 820fe9fc2b |
32
Cambios.bas
32
Cambios.bas
@@ -21,11 +21,39 @@ 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
|
' - VERSION 5.09.16.2
|
||||||
' - feat(logs): Implementación de Cacheo y Escritura Transaccional en Lotes
|
' - feat(logs): Implementación de Cacheo y Escritura Transaccional en Lotes
|
||||||
'
|
'
|
||||||
|
|||||||
477
DBHandlerB4X.bas
477
DBHandlerB4X.bas
@@ -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.
|
||||||
@@ -427,34 +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 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 queryName As String = ReadObject(in)
|
||||||
|
Dim params As List = ReadList(in)
|
||||||
|
' Log(params)
|
||||||
If numberOfStatements = 1 Then
|
If numberOfStatements = 1 Then
|
||||||
singleQueryName = queryName 'Capturamos el nombre del query.
|
singleQueryName = queryName 'Capturamos el nombre del query.
|
||||||
End If
|
End If
|
||||||
Dim sqlCommand As String = Connector.GetCommand(DB, queryName)
|
Dim sqlCommand As String = Connector.GetCommand(DB, queryName)
|
||||||
|
' Log(sqlCommand)
|
||||||
' <<< INICIO NUEVA VALIDACIÓN: VERIFICAR SI EL COMANDO EXISTE (V1) >>>
|
' <<< INICIO NUEVA VALIDACIÓN: VERIFICAR SI EL COMANDO EXISTE (V1) >>>
|
||||||
If sqlCommand = Null Or sqlCommand = "null" Or sqlCommand.Trim = "" Then
|
If sqlCommand = Null Or sqlCommand = "null" Or sqlCommand.Trim = "" Then
|
||||||
con.Rollback ' Deshace la transacción si un comando es inválido.
|
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}'."$
|
Dim errorMessage As String = $"El comando '${queryName}' no fue encontrado en el config.properties de '${DB}'."$
|
||||||
Log(errorMessage)
|
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) >>>
|
||||||
@@ -465,29 +501,38 @@ 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
|
If numberOfStatements = 1 And singleQueryName <> "" Then
|
||||||
@@ -499,24 +544,24 @@ 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) >>>
|
||||||
@@ -531,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
|
||||||
|
|||||||
@@ -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 ---
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ AcquireIncrement=1
|
|||||||
MaxConnectionAge=60
|
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
|
||||||
|
|||||||
@@ -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
392
Files/www/manager.html
Normal file
@@ -0,0 +1,392 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="es">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>jRDC2-Multi - Panel de Administración</title>
|
||||||
|
<style>
|
||||||
|
body {
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto,
|
||||||
|
Helvetica, Arial, sans-serif;
|
||||||
|
background-color: #f8f9fa;
|
||||||
|
color: #212529;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
.admin-menu {
|
||||||
|
background-color: #e9ecef;
|
||||||
|
padding: 1.5em 2em;
|
||||||
|
border-bottom: 1px solid #dee2e6;
|
||||||
|
}
|
||||||
|
.admin-menu h2 {
|
||||||
|
margin: 0 0 0.2em 0;
|
||||||
|
color: #343a40;
|
||||||
|
}
|
||||||
|
.admin-menu p {
|
||||||
|
margin: 0 0 1em 0;
|
||||||
|
color: #495057;
|
||||||
|
}
|
||||||
|
.admin-menu a {
|
||||||
|
color: #007bff;
|
||||||
|
text-decoration: none;
|
||||||
|
font-weight: 500;
|
||||||
|
margin-right: 0.5em;
|
||||||
|
padding-right: 0.5em;
|
||||||
|
border-right: 1px solid #ccc;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
.admin-menu a:last-child {
|
||||||
|
border-right: none;
|
||||||
|
}
|
||||||
|
.admin-menu a:hover {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
.main-content {
|
||||||
|
padding: 2em;
|
||||||
|
}
|
||||||
|
#output-container {
|
||||||
|
background: #fff;
|
||||||
|
padding: 1em;
|
||||||
|
border: 1px solid #eee;
|
||||||
|
border-radius: 8px;
|
||||||
|
font-family: monospace;
|
||||||
|
white-space: pre-wrap;
|
||||||
|
word-wrap: break-word;
|
||||||
|
min-height: 200px;
|
||||||
|
overflow-x: auto;
|
||||||
|
}
|
||||||
|
table {
|
||||||
|
width: 100%;
|
||||||
|
border-collapse: collapse;
|
||||||
|
margin-top: 1.5em;
|
||||||
|
font-family: sans-serif;
|
||||||
|
font-size: 0.9em;
|
||||||
|
}
|
||||||
|
th,
|
||||||
|
td {
|
||||||
|
padding: 10px 12px;
|
||||||
|
text-align: center;
|
||||||
|
border-bottom: 1px solid #dee2e6;
|
||||||
|
}
|
||||||
|
thead {
|
||||||
|
background-color: #007bff;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
th {
|
||||||
|
font-weight: 600;
|
||||||
|
cursor: help;
|
||||||
|
}
|
||||||
|
tbody td:first-child {
|
||||||
|
font-weight: bold;
|
||||||
|
color: #0056b3;
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* --- ESTILOS PARA EL INDICADOR SSE --- */
|
||||||
|
.sse-status-container {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
margin-top: 1em;
|
||||||
|
font-size: 0.8em;
|
||||||
|
}
|
||||||
|
.sse-status {
|
||||||
|
display: inline-block;
|
||||||
|
padding: 4px 8px;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-weight: bold;
|
||||||
|
margin-left: 10px;
|
||||||
|
min-width: 80px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
.sse-connected {
|
||||||
|
background-color: #28a745;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
.sse-disconnected {
|
||||||
|
background-color: #dc3545;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
.sse-connecting {
|
||||||
|
background-color: #ffc107;
|
||||||
|
color: #212529;
|
||||||
|
}
|
||||||
|
.cell-update {
|
||||||
|
animation: flash 0.7s ease-out;
|
||||||
|
}
|
||||||
|
@keyframes flash {
|
||||||
|
0% {
|
||||||
|
background-color: #ffc107;
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
background-color: transparent;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="admin-menu">
|
||||||
|
<h2>Panel de Administración jRDC</h2>
|
||||||
|
<p>Bienvenido, <strong>admin</strong></p>
|
||||||
|
<nav id="main-nav">
|
||||||
|
<a data-command="test">Test</a>
|
||||||
|
<a data-command="ping">Ping</a>
|
||||||
|
<a data-command="reload">Reload</a>
|
||||||
|
<a data-command="slowqueries">Queries Lentas</a>
|
||||||
|
<a data-command="getstats">Estadísticas Pool</a>
|
||||||
|
<a data-command="rpm2">Reiniciar (pm2)</a>
|
||||||
|
<a data-command="reviveBow">Revive Bow</a>
|
||||||
|
<a data-command="getconfiginfo">Info</a>
|
||||||
|
</nav>
|
||||||
|
<div class="sse-status-container">
|
||||||
|
<span>Estado de Estadísticas en Tiempo Real:</span>
|
||||||
|
<span id="sse-status" class="sse-status sse-disconnected"
|
||||||
|
>Desconectado</span
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="main-content">
|
||||||
|
<h1 id="content-title">Bienvenido</h1>
|
||||||
|
<div id="output-container">
|
||||||
|
<p style="font-family: sans-serif">
|
||||||
|
Selecciona una opción del menú para comenzar.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
const outputContainer = document.getElementById("output-container");
|
||||||
|
const contentTitle = document.getElementById("content-title");
|
||||||
|
const sseStatus = document.getElementById("sse-status");
|
||||||
|
let sseConnection = null;
|
||||||
|
|
||||||
|
// --- CONFIGURACIÓN PARA LA TABLA DE ESTADÍSTICAS ---
|
||||||
|
const COLUMN_ORDER = [
|
||||||
|
"InitialPoolSize",
|
||||||
|
"MinPoolSize",
|
||||||
|
"MaxPoolSize",
|
||||||
|
"AcquireIncrement",
|
||||||
|
"TotalConnections",
|
||||||
|
"BusyConnections",
|
||||||
|
"IdleConnections",
|
||||||
|
"CheckoutTimeout",
|
||||||
|
"MaxIdleTime",
|
||||||
|
"MaxConnectionAge",
|
||||||
|
];
|
||||||
|
const HEADER_TOOLTIPS = {
|
||||||
|
InitialPoolSize:
|
||||||
|
"Número de conexiones que el pool intenta adquirir al arrancar.",
|
||||||
|
MinPoolSize: "Número mínimo de conexiones que el pool mantendrá.",
|
||||||
|
MaxPoolSize: "Número máximo de conexiones que el pool puede mantener.",
|
||||||
|
AcquireIncrement:
|
||||||
|
"Número de conexiones a adquirir cuando el pool se queda sin conexiones.",
|
||||||
|
TotalConnections: "Número total de conexiones (ocupadas + libres).",
|
||||||
|
BusyConnections: "Número de conexiones activamente en uso.",
|
||||||
|
IdleConnections: "Número de conexiones disponibles en el pool.",
|
||||||
|
CheckoutTimeout: "Tiempo máximo de espera por una conexión (ms).",
|
||||||
|
MaxIdleTime:
|
||||||
|
"Tiempo máximo que una conexión puede estar inactiva (segundos).",
|
||||||
|
MaxConnectionAge: "Tiempo máximo de vida de una conexión (segundos).",
|
||||||
|
};
|
||||||
|
|
||||||
|
// --- MANEJO DE LA CONEXIÓN SSE ---
|
||||||
|
|
||||||
|
function connectSSE() {
|
||||||
|
if (sseConnection && sseConnection.readyState !== EventSource.CLOSED) {
|
||||||
|
return; // Ya está conectado o conectando
|
||||||
|
}
|
||||||
|
|
||||||
|
outputContainer.innerHTML =
|
||||||
|
'<p style="font-family: sans-serif;">Esperando datos del pool de conexiones...</p>';
|
||||||
|
updateSSEStatus("connecting");
|
||||||
|
|
||||||
|
// La ruta debe coincidir con la que registraste en srvr.AddHandler
|
||||||
|
const SSE_ENDPOINT = "/stats-stream";
|
||||||
|
sseConnection = new EventSource(SSE_ENDPOINT);
|
||||||
|
|
||||||
|
sseConnection.onopen = () => {
|
||||||
|
console.log("Conexión SSE establecida.");
|
||||||
|
updateSSEStatus("connected");
|
||||||
|
};
|
||||||
|
|
||||||
|
// Escucha el evento específico "stats_update"
|
||||||
|
sseConnection.addEventListener("stats_update", (event) => {
|
||||||
|
try {
|
||||||
|
const data = JSON.parse(event.data);
|
||||||
|
renderOrUpdateStatsTable(data);
|
||||||
|
} catch (e) {
|
||||||
|
console.error("Error al parsear datos SSE:", e);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
sseConnection.onerror = () => {
|
||||||
|
console.error("Error en la conexión SSE. Reintentando...");
|
||||||
|
updateSSEStatus("disconnected");
|
||||||
|
sseConnection.close();
|
||||||
|
// El navegador reintentará automáticamente la conexión
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function disconnectSSE() {
|
||||||
|
if (sseConnection) {
|
||||||
|
sseConnection.close();
|
||||||
|
sseConnection = null;
|
||||||
|
console.log("Conexión SSE cerrada.");
|
||||||
|
updateSSEStatus("disconnected");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateSSEStatus(status) {
|
||||||
|
switch (status) {
|
||||||
|
case "connected":
|
||||||
|
sseStatus.textContent = "Conectado";
|
||||||
|
sseStatus.className = "sse-status sse-connected";
|
||||||
|
break;
|
||||||
|
case "connecting":
|
||||||
|
sseStatus.textContent = "Conectando";
|
||||||
|
sseStatus.className = "sse-status sse-connecting";
|
||||||
|
break;
|
||||||
|
case "disconnected":
|
||||||
|
sseStatus.textContent = "Desconectado";
|
||||||
|
sseStatus.className = "sse-status sse-disconnected";
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- RENDERIZADO Y ACTUALIZACIÓN DE LA TABLA ---
|
||||||
|
|
||||||
|
function renderOrUpdateStatsTable(data) {
|
||||||
|
const table = document.getElementById("stats-table");
|
||||||
|
// Si la tabla no existe, la crea
|
||||||
|
if (!table) {
|
||||||
|
outputContainer.innerHTML = createStatsTableHTML(data);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Si la tabla ya existe, solo actualiza las celdas
|
||||||
|
for (const dbKey in data) {
|
||||||
|
const poolData = data[dbKey];
|
||||||
|
COLUMN_ORDER.forEach((metric) => {
|
||||||
|
const cell = document.getElementById(`${dbKey}_${metric}`);
|
||||||
|
if (cell && cell.textContent != poolData[metric]) {
|
||||||
|
cell.textContent = poolData[metric];
|
||||||
|
cell.classList.add("cell-update");
|
||||||
|
setTimeout(() => cell.classList.remove("cell-update"), 700);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function createStatsTableHTML(data) {
|
||||||
|
let tableHtml = `<table id="stats-table"><thead><tr><th title="Nombre de la Base de Datos">DB Key</th>`;
|
||||||
|
COLUMN_ORDER.forEach((key) => {
|
||||||
|
tableHtml += `<th title="${HEADER_TOOLTIPS[key] || ""}">${key}</th>`;
|
||||||
|
});
|
||||||
|
tableHtml += `</tr></thead><tbody>`;
|
||||||
|
for (const dbKey in data) {
|
||||||
|
const poolData = data[dbKey];
|
||||||
|
tableHtml += `<tr><td>${dbKey}</td>`;
|
||||||
|
COLUMN_ORDER.forEach((metric) => {
|
||||||
|
// Se añade un ID único a cada celda: "DB1_TotalConnections"
|
||||||
|
tableHtml += `<td id="${dbKey}_${metric}">${
|
||||||
|
poolData[metric] ?? "N/A"
|
||||||
|
}</td>`;
|
||||||
|
});
|
||||||
|
tableHtml += `</tr>`;
|
||||||
|
}
|
||||||
|
tableHtml += `</tbody></table>`;
|
||||||
|
return tableHtml;
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- MANEJO DE COMANDOS ESTÁTICOS (SIN CAMBIOS) ---
|
||||||
|
|
||||||
|
async function loadStaticContent(command) {
|
||||||
|
contentTitle.textContent = `Resultado del Comando: '${command}'`;
|
||||||
|
outputContainer.innerHTML = "Cargando...";
|
||||||
|
try {
|
||||||
|
const response = await fetch(`/manager?command=${command}`);
|
||||||
|
const responseText = await response.text();
|
||||||
|
if (!response.ok)
|
||||||
|
throw new Error(
|
||||||
|
`Error del servidor (${response.status}): ${responseText}`
|
||||||
|
);
|
||||||
|
|
||||||
|
const contentType = response.headers.get("content-type");
|
||||||
|
if (contentType && contentType.includes("application/json")) {
|
||||||
|
const data = JSON.parse(responseText);
|
||||||
|
if (command === "slowqueries") {
|
||||||
|
outputContainer.innerHTML = data.message
|
||||||
|
? `<p>${data.message}</p>`
|
||||||
|
: createTableFromJSON(data.data);
|
||||||
|
} else {
|
||||||
|
outputContainer.textContent = JSON.stringify(data, null, 2);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
outputContainer.textContent = responseText;
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
outputContainer.textContent = `Error al procesar la respuesta:\n${error.message}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function createTableFromJSON(data) {
|
||||||
|
if (!data || data.length === 0)
|
||||||
|
return "<p>No se encontraron queries lentas.</p>";
|
||||||
|
const headers = Object.keys(data[0]);
|
||||||
|
let table = "<table><thead><tr>";
|
||||||
|
headers.forEach((h) => (table += `<th>${h.replace(/_/g, " ")}</th>`));
|
||||||
|
table += "</tr></thead><tbody>";
|
||||||
|
data.forEach((row) => {
|
||||||
|
table += "<tr>";
|
||||||
|
headers.forEach((h) => (table += `<td>${row[h]}</td>`));
|
||||||
|
table += "</tr>";
|
||||||
|
});
|
||||||
|
table += "</tbody></table>";
|
||||||
|
return table;
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- EVENT LISTENER PRINCIPAL ---
|
||||||
|
|
||||||
|
document.getElementById("main-nav").addEventListener("click", (event) => {
|
||||||
|
if (event.target.tagName === "A") {
|
||||||
|
const command = event.target.dataset.command;
|
||||||
|
if (!command) return;
|
||||||
|
|
||||||
|
if (command === "reload") {
|
||||||
|
// Pedimos al usuario la DB Key. Si la deja vacía, se asume recarga total.
|
||||||
|
const dbKey = prompt(
|
||||||
|
"Ingrese la llave de la DB a recargar (ej: DB2, DB3). Deje vacío para recargar TODAS:",
|
||||||
|
""
|
||||||
|
);
|
||||||
|
if (dbKey === null) {
|
||||||
|
// El usuario presionó Cancelar o cerró la ventana. NO HACER NADA.
|
||||||
|
outputContainer.textContent = "Recarga cancelada por el usuario.";
|
||||||
|
contentTitle.textContent = "Administración";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let finalCommand = "reload";
|
||||||
|
if (dbKey && dbKey.trim() !== "") {
|
||||||
|
// Si el usuario especificó una DB (ej. DB2), construimos el comando con el parámetro 'db'.
|
||||||
|
const key = dbKey.toUpperCase().trim();
|
||||||
|
finalCommand = `reload&db=${key}`;
|
||||||
|
outputContainer.innerHTML = `<p style="font-family: sans-serif;">Intentando recargar: <b>${key}</b> (Hot-Swap)...</p>`;
|
||||||
|
} else {
|
||||||
|
// Recarga total.
|
||||||
|
outputContainer.innerHTML = `<p style="font-family: sans-serif;">Intentando recargar: <b>TODAS</b> (Hot-Swap)...</p>`;
|
||||||
|
}
|
||||||
|
disconnectSSE();
|
||||||
|
// Llamamos a la función loadStaticContent con el comando completo (ej: 'reload&db=DB2' o 'reload')
|
||||||
|
loadStaticContent(finalCommand);
|
||||||
|
} else if (command === "getstats") {
|
||||||
|
contentTitle.textContent = `Estadísticas del Pool en Tiempo Real`;
|
||||||
|
connectSSE();
|
||||||
|
} else {
|
||||||
|
// Si se selecciona cualquier otro comando, se desconecta del SSE
|
||||||
|
disconnectSSE();
|
||||||
|
loadStaticContent(command);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
812
Manager.bas
812
Manager.bas
@@ -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
|
||||||
@@ -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.
|
||||||
|
|||||||
@@ -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
187
SSE.bas
Normal 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
131
SSEHandler.bas
Normal file
@@ -0,0 +1,131 @@
|
|||||||
|
B4J=true
|
||||||
|
Group=Default Group
|
||||||
|
ModulesStructureVersion=1
|
||||||
|
Type=Class
|
||||||
|
Version=10.3
|
||||||
|
@EndOfDesignText@
|
||||||
|
' Handler class: StatsSSEHandler.b4j
|
||||||
|
' Gestiona y transmite en tiempo real las estadísticas del pool de conexiones vía Server-Sent Events (SSE).
|
||||||
|
' Opera en modo Singleton: una única instancia maneja todas las conexiones.
|
||||||
|
|
||||||
|
Sub Class_Globals
|
||||||
|
' Almacena de forma centralizada a todos los clientes (navegadores) conectados.
|
||||||
|
' La clave es un ID único y el valor es el canal de comunicación (OutputStream).
|
||||||
|
Private Connections As Map
|
||||||
|
|
||||||
|
' Timer #1 ("El Vigilante"): Se encarga de detectar y eliminar conexiones muertas.
|
||||||
|
Private RemoveTimer As Timer
|
||||||
|
|
||||||
|
' Timer #2 ("El Informante"): Se encarga de recolectar y enviar los datos de estadísticas.
|
||||||
|
Dim StatsTimer As Timer
|
||||||
|
Dim const UPDATE_INTERVAL_MS As Long = 2000 ' Intervalo de envío de estadísticas: 2 segundos.
|
||||||
|
End Sub
|
||||||
|
|
||||||
|
' Se ejecuta UNA SOLA VEZ cuando el servidor arranca, gracias al modo Singleton.
|
||||||
|
Public Sub Initialize
|
||||||
|
Log("Stats SSE Handler Initialized (Singleton Mode)")
|
||||||
|
|
||||||
|
' Crea el mapa de conexiones, asegurando que sea seguro para el manejo de múltiples hilos.
|
||||||
|
Connections = Main.srvr.CreateThreadSafeMap
|
||||||
|
|
||||||
|
' Configura y activa el timer para la limpieza de conexiones cada 5 segundos.
|
||||||
|
' NOTA: El EventName "RemoveTimer" debe coincidir con el nombre de la subrutina del tick.
|
||||||
|
RemoveTimer.Initialize("RemoveTimer", 5000)
|
||||||
|
RemoveTimer.Enabled = True
|
||||||
|
|
||||||
|
' Configura y activa el timer para el envío de estadísticas.
|
||||||
|
' NOTA: El EventName "StatsTimer" debe coincidir con el nombre de la subrutina del tick.
|
||||||
|
StatsTimer.Initialize("StatsTimer", UPDATE_INTERVAL_MS)
|
||||||
|
StatsTimer.Enabled = True
|
||||||
|
End Sub
|
||||||
|
|
||||||
|
' Es el punto de entrada principal. Atiende todas las peticiones HTTP dirigidas a este handler.
|
||||||
|
Sub Handle(req As ServletRequest, resp As ServletResponse)
|
||||||
|
|
||||||
|
Log($"StatsTimerinicializado: ${StatsTimer.IsInitialized}, StatsTimer habilitado: ${StatsTimer.Enabled}"$)
|
||||||
|
StatsTimer.Initialize("StatsTimer", 2000)
|
||||||
|
StatsTimer.Enabled = True
|
||||||
|
|
||||||
|
' Filtro de seguridad: verifica si el usuario tiene una sesión autorizada.
|
||||||
|
If req.GetSession.GetAttribute2("user_is_authorized", False) = False Then
|
||||||
|
resp.SendRedirect("/login")
|
||||||
|
Return
|
||||||
|
End If
|
||||||
|
|
||||||
|
' Procesa únicamente las peticiones GET, que son las que usan los navegadores para iniciar una conexión SSE.
|
||||||
|
If req.Method = "GET" Then
|
||||||
|
' Mantiene la petición activa de forma asíncrona para poder enviar datos en el futuro.
|
||||||
|
Dim reqJO As JavaObject = req
|
||||||
|
reqJO.RunMethod("startAsync", Null)
|
||||||
|
|
||||||
|
' Registra al nuevo cliente para que empiece a recibir eventos.
|
||||||
|
SSE.AddTarget("stats", resp)
|
||||||
|
Else
|
||||||
|
' Rechaza cualquier otro método HTTP (POST, PUT, etc.) con un error.
|
||||||
|
resp.SendError(405, "Method Not Allowed")
|
||||||
|
End If
|
||||||
|
End Sub
|
||||||
|
|
||||||
|
' --- LÓGICA DE LOS TIMERS ---
|
||||||
|
|
||||||
|
' Evento del Timer #1 ("El Vigilante"): se dispara cada 5 segundos.
|
||||||
|
Sub RemoveTimer_Tick
|
||||||
|
' Log("REMOVETIMER TICK")
|
||||||
|
' Optimización: si no hay nadie conectado, no hace nada.
|
||||||
|
If Connections.Size = 0 Then Return
|
||||||
|
|
||||||
|
' Itera sobre todos los clientes para verificar si siguen activos.
|
||||||
|
For Each key As String In Connections.Keys
|
||||||
|
Try
|
||||||
|
' Envía un evento "ping" silencioso. Si la conexión está viva, no pasa nada.
|
||||||
|
SSE.SendMessage(Connections.Get(key), "ping", "", 0, "")
|
||||||
|
Catch
|
||||||
|
' Si el envío falla, la conexión está muerta. Se procede a la limpieza.
|
||||||
|
Log("######################")
|
||||||
|
Log("## Removing (timer cleanup): " & key)
|
||||||
|
Log("######################")
|
||||||
|
Connections.Remove(key)
|
||||||
|
End Try
|
||||||
|
Next
|
||||||
|
End Sub
|
||||||
|
|
||||||
|
' Evento del Timer #2 ("El Informante"): se dispara cada 2 segundos.
|
||||||
|
public Sub StatsTimer_Tick
|
||||||
|
' Optimización: si no hay nadie conectado, no realiza el trabajo pesado.
|
||||||
|
If Connections.Size = 0 Then Return
|
||||||
|
|
||||||
|
Try
|
||||||
|
' Prepara un mapa para almacenar las estadísticas recolectadas.
|
||||||
|
Dim allPoolStats As Map
|
||||||
|
allPoolStats.Initialize
|
||||||
|
|
||||||
|
' Bloquea el acceso a los conectores para leer sus datos de forma segura.
|
||||||
|
Main.MainConnectorsLock.RunMethod("lock", Null)
|
||||||
|
For Each dbKey As String In Main.listaDeCP
|
||||||
|
Dim connector As RDCConnector
|
||||||
|
If Main.Connectors.ContainsKey(dbKey) Then
|
||||||
|
connector = Main.Connectors.Get(dbKey)
|
||||||
|
If connector.IsInitialized Then
|
||||||
|
allPoolStats.Put(dbKey, connector.GetPoolStats)
|
||||||
|
Else
|
||||||
|
allPoolStats.Put(dbKey, CreateMap("Error": "Conector no inicializado"))
|
||||||
|
End If
|
||||||
|
End If
|
||||||
|
Next
|
||||||
|
' Libera el bloqueo para que otras partes del programa puedan usar los conectores.
|
||||||
|
Main.MainConnectorsLock.RunMethod("unlock", Null)
|
||||||
|
|
||||||
|
' Convierte el mapa de estadísticas a un formato de texto JSON.
|
||||||
|
Dim j As JSONGenerator
|
||||||
|
j.Initialize(allPoolStats)
|
||||||
|
Dim jsonStats As String = j.ToString
|
||||||
|
|
||||||
|
' Llama al "locutor" para enviar el JSON a todos los clientes conectados.
|
||||||
|
SSE.Broadcast("stats", "stats_update", jsonStats, 0)
|
||||||
|
|
||||||
|
Catch
|
||||||
|
' Captura y registra cualquier error que ocurra durante la recolección de datos.
|
||||||
|
Log($"[SSE] Error CRÍTICO durante la adquisición de estadísticas: ${LastException.Message}"$)
|
||||||
|
End Try
|
||||||
|
End Sub
|
||||||
|
|
||||||
168
jRDC_Multi.b4j
168
jRDC_Multi.b4j
@@ -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.2
|
' 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
|
||||||
@@ -125,18 +126,40 @@ Sub Process_Globals
|
|||||||
|
|
||||||
Public QueryLogCache As List ' Cache para los logs de rendimiento (query_logs)
|
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 ErrorLogCache As List ' Cache para los logs de errores y advertencias
|
||||||
Public Const LOG_CACHE_THRESHOLD As Int = 10 ' Umbral de registros para forzar la escritura
|
Public LOG_CACHE_THRESHOLD As Int = 350 ' Umbral de registros para forzar la escritura
|
||||||
|
|
||||||
Dim logger As Boolean
|
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
|
#if DEBUG
|
||||||
logger = True
|
logger = True
|
||||||
|
LOG_CACHE_THRESHOLD = 10
|
||||||
#else
|
#else
|
||||||
logger = False
|
logger = False
|
||||||
#End If
|
#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
|
QueryLogCache.Initialize
|
||||||
@@ -159,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
|
||||||
@@ -178,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
|
||||||
|
|
||||||
@@ -309,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.
|
||||||
@@ -329,7 +354,6 @@ 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)
|
||||||
@@ -339,6 +363,9 @@ Sub InitializeSQLiteDatabase
|
|||||||
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"
|
||||||
@@ -350,10 +377,23 @@ 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 <<<
|
||||||
If logger Then Log("Verificando y migrando tabla 'query_logs' si es necesario.")
|
If logger Then Log("Verificando y migrando tabla 'query_logs' si es necesario.")
|
||||||
@@ -543,7 +583,7 @@ Public Sub WriteQueryLogsBatch
|
|||||||
|
|
||||||
MainConnectorsLock.RunMethod("unlock", Null)
|
MainConnectorsLock.RunMethod("unlock", Null)
|
||||||
|
|
||||||
If logger Then Log($"[LOG BATCH] Iniciando escritura transaccional de ${batchSize} logs de rendimiento. Logs copiados: ${logsToWrite.Size}"$)
|
' If logger Then Log($"[LOG BATCH] Iniciando escritura transaccional de ${batchSize} logs de rendimiento. Logs copiados: ${logsToWrite.Size}"$)
|
||||||
|
|
||||||
' === PASO 2: Escritura Transaccional a SQLite ===
|
' === PASO 2: Escritura Transaccional a SQLite ===
|
||||||
|
|
||||||
@@ -561,7 +601,7 @@ Public Sub WriteQueryLogsBatch
|
|||||||
' 2. Finalizar la transacción: Escritura eficiente a disco.
|
' 2. Finalizar la transacción: Escritura eficiente a disco.
|
||||||
SQL1.TransactionSuccessful
|
SQL1.TransactionSuccessful
|
||||||
|
|
||||||
if logger then Log($"[LOG BATCH] Lote de ${batchSize} logs de rendimiento escrito exitosamente."$)
|
If logger Then Log($"[LOG BATCH] Lote de ${batchSize} logs de rendimiento escrito exitosamente."$)
|
||||||
|
|
||||||
Catch
|
Catch
|
||||||
' Si falla, deshacemos todos los logs del lote y registramos el fallo.
|
' Si falla, deshacemos todos los logs del lote y registramos el fallo.
|
||||||
@@ -657,15 +697,83 @@ Public Sub WriteErrorLogsBatch
|
|||||||
End Sub
|
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.
|
||||||
|
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.")
|
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.
|
||||||
If logger Then 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
|
||||||
|
|||||||
@@ -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=Main,TimerLogs_Tick,533,0,Main,LogQueryPerformance,414,5,Main,LogServerError,459,0,Main,AppStart,244,0,Main,WriteQueryLogsBatch,512,1,Main,borraArribaDe15000Logs,617,0,Main,WriteErrorLogsBatch,602,1,Main,InitializeSQLiteDatabase,365,0,Main,Process_Globals,76,4,Cambios,Process_Globals,22,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
|
||||||
|
|||||||
Reference in New Issue
Block a user