AppType=StandardJava Build1=Default,b4j.JRDCMulti File1=config.DB2.properties File2=config.DB3.properties File3=config.DB4.properties File4=config.properties File5=reiniciaProcesoBow.bat File6=reiniciaProcesoPM2.bat File7=start.bat File8=start2.bat File9=stop.bat FileGroup1=Default Group FileGroup2=Default Group FileGroup3=Default Group FileGroup4=Default Group FileGroup5=Default Group FileGroup6=Default Group FileGroup7=Default Group FileGroup8=Default Group FileGroup9=Default Group Group=Default Group Library1=bcrypt Library2=byteconverter Library3=javaobject Library4=jcore Library5=jrandomaccessfile Library6=jserver Library7=jshell Library8=json Library9=jsql Module1=Cambios Module10=Manager Module11=Manager0 Module12=ParameterValidationUtils Module13=ping Module14=RDCConnector Module15=SSE Module16=SSEHandler Module17=TestHandler Module2=ChangePassHandler Module3=DBHandlerB4X Module4=DBHandlerJSON Module5=DoLoginHandler Module6=faviconHandler Module7=GlobalParameters Module8=LoginHandler Module9=LogoutHandler NumberOfFiles=9 NumberOfLibraries=9 NumberOfModules=17 Version=10.3 @EndOfDesignText@ 'Non-UI application (console / server application) #Region Project Attributes #CommandLineArgs: #MergeLibraries: True ' VERSION 5.09.19 '########################################################################################################### '###################### PULL ############################################################# 'Ctrl + click ide://run?file=%WINDIR%\System32\cmd.exe&Args=/c&Args=git&Args=pull '########################################################################################################### '###################### PUSH ############################################################# 'Ctrl + click ide://run?file=%WINDIR%\System32\WindowsPowerShell\v1.0\powershell.exe&Args=github&Args=..\..\ '########################################################################################################### '###################### PUSH TORTOISE GIT ######################################################### 'Ctrl + click ide://run?file=%WINDIR%\System32\WindowsPowerShell\v1.0\powershell.exe&Args=TortoiseGitProc&Args=/command:commit&Args=/path:"../"&Args=/closeonend:2 '########################################################################################################### #End Region 'change based on the jdbc jar file '#AdditionalJar: mysql-connector-java-5.1.27-bin '#AdditionalJar: postgresql-42.7.0 #AdditionalJar: ojdbc11 ' Librería para manejar la base de datos SQLite #AdditionalJar: sqlite-jdbc-3.7.2 Sub Process_Globals ' --- Variables globales accesibles desde cualquier parte del proyecto --- ' Objeto principal del servidor HTTP de B4J. Public srvr As Server ' La versión actual de este servidor jRDC modificado. Public const VERSION As Float = 2.23 ' Tipos personalizados (clases) para la serialización y deserialización de datos Type DBCommand (Name As String, Parameters() As Object) Type DBResult (Tag As Object, Columns As Map, Rows As List) ' Contiene una lista de los identificadores de bases de datos configuradas (ej. "DB1", "DB2"). Public listaDeCP As List ' Una lista temporal para almacenar los nombres de archivos de configuración encontrados. Private cpFiles As List ' Mapas globales para gestionar los conectores de base de datos y los comandos SQL. Public Connectors, commandsMap As Map ' Objeto SQL para interactuar con la base de datos de usuarios y logs (SQLite). Public SQL1 As SQL ' Objeto para realizar operaciones de hashing de contraseñas de forma segura. Private bc As BCrypt ' Objeto de bloqueo (ReentrantLock) para proteger Main.Connectors durante Hot-Swap. Public MainConnectorsLock As JavaObject ' Timer para ejecutar tareas periódicas, como la limpieza de logs. Public timerLogs As Timer ' NUEVAS VARIABLES para control granular de logs ' Mapa para almacenar el estado de logging (True/False) por cada DBKey (DB1, DB2, etc.). Public SQLiteLoggingStatusByDB As Map ' Bandera global que indica si AL MENOS una base de datos tiene los logs habilitados. Public IsAnySQLiteLoggingEnabled As Boolean ' Tipo para encapsular el resultado de la validación de parámetros. Type ParameterValidationResult ( _ Success As Boolean, _ ErrorMessage As String, _ ParamsToExecute As List _ ' La lista de parámetros final a usar en la ejecución SQL ) Public QueryLogCache As List ' Cache para los logs de rendimiento (query_logs) Public ErrorLogCache As List ' Cache para los logs de errores y advertencias Public LOG_CACHE_THRESHOLD As Int = 350 ' Umbral de registros para forzar la escritura Dim logger As Boolean Public LatestPoolStats As Map ' Mapa Thread-Safe para almacenar las últimas métricas de cada pool. End Sub Sub AppStart (Args() As String) SSE.Initialize #if DEBUG logger = True LOG_CACHE_THRESHOLD = 10 #else logger = False #End If ' --- Subrutina principal que se ejecuta al iniciar la aplicación --- ' 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") QueryLogCache.Initialize ErrorLogCache.Initialize ' 1. Inicializa la base de datos local de usuarios (SQLite) y la tabla de logs. InitializeSQLiteDatabase ' 2. Inicializa los mapas globales definidos en GlobalParameters.bas. GlobalParameters.mpLogs.Initialize GlobalParameters.mpTotalRequests.Initialize GlobalParameters.mpTotalConnections.Initialize GlobalParameters.mpBlockConnection.Initialize ' Aseguramos que el mapa de conteo de peticiones activas sea thread-safe. GlobalParameters.ActiveRequestsCountByDB = srvr.CreateThreadSafeMap ' 3. Inicializa las estructuras principales del servidor HTTP. listaDeCP.Initialize srvr.Initialize("") Connectors = srvr.CreateThreadSafeMap commandsMap.Initialize LatestPoolStats = srvr.CreateThreadSafeMap ' Inicializar el mapa de estadísticas como Thread-Safe ' NUEVO: Inicializar el mapa de estado de logs granular SQLiteLoggingStatusByDB.Initialize ' Creamos una instancia de ReentrantLock para proteger Main.Connectors. MainConnectorsLock.InitializeNewInstance("java.util.concurrent.locks.ReentrantLock", Null) ' === 4. INICIALIZACIÓN DEL CONECTOR PARA LA BASE DE DATOS PRINCIPAL (DB1) === Try Dim con1 As RDCConnector con1.Initialize("DB1") Connectors.Put("DB1", con1) srvr.Port = con1.serverPort listaDeCP.Add("DB1") Log($"Main.AppStart: Conector 'DB1' inicializado exitosamente en puerto: ${srvr.Port}"$) ' Lógica de Logs para DB1 (Fuente principal de configuración) Dim enableLogsSetting As Int = con1.config.GetDefault("enableSQLiteLogs", 0).As(Int) Dim isEnabled As Boolean = (enableLogsSetting = 1) SQLiteLoggingStatusByDB.Put("DB1", isEnabled) ' Guardar el estado Catch Dim ErrorMsg As String = $"Main.AppStart: ERROR CRÍTICO al inicializar conector 'DB1': ${LastException.Message}"$ Log(ErrorMsg) LogServerError("ERROR", "Main.AppStart", ErrorMsg, "DB1", Null, Null) ExitApplication End Try ' === 5. DETECCIÓN E INICIALIZACIÓN DE BASES DE DATOS ADICIONALES (DB2, DB3, DB4) === cpFiles = File.ListFiles("./") If cpFiles.Size > 0 Then For i = 0 To cpFiles.Size - 1 ' Procesa 'config.DB2.properties' If cpFiles.Get(i) = "config.DB2.properties" Then Try Dim con2 As RDCConnector con2.Initialize("DB2") Connectors.Put("DB2", con2) listaDeCP.Add("DB2") Log("Main.AppStart: Conector 'DB2' inicializado exitosamente.") ' Lógica de Logs para DB2 Dim enableLogsSetting As Int = con2.config.GetDefault("enableSQLiteLogs", 0).As(Int) Dim isEnabled As Boolean = (enableLogsSetting = 1) SQLiteLoggingStatusByDB.Put("DB2", isEnabled) ' Guardar el estado Catch Dim ErrorMsg As String = $"Main.AppStart: ERROR al inicializar conector 'DB2': ${LastException.Message}"$ Log(ErrorMsg) LogServerError("ERROR", "Main.AppStart", ErrorMsg, "DB2", Null, Null) End Try End If ' Procesa 'config.DB3.properties' If cpFiles.Get(i) = "config.DB3.properties" Then Try Dim con3 As RDCConnector con3.Initialize("DB3") Connectors.Put("DB3", con3) listaDeCP.Add("DB3") Log("Main.AppStart: Conector 'DB3' inicializado exitosamente.") ' Lógica de Logs para DB3 Dim enableLogsSetting As Int = con3.config.GetDefault("enableSQLiteLogs", 0).As(Int) Dim isEnabled As Boolean = (enableLogsSetting = 1) SQLiteLoggingStatusByDB.Put("DB3", isEnabled) ' Guardar el estado Catch Dim ErrorMsg As String = $"Main.AppStart: ERROR al inicializar conector 'DB3': ${LastException.Message}"$ Log(ErrorMsg) LogServerError("ERROR", "Main.AppStart", ErrorMsg, "DB3", Null, Null) End Try End If ' Procesa 'config.DB4.properties' If cpFiles.Get(i) = "config.DB4.properties" Then Try Dim con4 As RDCConnector con4.Initialize("DB4") Connectors.Put("DB4", con4) listaDeCP.Add("DB4") Log("Main.AppStart: Conector 'DB4' inicializado exitosamente.") ' Lógica de Logs para DB4 Dim enableLogsSetting As Int = con4.config.GetDefault("enableSQLiteLogs", 0).As(Int) Dim isEnabled As Boolean = (enableLogsSetting = 1) SQLiteLoggingStatusByDB.Put("DB4", isEnabled) ' Guardar el estado Catch Dim ErrorMsg As String = $"Main.AppStart: ERROR al inicializar conector 'DB4': ${LastException.Message}"$ Log(ErrorMsg) LogServerError("ERROR", "Main.AppStart", ErrorMsg, "DB4", Null, Null) End Try End If Next Else Log("Main.AppStart: No se encontraron archivos de configuración adicionales (config.DBx.properties).") End If ' Log final de las bases de datos que el servidor está gestionando. Dim sbListaDeCP_Log As StringBuilder sbListaDeCP_Log.Initialize For Each item As String In listaDeCP sbListaDeCP_Log.Append(item).Append(", ") Next If sbListaDeCP_Log.Length > 0 Then sbListaDeCP_Log.Remove(sbListaDeCP_Log.Length - 2, sbListaDeCP_Log.Length) End If Log($"Main.AppStart: Bases de datos configuradas y listas: [${sbListaDeCP_Log.ToString}]"$) ' <<<< Bloque de inicialización del Timer para la limpieza de logs >>>> ' Inicialización INCONDICIONAL del Timer (Garantiza que el objeto exista y prevenga el IllegalStateException) timerLogs.Initialize("TimerLogs", 600000) ' 10 minutos = 600 * 1000 = 600000 ms ' CONTROL CONDICIONAL BASADO EN EL ESTADO GRANULAR IsAnySQLiteLoggingEnabled = False For Each dbStatus As Boolean In SQLiteLoggingStatusByDB.Values If dbStatus Then IsAnySQLiteLoggingEnabled = True Exit ' Si uno está activo, es suficiente para encender el Timer End If Next If IsAnySQLiteLoggingEnabled Then timerLogs.Enabled = True If logger Then Log("Main.AppStart: Timer de limpieza de logs ACTIVADO (al menos una DB requiere logs).") Else timerLogs.Enabled = False If logger Then Log("Main.AppStart: Timer de limpieza de logs DESHABILITADO (ninguna DB requiere logs).") End If ' <<<< Fin del bloque del Timer >>>> ' === 6. REGISTRO DE HANDLERS HTTP PARA EL SERVIDOR === srvr.AddHandler("/ping", "ping", False) srvr.AddHandler("/test", "TestHandler", False) srvr.AddHandler("/login", "LoginHandler", False) srvr.AddHandler("/dologin", "DoLoginHandler", False) srvr.AddHandler("/logout", "LogoutHandler", False) srvr.AddHandler("/changepass", "ChangePassHandler", False) srvr.AddHandler("/manager", "Manager", False) srvr.AddHandler("/DBJ", "DBHandlerJSON", False) srvr.AddHandler("/dbrquery", "DBHandlerJSON", False) srvr.AddHandler("/favicon.ico", "faviconHandler", False) srvr.AddHandler("/stats-stream", "SSEHandler", False) srvr.AddHandler("/*", "DBHandlerB4X", False) ' 7. Inicia el servidor HTTP. srvr.Start Log("===========================================================") Log($"-=== jRDC está funcionando en el puerto: ${srvr.Port} (versión = $1.2{VERSION}) ===-"$) Log("===========================================================") ' 8. Inicia el bucle de mensajes de B4J. StartMessageLoop End Sub ' --- Subrutina para inicializar la base de datos de usuarios local (SQLite) --- Sub InitializeSQLiteDatabase Dim dbFileName As String = "users.db" If File.Exists(File.DirApp, dbFileName) = False Then Log("Creando nueva base de datos de usuarios: " & dbFileName) SQL1.InitializeSQLite(File.DirApp, dbFileName, True) ' Crear tabla 'users' Dim createUserTable As String = "CREATE TABLE users (username TEXT PRIMARY KEY, password_hash TEXT NOT NULL)" SQL1.ExecNonQuery(createUserTable) ' Crear tabla 'query_logs' If logger Then Log("Creando tabla 'query_logs' con columnas de rendimiento.") Dim createQueryLogsTable As String = "CREATE TABLE query_logs (id INTEGER PRIMARY KEY AUTOINCREMENT, query_name TEXT, duration_ms INTEGER, timestamp INTEGER, db_key TEXT, client_ip TEXT, busy_connections INTEGER, handler_active_requests INTEGER)" SQL1.ExecNonQuery(createQueryLogsTable) SQL1.ExecNonQuery("PRAGMA journal_mode=WAL;") SQL1.ExecNonQuery("PRAGMA synchronous=NORMAL;") ' Insertar usuario por defecto Dim defaultUser As String = "admin" Dim defaultPass As String = "12345" Dim hashedPass As String = bc.hashpw(defaultPass, bc.gensalt) SQL1.ExecNonQuery2("INSERT INTO users (username, password_hash) VALUES (?, ?)", Array As Object(defaultUser, hashedPass)) Log($"Usuario por defecto creado -> user: ${defaultUser}, pass: ${defaultPass}"$) ' Crear tabla 'errores' 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)" 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 SQL1.InitializeSQLite(File.DirApp, dbFileName, True) 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 <<< If logger Then Log("Verificando y migrando tabla 'query_logs' si es necesario.") If SQL1.ExecQuerySingleResult("SELECT name FROM sqlite_master WHERE type='table' AND name='query_logs'") = Null Then If logger Then Log("Tabla 'query_logs' no encontrada, creándola con columnas de rendimiento.") Dim createQueryLogsTable As String = "CREATE TABLE query_logs (id INTEGER PRIMARY KEY AUTOINCREMENT, query_name TEXT, duration_ms INTEGER, timestamp INTEGER, db_key TEXT, client_ip TEXT, busy_connections INTEGER, handler_active_requests INTEGER)" SQL1.ExecNonQuery(createQueryLogsTable) Else ' Si la tabla query_logs ya existe, entonces verificamos y añadimos las columnas faltantes (busy_connections, handler_active_requests). Dim columnExists As Boolean Dim rs As ResultSet ' --- VERIFICAR Y AÑADIR busy_connections --- columnExists = False rs = SQL1.ExecQuery("PRAGMA table_info(query_logs)") Do While rs.NextRow If rs.GetString("name").EqualsIgnoreCase("busy_connections") Then columnExists = True Exit ' La columna ya existe, salimos del bucle. End If Loop rs.Close If columnExists = False Then If logger Then Log("Añadiendo columna 'busy_connections' a query_logs.") SQL1.ExecNonQuery("ALTER TABLE query_logs ADD COLUMN busy_connections INTEGER DEFAULT 0") End If ' --- VERIFICAR Y AÑADIR handler_active_requests --- columnExists = False rs = SQL1.ExecQuery("PRAGMA table_info(query_logs)") Do While rs.NextRow If rs.GetString("name").EqualsIgnoreCase("handler_active_requests") Then columnExists = True Exit ' La columna ya existe, salimos del bucle. End If Loop rs.Close If columnExists = False Then If logger Then Log("Añadiendo columna 'handler_active_requests' a query_logs.") SQL1.ExecNonQuery("ALTER TABLE query_logs ADD COLUMN handler_active_requests INTEGER DEFAULT 0") End If columnExists = False rs = SQL1.ExecQuery("PRAGMA table_info(query_logs)") Do While rs.NextRow If rs.GetString("name").EqualsIgnoreCase("timestamp_text_local") Then columnExists = True Exit ' La columna ya existe, salimos del bucle. End If Loop rs.Close If columnExists = False Then If logger Then Log("Añadiendo columna 'timestamp_text_local' a query_logs.") ' Usamos 'TEXT' para almacenar la cadena de fecha/hora formateada. SQL1.ExecNonQuery("ALTER TABLE query_logs ADD COLUMN timestamp_text_local TEXT") End If ' >>> INICIO: Lógica de migración para 'errores' si la DB ya existía <<< If logger Then Log("Verificando y migrando tabla 'errores' si es necesario.") If SQL1.ExecQuerySingleResult("SELECT name FROM sqlite_master WHERE type='table' AND name='errores'") = Null Then If logger Then Log("Tabla 'errores' no encontrada, creándola.") Dim createErrorsTable As String = "CREATE TABLE errores (id INTEGER PRIMARY KEY AUTOINCREMENT, timestamp INTEGER, type TEXT, source TEXT, message TEXT, db_key TEXT, command_name TEXT, client_ip TEXT)" SQL1.ExecNonQuery(createErrorsTable) Else If logger Then Log("Tabla 'errores' ya existe.") End If ' >>> FIN: Lógica de migración para 'errores' <<< End If ' >>> FIN: Lógica de migración (ALTER TABLE) <<< End If End Sub Public Sub LogQueryPerformance(QueryName As String, DurationMs As Long, DbKey As String, ClientIp As String, HandlerActiveRequests As Int, PoolBusyConnections As Int) Dim isEnabled As Boolean = SQLiteLoggingStatusByDB.GetDefault(DbKey, False) If isEnabled Then ' Formato de tiempo necesario para la columna timestamp_text_local DateTime.DateFormat = "yyyy-MM-dd HH:mm:ss.SSS" Dim formattedTimestamp As String = DateTime.Date(DateTime.Now) ' 1. Crear el mapa de datos (log entry) Dim logEntry As Map = CreateMap("query_name": QueryName, "duration_ms": DurationMs, "timestamp": DateTime.Now, _ "db_key": DbKey, "client_ip": ClientIp, "busy_connections": PoolBusyConnections, _ "handler_active_requests": HandlerActiveRequests, "timestamp_text_local": formattedTimestamp) ' 2. Zona Crítica: Añadir a la caché y verificar el umbral Dim shouldWriteBatch As Boolean = False ' Usamos el lock global para garantizar que la adición y la verificación del tamaño sean atómicas. MainConnectorsLock.RunMethod("lock", Null) QueryLogCache.Add(logEntry) If QueryLogCache.Size >= LOG_CACHE_THRESHOLD Then shouldWriteBatch = True End If MainConnectorsLock.RunMethod("unlock", Null) ' 3. Si se alcanzó el umbral, disparamos la escritura. ' NO DEBE HACERSE CON EL LOCK PUESTO. If shouldWriteBatch Then CallSub(Me, "WriteQueryLogsBatch") End If End If End Sub ' --- Subrutina para registrar errores y advertencias en la tabla 'errores'. --- Public Sub LogServerError(Type0 As String, Source As String, Message As String, DBKey As String, CommandName As String, ClientIp As String) Dim isEnabled As Boolean = SQLiteLoggingStatusByDB.GetDefault(DBKey, False) If isEnabled Then ' Log($"[DEBUG CACHE] Se recibió log de error/advertencia para: ${CommandName}"$) '<--- Nuevo Log 1 Dim logEntry As Map = CreateMap("timestamp": DateTime.Now, "type": Type0, "source": Source, "message": Message, _ "db_key": DBKey, "command_name": CommandName, "client_ip": ClientIp) Dim shouldWriteBatch As Boolean = False ' 1. Zona Crítica: Añadir a la caché y verificar el umbral ' Usamos el lock para Thread Safety MainConnectorsLock.RunMethod("lock", Null) ' Log($"[DEBUG CACHE] Lock adquirido. Tamaño actual de ErrorLogCache: ${ErrorLogCache.Size}"$) '<--- Nuevo Log 2 ErrorLogCache.Add(logEntry) ' Log($"[DEBUG CACHE] Log añadido. Nuevo tamaño: ${ErrorLogCache.Size}. Umbral: ${LOG_CACHE_THRESHOLD}"$) '<--- Nuevo Log 3 If ErrorLogCache.Size >= LOG_CACHE_THRESHOLD Then shouldWriteBatch = True ' Log(">>> [DEBUG CACHE] UMBRAL ALCANZADO. DISPARANDO ESCRITURA BATCH. <<<") '<--- Nuevo Log 4 End If MainConnectorsLock.RunMethod("unlock", Null) ' Log($"[DEBUG CACHE] Lock liberado."$) '<--- Nuevo Log 5 ' 2. Si se alcanzó el umbral (o si el timer lo llama), disparamos la escritura. If shouldWriteBatch Then CallSub(Me, "WriteErrorLogsBatch") End If Else ' Log($"[DEBUG CACHE] Logging deshabilitado para DBKey: ${DBKey}. Log de error omitido."$) End If End Sub Public Sub WriteQueryLogsBatch Dim logsToWrite As List logsToWrite.Initialize ' 1. Inicializar la lista local (CRÍTICO) ' === PASO 1: Intercambio Atómico de Caché (Protegido por ReentrantLock) === MainConnectorsLock.RunMethod("lock", Null) If QueryLogCache.Size = 0 Then MainConnectorsLock.RunMethod("unlock", Null) ' Log("[DEBUG BATCH-Q] Saliendo: Caché de rendimiento vacía.") Return End If ' *** CORRECCIÓN CRÍTICA: Copia de contenido (AddAll) en lugar de referencia. *** logsToWrite.AddAll(QueryLogCache) Dim batchSize As Int = logsToWrite.Size ' Vaciamos la caché global. logsToWrite ahora contiene la copia de los elementos. QueryLogCache.Initialize MainConnectorsLock.RunMethod("unlock", Null) ' If logger Then Log($"[LOG BATCH] Iniciando escritura transaccional de ${batchSize} logs de rendimiento. Logs copiados: ${logsToWrite.Size}"$) ' === PASO 2: Escritura Transaccional a SQLite === Try ' 1. Iniciar la transacción: Todo lo que siga es una única operación de disco. SQL1.BeginTransaction For Each logEntry As Map In logsToWrite SQL1.ExecNonQuery2("INSERT INTO query_logs (query_name, duration_ms, timestamp, db_key, client_ip, busy_connections, handler_active_requests, timestamp_text_local) VALUES (?, ?, ?, ?, ?, ?, ?, ?)", _ Array As Object(logEntry.Get("query_name"), logEntry.Get("duration_ms"), logEntry.Get("timestamp"), logEntry.Get("db_key"), _ logEntry.Get("client_ip"), logEntry.Get("busy_connections"), logEntry.Get("handler_active_requests"), _ logEntry.Get("timestamp_text_local"))) Next ' 2. Finalizar la transacción: Escritura eficiente a disco. SQL1.TransactionSuccessful If logger Then Log($"[LOG BATCH] Lote de ${batchSize} logs de rendimiento escrito exitosamente."$) Catch ' Si falla, deshacemos todos los logs del lote y registramos el fallo. SQL1.Rollback Dim ErrorMsg As String = "ERROR CRÍTICO: Fallo al escribir lote de logs de rendimiento en SQLite: " & LastException.Message Log(ErrorMsg) ' Usamos LogServerError para que el fallo quede registrado en la tabla 'errores' si el logging está habilitado. LogServerError("ERROR", "Main.WriteQueryLogsBatch", ErrorMsg, Null, "log_batch_write_performance", Null) End Try End Sub ' --- Subrutina de evento para el Timer 'timerLogs'. --- ' El estado 'Enabled' del Timer ya está controlado por IsAnySQLiteLoggingEnabled en AppStart y Manager. Sub TimerLogs_Tick Try ' 1. Vaciado de logs de rendimiento (asumiendo que WriteQueryLogsBatch también fue implementado) WriteQueryLogsBatch ' 2. Vaciado de logs de errores WriteErrorLogsBatch ' 3. Limpieza y VACUUM (esto ya verifica IsAnySQLiteLoggingEnabled [8]) borraArribaDe15000Logs Catch Dim ErrorMsg As String = "ERROR en TimerLogs_Tick al intentar borrar logs: " & LastException.Message Log(ErrorMsg) LogServerError("ERROR", "Main.TimerLogs_Tick", ErrorMsg, Null, "log_cleanup", Null) End Try End Sub Public Sub WriteErrorLogsBatch Dim logsToWrite As List logsToWrite.Initialize ' *** Aseguramos que logsToWrite sea una LISTA NUEVA y no dependa de la referencia. ' === PASO 1: Intercambio Atómico de Caché (Protegido por ReentrantLock) === MainConnectorsLock.RunMethod("lock", Null) ' Adquirimos el bloqueo. ' Log($"[DEBUG BATCH] Lock adquirido en WriteErrorLogsBatch. Caché Size: ${ErrorLogCache.Size}"$) If ErrorLogCache.Size = 0 Then MainConnectorsLock.RunMethod("unlock", Null) ' Log("[DEBUG BATCH] Saliendo: Caché vacía.") Return End If ' *** CORRECCIÓN CRÍTICA: Copiamos el CONTENIDO de forma atómica. *** logsToWrite.AddAll(ErrorLogCache) ' <--- ESTO PASA LOS 10 REGISTROS A LA NUEVA LISTA ' Vaciamos la caché global. logsToWrite AHORA ES INDEPENDIENTE. ErrorLogCache.Initialize MainConnectorsLock.RunMethod("unlock", Null) ' Liberamos el bloqueo. ' Usamos el tamaño de la lista *copiada*. Dim batchSize As Int = logsToWrite.Size If logger Then Log($"[LOG BATCH] Iniciando escritura transaccional de ${batchSize} logs de ERRORES a SQLite. Logs copiados: ${logsToWrite.Size}"$) ' === PASO 2: Escritura Transaccional a SQLite (Usa logsToWrite) === If batchSize = 0 Then Log("ADVERTENCIA: Fallo en la copia de la lista. logsToWrite está vacía. Abortando escritura.") Return End If Try ' 1. Iniciar la transacción. SQL1.BeginTransaction For Each logEntry As Map In logsToWrite ' ... (Tu lógica de SQL1.ExecNonQuery2 aquí) ... SQL1.ExecNonQuery2("INSERT INTO errores (timestamp, type, source, message, db_key, command_name, client_ip) VALUES (?, ?, ?, ?, ?, ?, ?)", _ Array As Object(logEntry.Get("timestamp"), logEntry.Get("type"), logEntry.Get("source"), logEntry.Get("message"), _ logEntry.Get("db_key"), logEntry.Get("command_name"), logEntry.Get("client_ip"))) Next ' 2. Confirmar la transacción. SQL1.TransactionSuccessful If logger Then Log($"[LOG BATCH] Lote de ${logsToWrite.Size} logs de ERRORES escrito exitosamente."$) Catch ' 3. Rollback si falla. SQL1.Rollback Dim ErrorMsg As String = "ERROR CRÍTICO: Fallo al escribir lote de logs de ERRORES en SQLite: " & LastException.Message Log(ErrorMsg) End Try End Sub ' --- Borra los registros más antiguos de la tabla 'query_logs' y hace VACUUM. --- Sub borraArribaDe15000Logs 'ignore 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.") 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;") Else ' 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.") End If 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