mirror of
https://github.com/KeymonSoft/jRDC-Multi.git
synced 2026-04-18 13:19:20 +00:00
- 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`.
314 lines
13 KiB
QBasic
314 lines
13 KiB
QBasic
B4J=true
|
|
Group=Default Group
|
|
ModulesStructureVersion=1
|
|
Type=Class
|
|
Version=10.3
|
|
@EndOfDesignText@
|
|
' Módulo de clase: Manager
|
|
' Este handler proporciona un panel de administración web para el servidor jRDC2-Multi.
|
|
' Permite monitorear el estado del servidor, recargar configuraciones de bases de datos,
|
|
' ver estadísticas de rendimiento, reiniciar servicios externos, y gestionar la autenticación de usuarios.
|
|
|
|
Sub Class_Globals
|
|
' Objeto para generar respuestas JSON. Se utiliza para mostrar mapas de datos de forma legible.
|
|
Dim j As JSONGenerator
|
|
' La clase BCrypt no se usa directamente en este módulo, pero se mantiene si hubiera planes futuros.
|
|
' Private bc As BCrypt
|
|
End Sub
|
|
|
|
' Subrutina de inicialización de la clase. Se llama cuando se crea un objeto de esta clase.
|
|
Public Sub Initialize
|
|
' No se requiere inicialización específica para esta clase en este momento.
|
|
End Sub
|
|
|
|
' Método principal que maneja las peticiones HTTP para el panel de administración.
|
|
' req: El objeto ServletRequest que contiene la información de la petición entrante.
|
|
' resp: El objeto ServletResponse para construir y enviar la respuesta al cliente.
|
|
' Módulo de clase: Manager
|
|
' ... (tu código de Class_Globals e Initialize se queda igual) ...
|
|
|
|
' Método principal que maneja las peticiones HTTP para el panel de administración.
|
|
' Refactorizado para funcionar como una API con un frontend estático.
|
|
Sub Handle(req As ServletRequest, resp As ServletResponse)
|
|
' --- 1. Bloque de Seguridad (sin cambios) ---
|
|
If req.GetSession.GetAttribute2("user_is_authorized", False) = False Then
|
|
resp.SendRedirect("/login")
|
|
Return
|
|
End If
|
|
|
|
Dim Command As String = req.GetParameter("command")
|
|
|
|
' --- 2. Servidor de la Página Principal ---
|
|
' Si NO se especifica un comando, servimos la página principal del manager desde la carpeta 'www'.
|
|
If Command = "" Then
|
|
Try
|
|
resp.ContentType = "text/html; charset=utf-8"
|
|
resp.Write(File.ReadString(File.DirApp, "www/manager.html"))
|
|
Catch
|
|
resp.SendError(500, "Error: No se pudo encontrar el archivo principal del panel (www/manager.html). " & LastException.Message)
|
|
End Try
|
|
Return
|
|
End If
|
|
|
|
' --- 3. Manejo de Comandos como API ---
|
|
' La variable 'j' (JSONGenerator) está en Class_Globals
|
|
|
|
Select Command.ToLowerCase
|
|
|
|
' --- Comandos que devuelven JSON ---
|
|
Case "getstats"
|
|
resp.ContentType = "application/json; charset=utf-8"
|
|
Dim allPoolStats As Map
|
|
allPoolStats.Initialize
|
|
|
|
For Each dbKey As String In Main.listaDeCP
|
|
Dim connector As RDCConnector = Main.Connectors.Get(dbKey)
|
|
If connector.IsInitialized Then
|
|
allPoolStats.Put(dbKey, connector.GetPoolStats)
|
|
Else
|
|
allPoolStats.Put(dbKey, CreateMap("Error": "Conector no inicializado"))
|
|
End If
|
|
Next
|
|
|
|
j.Initialize(allPoolStats)
|
|
resp.Write(j.ToString)
|
|
Return
|
|
|
|
Case "slowqueries"
|
|
resp.ContentType = "application/json; charset=utf-8"
|
|
Dim results As List
|
|
results.Initialize
|
|
Try
|
|
' Verificamos si la tabla de logs existe antes de consultarla
|
|
Dim tableExists As Boolean = Main.SQL1.ExecQuerySingleResult($"SELECT name FROM sqlite_master WHERE type='table' AND name='query_logs';"$) <> Null
|
|
If tableExists = False Then
|
|
' Si la tabla no existe, devolvemos un JSON con un mensaje claro y terminamos.
|
|
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
|
|
|
|
' La tabla existe, procedemos con la consulta original
|
|
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
|
|
|
|
' 1. Creamos un mapa "raíz" para contener nuestra lista.
|
|
Dim root As Map
|
|
root.Initialize
|
|
root.Put("data", results) ' La llave puede ser lo que quieras, "data" es común.
|
|
|
|
' 2. Ahora sí, inicializamos el generador con el mapa raíz.
|
|
j.Initialize(root)
|
|
resp.Write(j.ToString)
|
|
|
|
Catch
|
|
Log("Error CRÍTICO al obtener queries lentas en Manager API: " & LastException.Message)
|
|
|
|
' <<< CORRECCIÓN AQUÍ >>>
|
|
' Se utiliza la propiedad .Status para asignar el código de error
|
|
resp.Status = 500 ' Internal Server Error
|
|
|
|
' 1. Creamos un mapa "raíz" para contener nuestra lista.
|
|
Dim root As Map
|
|
root.Initialize
|
|
root.Put("data", results) ' La llave puede ser lo que quieras, "data" es común.
|
|
|
|
' 2. Ahora sí, inicializamos el generador con el mapa raíz.
|
|
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 ORIGINAL: Se mantiene intacta toda la lógica de recarga >>>
|
|
' (Copiada y pegada directamente de tu código anterior)
|
|
sbTemp.Append($"Iniciando recarga de configuración (Hot-Swap) ($DateTime{DateTime.Now})"$).Append(" " & CRLF)
|
|
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 newConnectors As Map
|
|
newConnectors.Initialize
|
|
Dim oldConnectors As Map
|
|
Dim reloadSuccessful As Boolean = True
|
|
Main.MainConnectorsLock.RunMethod("lock", Null)
|
|
oldConnectors = Main.Connectors
|
|
Main.MainConnectorsLock.RunMethod("unlock", Null)
|
|
|
|
For Each dbKey As String In Main.listaDeCP
|
|
Try
|
|
Dim newRDC As RDCConnector
|
|
newRDC.Initialize(dbKey)
|
|
Dim enableLogsSetting As Int = newRDC.config.GetDefault("enableSQLiteLogs", 0)
|
|
Dim isEnabled As Boolean = (enableLogsSetting = 1)
|
|
newConnectors.Put(dbKey & "_LOG_STATE", isEnabled)
|
|
sbTemp.Append($" -> Logs de ${dbKey} activados: ${isEnabled}"$).Append(" " & CRLF)
|
|
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
|
|
End Try
|
|
Next
|
|
|
|
If reloadSuccessful Then
|
|
Main.MainConnectorsLock.RunMethod("lock", Null)
|
|
Main.Connectors = newConnectors
|
|
Main.MainConnectorsLock.RunMethod("unlock", Null)
|
|
Main.SQLiteLoggingStatusByDB.Clear
|
|
Dim isAnyEnabled As Boolean = False
|
|
For Each dbKey As String In Main.listaDeCP
|
|
Dim isEnabled As Boolean = newConnectors.Get(dbKey & "_LOG_STATE")
|
|
Main.SQLiteLoggingStatusByDB.Put(dbKey, isEnabled)
|
|
If isEnabled Then isAnyEnabled = True
|
|
Next
|
|
Main.IsAnySQLiteLoggingEnabled = isAnyEnabled
|
|
If Main.IsAnySQLiteLoggingEnabled Then
|
|
Main.timerLogs.Enabled = True
|
|
sbTemp.Append($" -> Logs de SQLite HABILITADOS (Granular). Timer de limpieza ACTIVADO."$).Append(" " & CRLF)
|
|
Else
|
|
Main.timerLogs.Enabled = False
|
|
sbTemp.Append($" -> Logs de SQLite DESHABILITADOS (Total). Timer de limpieza PERMANECERÁ DETENIDO."$).Append(" " & CRLF)
|
|
End If
|
|
sbTemp.Append($"¡Recarga de configuración completada con éxito (Hot-Swap)!"$).Append(" " & CRLF)
|
|
Else
|
|
If oldTimerState Then
|
|
Main.timerLogs.Enabled = True
|
|
sbTemp.Append(" -> Restaurando Timer de limpieza de logs (SQLite) 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
|
|
|
|
' <<< CAMBIO: Se devuelve el contenido del StringBuilder como texto plano >>>
|
|
resp.Write(sbTemp.ToString)
|
|
Return
|
|
|
|
Case "test"
|
|
resp.ContentType = "text/plain; charset=utf-8"
|
|
Dim sb As StringBuilder
|
|
sb.Initialize
|
|
Try
|
|
Dim con As SQL = Main.Connectors.Get("DB1").As(RDCConnector).GetConnection("")
|
|
sb.Append("Connection successful." & CRLF & CRLF)
|
|
Dim estaDB As String = ""
|
|
Log(Main.listaDeCP)
|
|
For i = 0 To Main.listaDeCP.Size - 1
|
|
If Main.listaDeCP.get(i) <> "" Then estaDB = "." & Main.listaDeCP.get(i)
|
|
sb.Append($"Using config${estaDB}.properties"$ & CRLF)
|
|
Next
|
|
con.Close
|
|
resp.Write(sb.ToString)
|
|
Catch
|
|
resp.Write("Error fetching connection: " & LastException.Message)
|
|
End Try
|
|
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"
|
|
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
|
|
|
|
|
|
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)
|
|
|
|
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 Else
|
|
resp.ContentType = "text/plain; charset=utf-8"
|
|
resp.SendError(404, $"Comando desconocido: '{Command}'"$)
|
|
Return
|
|
|
|
End Select
|
|
End Sub |