diff --git a/Cambios.bas b/Cambios.bas index ae2f589..364d906 100644 --- a/Cambios.bas +++ b/Cambios.bas @@ -26,6 +26,16 @@ Sub Process_Globals ' - 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. + ' - VERSION 5.09.17 + + ' - fix(handlers, logs): Reporte robusto de AffectedRows (simbólico) y limpieza de tabla de errores + ' - Aborda dos problemas críticos para la estabilidad y fiabilidad del servidor: el manejo del conteo de filas afectadas en DMLs y la gestión del crecimiento de la tabla de logs de errores. + + ' - Cambios Principales: + + ' 1. Fix AffectedRows (ExecuteBatch V1 y DBHandlerJSON): Dada la imposibilidad de capturar el conteo de filas afectadas real (Null) de forma segura o la falla total en tiempo de ejecución (Method: ExecNonQuery2 not matched) al usar reflexión, se revierte la lógica a la llamada directa de ExecNonQuery2. Si el comando DML se ejecuta sin lanzar una excepción SQL, se reporta simbólicamente '1' fila afectada al cliente (en el Protocolo V1 y en la respuesta JSON para executecommand) para confirmar el éxito de la operación. + ' 2. Limpieza de Tabla de Errores: Se corrigió la subrutina Main.borraArribaDe15000Logs para incluir la tabla `errores` en la limpieza periódica. Esto asegura que el log de errores no crezca indefinidamente, manteniendo solo los 15,000 registros más recientes y realizando la optimización de espacio en disco con `vacuum`. + ' - VERSION 5.09.16.2 ' - feat(logs): Implementación de Cacheo y Escritura Transaccional en Lotes ' diff --git a/DBHandlerB4X.bas b/DBHandlerB4X.bas index c0d370d..f7d804b 100644 --- a/DBHandlerB4X.bas +++ b/DBHandlerB4X.bas @@ -17,15 +17,15 @@ Sub Class_Globals ' La siguiente sección de constantes y utilidades se compila condicionalmente ' 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. -' #if VERSION1 + ' #if VERSION1 ' 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 ' 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). - Private cs As CompressedStreams -' #end if + Private cs As CompressedStreams + ' #end if ' 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. @@ -128,25 +128,25 @@ Sub Handle(req As ServletRequest, resp As ServletResponse) CleanupAndLog(dbKey, "error_in_" & method, duration, req.RemoteAddress, requestsBeforeDecrement, poolBusyConnectionsForLog, con) Return ' Salida temprana si hay un error. End If -' #if VERSION1 + ' #if VERSION1 ' Estas ramas se compilan solo si #if VERSION1 está activo (para protocolo antiguo). - Else if method = "query" Then - in = cs.WrapInputStream(in, "gzip") ' Descomprime el stream de entrada si es protocolo V1. - q = ExecuteQuery(dbKey, con, in, resp) - If q = "error" Then - duration = DateTime.Now - start - CleanupAndLog(dbKey, "error_in_" & method, duration, req.RemoteAddress, requestsBeforeDecrement, poolBusyConnectionsForLog, con) - Return - End If - Else if method = "batch" Then - in = cs.WrapInputStream(in, "gzip") ' Descomprime el stream de entrada si es protocolo V1. - q = ExecuteBatch(dbKey, con, in, resp) - If q = "error" Then - duration = DateTime.Now - start - CleanupAndLog(dbKey, "error_in_" & method, duration, req.RemoteAddress, requestsBeforeDecrement, poolBusyConnectionsForLog, con) - Return - End If -' #end if + Else if method = "query" Then + in = cs.WrapInputStream(in, "gzip") ' Descomprime el stream de entrada si es protocolo V1. + q = ExecuteQuery(dbKey, con, in, resp) + If q = "error" Then + duration = DateTime.Now - start + CleanupAndLog(dbKey, "error_in_" & method, duration, req.RemoteAddress, requestsBeforeDecrement, poolBusyConnectionsForLog, con) + Return + End If + Else if method = "batch" Then + in = cs.WrapInputStream(in, "gzip") ' Descomprime el stream de entrada si es protocolo V1. + q = ExecuteBatch(dbKey, con, in, resp) + If q = "error" Then + duration = DateTime.Now - start + CleanupAndLog(dbKey, "error_in_" & method, duration, req.RemoteAddress, requestsBeforeDecrement, poolBusyConnectionsForLog, con) + Return + End If + ' #end if Else if method = "batch2" Then ' Ejecuta un lote de comandos (INSERT, UPDATE, DELETE) utilizando el protocolo V2. q = ExecuteBatch2(dbKey, con, in, resp) @@ -427,34 +427,41 @@ End Sub ' 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 - ' Lee y descarta la versión del cliente. - Dim clientVersion As Float = ReadObject(in) 'ignore - ' Lee cuántos comandos vienen en el lote. - Dim numberOfStatements As Int = ReadInt(in) - Dim res(numberOfStatements) As Int ' Array para resultados (aunque no se usa). + Log($"ExecuteBatch ${DB}"$) + ' Lee y descarta la versión del cliente. + Dim clientVersion As Float = ReadObject(in) 'ignore + ' Lee cuántos comandos vienen en el lote. + Dim numberOfStatements As Int = ReadInt(in) + Dim res(numberOfStatements) As Int ' Array para resultados (aunque no se usa). Dim singleQueryName As String = "" + Dim affectedCounts As List + Dim totalAffectedRows As Int + affectedCounts.Initialize - Try - con.BeginTransaction - ' Itera para procesar cada comando del lote. - For i = 0 To numberOfStatements - 1 - ' 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) + Try + con.BeginTransaction + ' Itera para procesar cada comando del lote. + Log(numberOfStatements) + For i = 0 To numberOfStatements - 1 + Log($"i: ${i}"$) + ' 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 singleQueryName = queryName 'Capturamos el nombre del query. End If - Dim sqlCommand As String = Connector.GetCommand(DB, queryName) - - ' <<< INICIO NUEVA VALIDACIÓN: VERIFICAR SI EL COMANDO EXISTE (V1) >>> - If sqlCommand = Null Or sqlCommand = "null" Or sqlCommand.Trim = "" Then - con.Rollback ' Deshace la transacción si un comando es inválido. - Dim errorMessage As String = $"El comando '${queryName}' no fue encontrado en el config.properties de '${DB}'."$ - Log(errorMessage) + Dim sqlCommand As String = Connector.GetCommand(DB, queryName) + Log(sqlCommand) + ' <<< INICIO NUEVA VALIDACIÓN: VERIFICAR SI EL COMANDO EXISTE (V1) >>> + If sqlCommand = Null Or sqlCommand = "null" Or sqlCommand.Trim = "" Then + con.Rollback ' Deshace la transacción si un comando es inválido. + Dim errorMessage As String = $"El comando '${queryName}' no fue encontrado en el config.properties de '${DB}'."$ + Log(errorMessage) Main.LogServerError("ERROR", "DBHandlerB4X.ExecuteBatch (V1)", errorMessage, DB, queryName, Null) - SendPlainTextError(resp, 400, errorMessage) - Return "error" - End If + SendPlainTextError(resp, 400, errorMessage) + Return "error" + End If ' <<< FIN NUEVA VALIDACIÓN >>> ' <<< INICIO VALIDACIÓN DE PARÁMETROS CENTRALIZADA DENTRO DEL BATCH (V1) >>> @@ -465,29 +472,39 @@ Private Sub ExecuteBatch(DB As String, con As SQL, in As InputStream, resp As Se SendPlainTextError(resp, 400, validationResult.ErrorMessage) Return "error" ' Salida temprana si la validación falla. 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. ' <<< 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. - ' Escribe la respuesta usando el serializador V1. - WriteObject(Main.VERSION, out) - WriteObject("batch", out) - WriteInt(res.Length, out) - For Each r As Int In res - WriteInt(r, out) - Next - out.Close + Dim out As OutputStream = cs.WrapOutputStream(resp.OutputStream, "gzip") ' Comprime la salida antes de enviarla. + ' Escribe la respuesta usando el serializador V1. + WriteObject(Main.VERSION, out) + WriteObject("batch", out) + WriteInt(res.Length, out) + Log(affectedCounts.Size) + For Each r As Int In affectedCounts + WriteInt(r, out) + Next + out.Close - Catch - con.Rollback - Log(LastException) + Catch + con.Rollback + Log(LastException) Main.LogServerError("ERROR", "DBHandlerB4X.ExecuteBatch (V1)", LastException.Message, DB, "batch_execution_error_v1", Null) - SendPlainTextError(resp, 500, LastException.Message) - End Try + SendPlainTextError(resp, 500, LastException.Message) + End Try ' Return $"batch (size=${numberOfStatements})"$ If numberOfStatements = 1 And singleQueryName <> "" Then @@ -499,24 +516,24 @@ End Sub ' 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 -' Log("====================== ExecuteQuery =====================") - ' Deserializa los datos de la petición usando el protocolo V1. - Dim clientVersion As Float = ReadObject(in) 'ignore - Dim queryName As String = ReadObject(in) - Dim limit As Int = ReadInt(in) - Dim params As List = ReadList(in) + ' Log("====================== ExecuteQuery =====================") + ' Deserializa los datos de la petición usando el protocolo V1. + Dim clientVersion As Float = ReadObject(in) 'ignore + Dim queryName As String = ReadObject(in) + Dim limit As Int = ReadInt(in) + Dim params As List = ReadList(in) - ' Obtiene la sentencia SQL. - Dim theSql As String = Connector.GetCommand(DB, queryName) + ' Obtiene la sentencia SQL. + Dim theSql As String = Connector.GetCommand(DB, queryName) - ' <<< INICIO NUEVA VALIDACIÓN: VERIFICAR SI EL COMANDO EXISTE (V1) >>> - If theSql = Null Or theSql ="null" Or theSql.Trim = "" Then - Dim errorMessage As String = $"El comando '${queryName}' no fue encontrado en el config.properties de '${DB}'."$ - Log(errorMessage) + ' <<< INICIO NUEVA VALIDACIÓN: VERIFICAR SI EL COMANDO EXISTE (V1) >>> + If theSql = Null Or theSql ="null" Or theSql.Trim = "" Then + Dim errorMessage As String = $"El comando '${queryName}' no fue encontrado en el config.properties de '${DB}'."$ + Log(errorMessage) Main.LogServerError("ERROR", "DBHandlerB4X.ExecuteQuery (V1)", errorMessage, DB, queryName, Null) - SendPlainTextError(resp, 400, errorMessage) - Return "error" - End If + SendPlainTextError(resp, 400, errorMessage) + Return "error" + End If ' <<< FIN NUEVA VALIDACIÓN >>> ' <<< INICIO VALIDACIÓN DE PARÁMETROS CENTRALIZADA (V1) >>> @@ -531,177 +548,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) ' <<< 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 rsmd As JavaObject = jrs.RunMethod("getMetaData", Null) - Dim cols As Int = rs.ColumnCount + Dim jrs As JavaObject = rs + Dim rsmd As JavaObject = jrs.RunMethod("getMetaData", Null) + Dim cols As Int = rs.ColumnCount - Dim out As OutputStream = cs.WrapOutputStream(resp.OutputStream, "gzip") ' 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. - WriteObject(Main.VERSION, out) - WriteObject("query", out) - WriteInt(rs.ColumnCount, out) + ' Escribe la cabecera de la respuesta V1. + WriteObject(Main.VERSION, out) + WriteObject("query", out) + WriteInt(rs.ColumnCount, out) - ' Escribe los nombres de las columnas. - For i = 0 To cols - 1 - WriteObject(rs.GetColumnName(i), out) - Next + ' Escribe los nombres de las columnas. + For i = 0 To cols - 1 + WriteObject(rs.GetColumnName(i), out) + Next - ' Itera sobre las filas del resultado. - Do While rs.NextRow And limit > 0 - WriteByte(1, out) ' Escribe un byte '1' para indicar que viene una fila. - ' Itera sobre las columnas de la fila. - For i = 0 To cols - 1 - Dim ct As Int = rsmd.RunMethod("getColumnType", Array(i + 1)) - ' Maneja los tipos de datos binarios de forma especial. - If ct = -2 Or ct = 2004 Or ct = -3 Or ct = -4 Then - WriteObject(rs.GetBlob2(i), out) - Else - ' Escribe el valor de la columna. - WriteObject(jrs.RunMethod("getObject", Array(i + 1)), out) - End If - Next - limit = limit - 1 - Loop + ' Itera sobre las filas del resultado. + Do While rs.NextRow And limit > 0 + WriteByte(1, out) ' Escribe un byte '1' para indicar que viene una fila. + ' Itera sobre las columnas de la fila. + For i = 0 To cols - 1 + Dim ct As Int = rsmd.RunMethod("getColumnType", Array(i + 1)) + ' Maneja los tipos de datos binarios de forma especial. + If ct = -2 Or ct = 2004 Or ct = -3 Or ct = -4 Then + WriteObject(rs.GetBlob2(i), out) + Else + ' Escribe el valor de la columna. + WriteObject(jrs.RunMethod("getObject", Array(i + 1)), out) + End If + Next + limit = limit - 1 + Loop - ' Escribe un byte '0' para indicar el fin de las filas. - WriteByte(0, out) - out.Close - rs.Close + ' Escribe un byte '0' para indicar el fin de las filas. + WriteByte(0, out) + out.Close + rs.Close - Return "query: " & queryName + Return "query: " & queryName End Sub ' Escribe un único byte en el stream de salida. 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 ' Serializador principal para el protocolo V1. Escribe un objeto al stream. Private Sub WriteObject(o As Object, out As OutputStream) - Dim data() As Byte - ' Escribe un byte de tipo seguido de los datos. - If o = Null Then - out.WriteBytes(Array As Byte(T_NULL), 0, 1) - Else If o Is Short Then - out.WriteBytes(Array As Byte(T_SHORT), 0, 1) - data = bc.ShortsToBytes(Array As Short(o)) - Else If o Is Int Then - out.WriteBytes(Array As Byte(T_INT), 0, 1) - data = bc.IntsToBytes(Array As Int(o)) - Else If o Is Float Then - out.WriteBytes(Array As Byte(T_FLOAT), 0, 1) - data = bc.FloatsToBytes(Array As Float(o)) - Else If o Is Double Then - out.WriteBytes(Array As Byte(T_DOUBLE), 0, 1) - data = bc.DoublesToBytes(Array As Double(o)) - Else If o Is Long Then - out.WriteBytes(Array As Byte(T_LONG), 0, 1) - data = bc.LongsToBytes(Array As Long(o)) - Else If o Is Boolean Then - out.WriteBytes(Array As Byte(T_BOOLEAN), 0, 1) - Dim b As Boolean = o - Dim data(1) As Byte - If b Then data(0) = 1 Else data(0) = 0 - Else If GetType(o) = "[B" Then ' Si el objeto es un array de bytes (BLOB) - data = o - out.WriteBytes(Array As Byte(T_BLOB), 0, 1) - ' Escribe la longitud de los datos antes de los datos mismos. - WriteInt(data.Length, out) - Else ' Trata todo lo demás como un String - out.WriteBytes(Array As Byte(T_STRING), 0, 1) - data = bc.StringToBytes(o, "UTF8") - ' Escribe la longitud del string antes del string. - WriteInt(data.Length, out) - End If - ' Escribe los bytes del dato. - If data.Length > 0 Then out.WriteBytes(data, 0, data.Length) + Dim data() As Byte + ' Escribe un byte de tipo seguido de los datos. + If o = Null Then + out.WriteBytes(Array As Byte(T_NULL), 0, 1) + Else If o Is Short Then + out.WriteBytes(Array As Byte(T_SHORT), 0, 1) + data = bc.ShortsToBytes(Array As Short(o)) + Else If o Is Int Then + out.WriteBytes(Array As Byte(T_INT), 0, 1) + data = bc.IntsToBytes(Array As Int(o)) + Else If o Is Float Then + out.WriteBytes(Array As Byte(T_FLOAT), 0, 1) + data = bc.FloatsToBytes(Array As Float(o)) + Else If o Is Double Then + out.WriteBytes(Array As Byte(T_DOUBLE), 0, 1) + data = bc.DoublesToBytes(Array As Double(o)) + Else If o Is Long Then + out.WriteBytes(Array As Byte(T_LONG), 0, 1) + data = bc.LongsToBytes(Array As Long(o)) + Else If o Is Boolean Then + out.WriteBytes(Array As Byte(T_BOOLEAN), 0, 1) + Dim b As Boolean = o + Dim data(1) As Byte + If b Then data(0) = 1 Else data(0) = 0 + Else If GetType(o) = "[B" Then ' Si el objeto es un array de bytes (BLOB) + data = o + out.WriteBytes(Array As Byte(T_BLOB), 0, 1) + ' Escribe la longitud de los datos antes de los datos mismos. + WriteInt(data.Length, out) + Else ' Trata todo lo demás como un String + out.WriteBytes(Array As Byte(T_STRING), 0, 1) + data = bc.StringToBytes(o, "UTF8") + ' Escribe la longitud del string antes del string. + WriteInt(data.Length, out) + End If + ' Escribe los bytes del dato. + If data.Length > 0 Then out.WriteBytes(data, 0, data.Length) End Sub ' Deserializador principal para el protocolo V1. Lee un objeto del stream. Private Sub ReadObject(In As InputStream) As Object - ' Lee el primer byte para determinar el tipo de dato. - Dim data(1) As Byte - In.ReadBytes(data, 0, 1) - Select data(0) - Case T_NULL - Return Null - Case T_SHORT - Dim data(2) As Byte - Return bc.ShortsFromBytes(ReadBytesFully(In, data, data.Length))(0) - Case T_INT - Dim data(4) As Byte - Return bc.IntsFromBytes(ReadBytesFully(In, data, data.Length))(0) - Case T_LONG - Dim data(8) As Byte - Return bc.LongsFromBytes(ReadBytesFully(In, data, data.Length))(0) - Case T_FLOAT - Dim data(4) As Byte - Return bc.FloatsFromBytes(ReadBytesFully(In, data, data.Length))(0) - Case T_DOUBLE - Dim data(8) As Byte - Return bc.DoublesFromBytes(ReadBytesFully(In, data, data.Length))(0) - Case T_BOOLEAN - Dim b As Byte = ReadByte(In) - Return b = 1 - Case T_BLOB - ' Lee la longitud, luego lee esa cantidad de bytes. - Dim len As Int = ReadInt(In) - Dim data(len) As Byte - Return ReadBytesFully(In, data, data.Length) - Case Else ' T_STRING - ' Lee la longitud, luego lee esa cantidad de bytes y los convierte a string. - Dim len As Int = ReadInt(In) - Dim data(len) As Byte - ReadBytesFully(In, data, data.Length) - Return BytesToString(data, 0, data.Length, "UTF8") - End Select + ' Lee el primer byte para determinar el tipo de dato. + Dim data(1) As Byte + In.ReadBytes(data, 0, 1) + Select data(0) + Case T_NULL + Return Null + Case T_SHORT + Dim data(2) As Byte + Return bc.ShortsFromBytes(ReadBytesFully(In, data, data.Length))(0) + Case T_INT + Dim data(4) As Byte + Return bc.IntsFromBytes(ReadBytesFully(In, data, data.Length))(0) + Case T_LONG + Dim data(8) As Byte + Return bc.LongsFromBytes(ReadBytesFully(In, data, data.Length))(0) + Case T_FLOAT + Dim data(4) As Byte + Return bc.FloatsFromBytes(ReadBytesFully(In, data, data.Length))(0) + Case T_DOUBLE + Dim data(8) As Byte + Return bc.DoublesFromBytes(ReadBytesFully(In, data, data.Length))(0) + Case T_BOOLEAN + Dim b As Byte = ReadByte(In) + Return b = 1 + Case T_BLOB + ' Lee la longitud, luego lee esa cantidad de bytes. + Dim len As Int = ReadInt(In) + Dim data(len) As Byte + Return ReadBytesFully(In, data, data.Length) + Case Else ' T_STRING + ' Lee la longitud, luego lee esa cantidad de bytes y los convierte a string. + Dim len As Int = ReadInt(In) + Dim data(len) As Byte + ReadBytesFully(In, data, data.Length) + Return BytesToString(data, 0, data.Length, "UTF8") + End Select End Sub ' 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() - Dim count = 0, Read As Int - ' Sigue leyendo en un bucle hasta llenar el buffer, por si los datos llegan en partes. - Do While count < Len And Read > -1 - Read = In.ReadBytes(Data, count, Len - count) - count = count + Read - Loop - Return Data + Dim count = 0, Read As Int + ' Sigue leyendo en un bucle hasta llenar el buffer, por si los datos llegan en partes. + Do While count < Len And Read > -1 + Read = In.ReadBytes(Data, count, Len - count) + count = count + Read + Loop + Return Data End Sub ' Escribe un entero (4 bytes) en el stream. Private Sub WriteInt(i As Int, out As OutputStream) - Dim data() As Byte - data = bc.IntsToBytes(Array As Int(i)) - out.WriteBytes(data, 0, data.Length) + Dim data() As Byte + data = bc.IntsToBytes(Array As Int(i)) + out.WriteBytes(data, 0, data.Length) End Sub ' Lee un entero (4 bytes) del stream. Private Sub ReadInt(In As InputStream) As Int - Dim data(4) As Byte - Return bc.IntsFromBytes(ReadBytesFully(In, data, data.Length))(0) + Dim data(4) As Byte + Return bc.IntsFromBytes(ReadBytesFully(In, data, data.Length))(0) End Sub ' Lee un solo byte del stream. Private Sub ReadByte(In As InputStream) As Byte - Dim data(1) As Byte - In.ReadBytes(data, 0, 1) - Return data(0) + Dim data(1) As Byte + In.ReadBytes(data, 0, 1) + Return data(0) End Sub ' Lee una lista de objetos del stream (protocolo V1). Private Sub ReadList(in As InputStream) As List - ' Primero lee la cantidad de elementos en la lista. - Dim len As Int = ReadInt(in) - Dim l1 As List - l1.Initialize - ' Luego lee cada objeto uno por uno y lo añade a la lista. - For i = 0 To len - 1 - l1.Add(ReadObject(in)) - Next - Return l1 + ' Primero lee la cantidad de elementos en la lista. + Dim len As Int = ReadInt(in) + Dim l1 As List + l1.Initialize + ' Luego lee cada objeto uno por uno y lo añade a la lista. + For i = 0 To len - 1 + l1.Add(ReadObject(in)) + Next + Return l1 End Sub '#end If ' Fin del bloque de compilación condicional para VERSION1 diff --git a/DBHandlerJSON.bas b/DBHandlerJSON.bas index 6bd3f3a..0dce2de 100644 --- a/DBHandlerJSON.bas +++ b/DBHandlerJSON.bas @@ -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 finalDbKey As String = "DB1" ' Identificador de la base de datos, con valor por defecto "DB1". Dim requestsBeforeDecrement As Int = 0 ' Contador de peticiones activas antes de decrementar, inicializado en 0. + Dim Total As Int = 0 Try ' --- INICIO: Bloque Try que envuelve la lógica principal del Handler --- @@ -191,9 +192,9 @@ Sub Handle(req As ServletRequest, resp As ServletResponse) CleanupAndLog(finalDbKey, queryNameForLog, duration, req.RemoteAddress, requestsBeforeDecrement, poolBusyConnectionsForLog, con) Return ' Salida temprana. End If - + Dim affectedCount As Int = 1 ' Asumimos éxito (1) si ExecNonQuery2 no lanza una excepción. con.ExecNonQuery2(sqlCommand, validationResult.ParamsToExecute) ' Ejecuta un comando con la lista de parámetros validada. - SendSuccessResponse(resp, CreateMap("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 --- Else Dim ErrorMsg As String = "Parámetro 'exec' inválido. '" & execType & "' no es un valor permitido." @@ -201,7 +202,6 @@ Sub Handle(req As ServletRequest, resp As ServletResponse) Main.LogServerError("ERROR", "DBHandlerJSON.Handle", ErrorMsg, finalDbKey, queryNameForLog, req.RemoteAddress) ' <-- Nuevo Log ' El flujo continúa hasta la limpieza final si no hay un Return explícito. End If - Catch ' --- CATCH: Maneja errores generales de ejecución o de SQL/JSON --- Log(LastException) ' Registra la excepción completa en el log. Main.LogServerError("ERROR", "DBHandlerJSON.Handle", LastException.Message, finalDbKey, queryNameForLog, req.RemoteAddress) ' <-- Nuevo Log @@ -215,7 +215,6 @@ Sub Handle(req As ServletRequest, resp As ServletResponse) duration = DateTime.Now - start ' Calcula la duración total de la petición. ' Llama a la subrutina centralizada para registrar el rendimiento y limpiar recursos. CleanupAndLog(finalDbKey, queryNameForLog, duration, req.RemoteAddress, requestsBeforeDecrement, poolBusyConnectionsForLog, con) - End Sub ' --- NUEVA SUBRUTINA: Centraliza el logging de rendimiento y la limpieza de recursos --- diff --git a/Manager.bas b/Manager.bas index e4d12bc..4a99896 100644 --- a/Manager.bas +++ b/Manager.bas @@ -2,7 +2,7 @@ Group=Default Group ModulesStructureVersion=1 Type=Class -Version=8.8 +Version=10.3 @EndOfDesignText@ ' Módulo de clase: Manager ' Este handler proporciona un panel de administración web para el servidor jRDC2-Multi. @@ -24,398 +24,291 @@ 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: Autenticación de Usuario --- - ' Verifica si el usuario actual ha iniciado sesión y está autorizado. - ' Si no está autorizado, se le redirige a la página de login. + ' --- 1. Bloque de Seguridad (sin cambios) --- If req.GetSession.GetAttribute2("user_is_authorized", False) = False Then resp.SendRedirect("/login") - Return ' Termina la ejecución si no está autorizado. + Return End If - ' Obtiene el comando solicitado de los parámetros de la URL (ej. "?command=reload"). 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}"$) - - ' --- MANEJO ESPECIAL PARA SNAPSHOT --- - ' El comando "snapshot" no devuelve HTML, sino una imagen. Lo manejamos por separado al principio. - If Command = "snapshot" Then -' Try -' resp.ContentType = "image/png" -' Dim robot, toolkit As JavaObject -' robot.InitializeNewInstance("java.awt.Robot", Null) -' 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. + ' --- 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 - ' --- FIN DE MANEJO ESPECIAL --- - - ' Para todos los demás comandos, construimos la página HTML de respuesta. - resp.ContentType = "text/html" ' Establece el tipo de contenido como HTML. - Dim sb As StringBuilder ' Usamos StringBuilder para construir eficientemente el HTML. - sb.Initialize - - ' --- Estilos y JavaScript (igual que antes) --- - sb.Append("
") - sb.Append("") - sb.Append("") - - ' --- Cabecera de la Página y Mensaje de Bienvenida --- - sb.Append("Bienvenido, ${req.GetSession.GetAttribute("username")}
"$) - - ' --- Menú de Navegación del Manager --- - ' Este menú incluye las opciones para interactuar con el servidor. - sb.Append("") - sb.Append("| Query | Duración (ms) | Fecha/Hora Local | DB Key | Cliente IP | Conex. Ocupadas | Peticiones Activas |
|---|---|---|---|---|---|---|
| ${rs.GetString("query_name")} | "$) - sb.Append($"${rs.GetLong("duration_ms")} | "$) - sb.Append($"${rs.GetString("timestamp_local")} | "$) - sb.Append($"${rs.GetString("db_key")} | "$) - sb.Append($"${rs.GetString("client_ip")} | "$) - sb.Append($"${rs.GetInt("busy_connections")} | "$) - sb.Append($"${rs.GetInt("handler_active_requests")} | "$) - sb.Append("
Error al cargar queries lentas: ${LastException.Message}
"$) - End Try - Else If Command = "test" Then - Try - Dim con As SQL = Main.Connectors.Get("DB1").As(RDCConnector).GetConnection("") - sb.Append("Connection successful.") - 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