Refactor code structure for improved readability and maintainability

This commit is contained in:
Miguel 2025-08-27 18:08:57 +02:00
parent f68bea6056
commit 480e831b7a
8 changed files with 10208 additions and 449 deletions

82
app.py
View File

@ -42,8 +42,12 @@ websocket_connections = set()
# --- Globals for Tray Icon ---
tray_icon = None
# --- Log batching optimization ---
log_batch_buffer = []
log_batch_lock = threading.Lock()
batch_timer = None
# --- Parámetros para envío directo de logs (optimizado) ---
def _send_message_to_clients(message: str):
"""Envía un mensaje directamente a todas las conexiones WebSocket activas."""
if not websocket_connections:
@ -59,6 +63,29 @@ def _send_message_to_clients(message: str):
websocket_connections.difference_update(dead_connections)
def _flush_log_batch():
"""Envía todos los logs acumulados en el batch y limpia el buffer."""
global log_batch_buffer, batch_timer
with log_batch_lock:
if log_batch_buffer:
# Unir todos los logs del batch en un solo mensaje
batch_message = "".join(log_batch_buffer)
_send_message_to_clients(batch_message)
log_batch_buffer.clear()
batch_timer = None
def _schedule_batch_flush():
"""Programa el envío del batch de logs en 1 segundo si no está ya programado."""
global batch_timer
if batch_timer is None:
batch_timer = threading.Timer(1.0, _flush_log_batch)
batch_timer.start()
@sock.route("/ws")
def handle_websocket(ws):
try:
@ -73,8 +100,15 @@ def handle_websocket(ws):
websocket_connections.remove(ws)
def broadcast_message(message):
"""Envía mensajes directamente via WebSocket (optimizado)."""
def broadcast_message(message, immediate=False):
"""Envía mensajes via WebSocket con batching optimizado.
Args:
message: El mensaje o lista de mensajes a enviar
immediate: Si True, envía inmediatamente. Si False, usa batching
"""
global log_batch_buffer
timestamp = datetime.now().strftime("[%H:%M:%S] ")
# Normalizar entrada a lista de mensajes
@ -98,9 +132,17 @@ def broadcast_message(message):
# Registrar en archivo (la clase Logger añade timestamp propio)
config_manager.append_log(raw_msg)
# Enviar inmediatamente via WebSocket con timestamp
# Formatear mensaje para WebSocket
formatted_msg_for_ws = f"{timestamp}{raw_msg}\n"
if immediate:
# Envío inmediato para mensajes críticos
_send_message_to_clients(formatted_msg_for_ws)
else:
# Batching para logs normales
with log_batch_lock:
log_batch_buffer.append(formatted_msg_for_ws)
_schedule_batch_flush()
@app.route("/api/execute_script", methods=["POST"])
@ -109,15 +151,19 @@ def execute_script():
script_group = request.json["group"]
script_name = request.json["script"]
# Crear callback que usa batching para logs normales
def batched_broadcast(message):
return broadcast_message(message, immediate=False)
# Ejecutar el script y obtener resultado
result = config_manager.execute_script(
script_group, script_name, broadcast_message
script_group, script_name, batched_broadcast
)
return jsonify(result)
except Exception as e:
error_msg = f"Error ejecutando script: {str(e)}"
broadcast_message(error_msg)
broadcast_message(error_msg, immediate=True)
return jsonify({"error": error_msg})
@ -127,15 +173,19 @@ def stop_script():
script_group = request.json["group"]
script_name = request.json["script"]
# Crear callback que usa batching para logs normales
def batched_broadcast(message):
return broadcast_message(message, immediate=False)
# Detener el script en ejecución
result = config_manager.stop_script(
script_group, script_name, broadcast_message
script_group, script_name, batched_broadcast
)
return jsonify(result)
except Exception as e:
error_msg = f"Error deteniendo script: {str(e)}"
broadcast_message(error_msg)
broadcast_message(error_msg, immediate=True)
return jsonify({"error": error_msg})
@ -571,10 +621,10 @@ def update_backend_setup():
message += f" ({len(errors)} errores)"
# Log del resultado
broadcast_message(f"🔄 {message}")
broadcast_message(f"🔄 {message}", immediate=True)
if errors:
for error in errors:
broadcast_message(f"{error}")
broadcast_message(f"{error}", immediate=True)
return jsonify(
{
@ -589,7 +639,7 @@ def update_backend_setup():
except Exception as e:
error_msg = f"Error en update_backend_setup: {str(e)}"
print(error_msg)
broadcast_message(f"{error_msg}")
broadcast_message(f"{error_msg}", immediate=True)
return jsonify({"status": "error", "message": error_msg}), 500
@ -721,6 +771,10 @@ def stop_icon_thread():
def shutdown_route():
"""Internal route to shut down the application via the tray icon."""
print("Shutdown endpoint called.")
# Enviar logs pendientes antes de cerrar
_flush_log_batch()
# Stop the main application thread by stopping the tray icon.
# Do this in a separate thread to allow the HTTP response to return first.
stopper = threading.Thread(target=stop_icon_thread, daemon=True)
@ -870,7 +924,7 @@ def execute_gui_script():
return jsonify(result)
except Exception as e:
error_msg = f"Error ejecutando script GUI: {str(e)}"
broadcast_message(error_msg)
broadcast_message(error_msg, immediate=True)
return jsonify({"error": error_msg}), 500
@ -1151,7 +1205,7 @@ def execute_csharp_executable():
return jsonify(result)
except Exception as e:
error_msg = f"Error ejecutando ejecutable C#: {str(e)}"
broadcast_message(error_msg)
broadcast_message(error_msg, immediate=True)
return jsonify({"error": error_msg}), 500
@ -1416,7 +1470,7 @@ def execute_python_script():
return jsonify(result)
except Exception as e:
error_msg = f"Error ejecutando script Python: {str(e)}"
broadcast_message(error_msg)
broadcast_message(error_msg, immediate=True)
return jsonify({"error": error_msg}), 500

View File

@ -5,5 +5,5 @@
},
"level2": {},
"level3": {},
"working_directory": "D:\\Trabajo\\VM\\45 - HENKEL - VM Auto Changeover\\ExportTia"
"working_directory": "C:\\Trabajo\\SIDEL\\09 - SAE452 - Diet as Regular - San Giorgio in Bosco\\ExportTia"
}

View File

@ -1,6 +1,7 @@
{
"path": "D:\\Trabajo\\VM\\45 - HENKEL - VM Auto Changeover\\ExportTia",
"path": "C:\\Trabajo\\SIDEL\\09 - SAE452 - Diet as Regular - San Giorgio in Bosco\\ExportTia",
"history": [
"C:\\Trabajo\\SIDEL\\09 - SAE452 - Diet as Regular - San Giorgio in Bosco\\ExportTia",
"D:\\Trabajo\\VM\\45 - HENKEL - VM Auto Changeover\\ExportTia",
"C:\\Trabajo\\SIDEL\\09 - SAE452 - Diet as Regular - San Giorgio in Bosco\\Reporte\\TiaExport",
"C:\\Trabajo\\SIDEL\\09 - SAE452 - Diet as Regular - San Giovanni in Bosco\\Reporte\\TiaExport",

View File

@ -15,5 +15,5 @@
"xref_source_subdir": "source"
},
"level3": {},
"working_directory": "D:\\Trabajo\\VM\\45 - HENKEL - VM Auto Changeover\\ExportTia"
"working_directory": "C:\\Trabajo\\SIDEL\\09 - SAE452 - Diet as Regular - San Giorgio in Bosco\\ExportTia"
}

View File

@ -1,171 +0,0 @@
#!/usr/bin/env python3
"""
Test script para verificar la detección de cambios basada en hash.
Este script demuestra cómo el sistema detecta cambios en archivos XML
utilizando hash SHA256 en lugar de solo tiempo/tamaño de archivo.
"""
import os
import json
import hashlib
import tempfile
import shutil
from datetime import datetime
def calculate_file_hash(filepath):
"""Calcula el hash SHA256 de un archivo."""
try:
sha256_hash = hashlib.sha256()
with open(filepath, "rb") as f:
for chunk in iter(lambda: f.read(4096), b""):
sha256_hash.update(chunk)
return sha256_hash.hexdigest()
except Exception as e:
print(f"Error calculando hash de {filepath}: {e}")
return None
def create_test_xml(content, filepath):
"""Crea un archivo XML de prueba con el contenido especificado."""
with open(filepath, "w", encoding="utf-8") as f:
f.write(content)
def create_test_json(xml_filepath, json_filepath):
"""Crea un archivo JSON de prueba con metadatos del XML."""
xml_hash = calculate_file_hash(xml_filepath)
xml_mtime = os.path.getmtime(xml_filepath)
xml_size = os.path.getsize(xml_filepath)
json_data = {
"block_name": "Test_Block",
"block_type": "FC",
"language": "LAD",
"source_xml_mod_time": xml_mtime,
"source_xml_size": xml_size,
"source_xml_hash": xml_hash,
"interface": {},
"networks": [],
}
with open(json_filepath, "w", encoding="utf-8") as f:
json.dump(json_data, f, indent=4)
return json_data
def test_hash_detection():
"""Prueba la detección de cambios basada en hash."""
print("=== Test de Detección de Cambios por Hash ===\n")
# Crear directorio temporal
with tempfile.TemporaryDirectory() as temp_dir:
xml_file = os.path.join(temp_dir, "test_block.xml")
json_file = os.path.join(temp_dir, "test_block_processed.json")
# Paso 1: Crear XML original
original_content = """<?xml version="1.0" encoding="UTF-8"?>
<Document>
<SW.Blocks.FC ID="A">
<AttributeList>
<Name>Test_Block</Name>
<Number>100</Number>
<ProgrammingLanguage>LAD</ProgrammingLanguage>
</AttributeList>
<ObjectList>
<NetworkSource />
</ObjectList>
</SW.Blocks.FC>
</Document>"""
create_test_xml(original_content, xml_file)
original_hash = calculate_file_hash(xml_file)
print(f"1. XML original creado")
print(f" Hash: {original_hash[:16]}...")
print(f" Tamaño: {os.path.getsize(xml_file)} bytes")
# Paso 2: Crear JSON con metadatos
json_data = create_test_json(xml_file, json_file)
print(f"\n2. JSON con metadatos creado")
print(f" Hash almacenado: {json_data['source_xml_hash'][:16]}...")
# Paso 3: Simular verificación - archivo sin cambios
current_hash = calculate_file_hash(xml_file)
hash_match = json_data["source_xml_hash"] == current_hash
print(f"\n3. Verificación sin cambios:")
print(f" Hash actual: {current_hash[:16]}...")
print(f" ¿Hash coincide?: {hash_match}")
print(f"{'SALTAR procesamiento' if hash_match else 'PROCESAR archivo'}")
# Paso 4: Modificar archivo manteniendo mismo tamaño y tiempo
print(f"\n4. Modificando XML (cambio sutil)...")
modified_content = original_content.replace(
"Test_Block", "Test_Blck"
) # Cambio sutil
# Guardar tiempo original
original_mtime = os.path.getmtime(xml_file)
create_test_xml(modified_content, xml_file)
# Restaurar tiempo original (simular que el timestamp no cambió)
os.utime(xml_file, (original_mtime, original_mtime))
new_size = os.path.getsize(xml_file)
new_mtime = os.path.getmtime(xml_file)
new_hash = calculate_file_hash(xml_file)
print(f" Nuevo hash: {new_hash[:16]}...")
print(f" Nuevo tamaño: {new_size} bytes")
print(
f" Tiempo modificación: {abs(new_mtime - json_data['source_xml_mod_time']) < 0.001}"
)
# Paso 5: Verificar detección por diferentes métodos
print(f"\n5. Comparación de métodos de detección:")
# Método tradicional (tiempo + tamaño)
time_match = abs(new_mtime - json_data["source_xml_mod_time"]) < 0.001
size_match = new_size == json_data["source_xml_size"]
traditional_would_skip = time_match and size_match
# Método por hash
hash_would_skip = new_hash == json_data["source_xml_hash"]
print(f" Método tradicional (tiempo+tamaño):")
print(f" ¿Tiempo coincide?: {time_match}")
print(f" ¿Tamaño coincide?: {size_match}")
print(f"{'SALTAR' if traditional_would_skip else 'PROCESAR'}")
print(f" Método por hash:")
print(f" ¿Hash coincide?: {hash_would_skip}")
print(f"{'SALTAR' if hash_would_skip else 'PROCESAR'}")
print(f"\n6. Resultado:")
if traditional_would_skip and not hash_would_skip:
print(f" ✅ HASH DETECTÓ CAMBIO que el método tradicional PERDIÓ")
print(f" 🔄 El archivo será regenerado correctamente")
elif not traditional_would_skip and not hash_would_skip:
print(f" ✅ Ambos métodos detectaron el cambio")
elif traditional_would_skip and hash_would_skip:
print(f" ✅ Ambos métodos indican que no hay cambios")
else:
print(f" ⚠️ Situación inesperada")
# Paso 6: Mostrar contenidos para verificación
print(f"\n7. Verificación de contenido:")
print(f" Contenido original tenía: 'Test_Block'")
print(f" Contenido nuevo tiene: 'Test_Blck'")
with open(xml_file, "r", encoding="utf-8") as f:
content = f.read()
has_original = "Test_Block" in content
has_modified = "Test_Blck" in content
print(f" ¿Contiene 'Test_Block'?: {has_original}")
print(f" ¿Contiene 'Test_Blck'?: {has_modified}")
if __name__ == "__main__":
test_hash_detection()

View File

@ -1,204 +0,0 @@
#!/usr/bin/env python3
"""
Test específico para demostrar casos donde el hash detecta cambios
que el método tradicional (tiempo + tamaño) podría perder.
"""
import os
import json
import hashlib
import tempfile
import time
def calculate_file_hash(filepath):
"""Calcula el hash SHA256 de un archivo."""
try:
sha256_hash = hashlib.sha256()
with open(filepath, "rb") as f:
for chunk in iter(lambda: f.read(4096), b""):
sha256_hash.update(chunk)
return sha256_hash.hexdigest()
except Exception as e:
print(f"Error calculando hash de {filepath}: {e}")
return None
def test_subtle_changes():
"""Demuestra detección de cambios sutiles que mantienen el mismo tamaño."""
print("=== Test: Cambios Sutiles con Mismo Tamaño ===\n")
with tempfile.TemporaryDirectory() as temp_dir:
xml_file = os.path.join(temp_dir, "test.xml")
# Crear contenido original de exactamente el mismo tamaño
original = """<?xml version="1.0"?>
<Block>
<Name>MotorControl_01</Name>
<Status>Active</Status>
</Block>"""
# Crear contenido modificado del mismo tamaño (cambiar solo algunos caracteres)
modified = """<?xml version="1.0"?>
<Block>
<Name>MotorControl_02</Name>
<Status>Active</Status>
</Block>"""
# Verificar que tienen el mismo tamaño
assert len(original) == len(
modified
), "Los contenidos deben tener el mismo tamaño"
# Paso 1: Crear archivo original
with open(xml_file, "w", encoding="utf-8") as f:
f.write(original)
original_hash = calculate_file_hash(xml_file)
original_size = os.path.getsize(xml_file)
original_mtime = os.path.getmtime(xml_file)
print(f"1. Archivo original:")
print(f" Contenido: MotorControl_01")
print(f" Hash: {original_hash[:16]}...")
print(f" Tamaño: {original_size} bytes")
# Simular JSON con metadatos
json_data = {
"source_xml_hash": original_hash,
"source_xml_size": original_size,
"source_xml_mod_time": original_mtime,
}
# Paso 2: Esperar un poco y modificar archivo manteniendo tamaño
time.sleep(0.1) # Pequeña pausa
with open(xml_file, "w", encoding="utf-8") as f:
f.write(modified)
# Restaurar tiempo de modificación original
os.utime(xml_file, (original_mtime, original_mtime))
new_hash = calculate_file_hash(xml_file)
new_size = os.path.getsize(xml_file)
new_mtime = os.path.getmtime(xml_file)
print(f"\n2. Archivo modificado:")
print(f" Contenido: MotorControl_02")
print(f" Hash: {new_hash[:16]}...")
print(f" Tamaño: {new_size} bytes")
print(
f" Tiempo: {'Igual' if abs(new_mtime - original_mtime) < 0.001 else 'Diferente'}"
)
# Paso 3: Comparar métodos de detección
print(f"\n3. Detección de cambios:")
# Método tradicional
time_match = abs(new_mtime - json_data["source_xml_mod_time"]) < 0.001
size_match = new_size == json_data["source_xml_size"]
traditional_detects_change = not (time_match and size_match)
# Método por hash
hash_detects_change = new_hash != json_data["source_xml_hash"]
print(f" Método tradicional:")
print(f" Tiempo coincide: {time_match}")
print(f" Tamaño coincide: {size_match}")
print(f" Detecta cambio: {traditional_detects_change}")
print(f" Método por hash:")
print(f" Hash coincide: {new_hash == json_data['source_xml_hash']}")
print(f" Detecta cambio: {hash_detects_change}")
# Resultado
print(f"\n4. Resultado:")
if not traditional_detects_change and hash_detects_change:
print(f" 🎯 HASH DETECTÓ CAMBIO que método tradicional PERDIÓ")
print(f" ✅ Sin hash: archivo NO se regeneraría (ERROR)")
print(f" ✅ Con hash: archivo SÍ se regenerará (CORRECTO)")
elif traditional_detects_change and hash_detects_change:
print(f" ✅ Ambos métodos detectaron el cambio correctamente")
else:
print(
f" Resultado: tradicional={traditional_detects_change}, hash={hash_detects_change}"
)
return not traditional_detects_change and hash_detects_change
def test_same_size_different_content():
"""Test con contenidos completamente diferentes pero mismo tamaño."""
print("\n" + "=" * 50)
print("=== Test: Contenido Diferente, Mismo Tamaño ===\n")
with tempfile.TemporaryDirectory() as temp_dir:
xml_file = os.path.join(temp_dir, "test.xml")
# Contenidos de igual longitud pero diferentes
content1 = "<Root><Item>AAAAAAAAAA</Item></Root>" # 33 chars
content2 = "<Root><Item>BBBBBBBBBB</Item></Root>" # 33 chars
assert len(content1) == len(content2), "Deben tener igual tamaño"
# Crear archivo con contenido 1
with open(xml_file, "w", encoding="utf-8") as f:
f.write(content1)
hash1 = calculate_file_hash(xml_file)
size1 = os.path.getsize(xml_file)
mtime1 = os.path.getmtime(xml_file)
# Cambiar a contenido 2 manteniendo tiempo
time.sleep(0.1)
with open(xml_file, "w", encoding="utf-8") as f:
f.write(content2)
os.utime(xml_file, (mtime1, mtime1))
hash2 = calculate_file_hash(xml_file)
size2 = os.path.getsize(xml_file)
mtime2 = os.path.getmtime(xml_file)
print(f"Contenido 1: {content1[:20]}...")
print(f"Contenido 2: {content2[:20]}...")
print(f"Tamaños: {size1} = {size2} ({'' if size1 == size2 else ''})")
print(f"Tiempos: {'iguales' if abs(mtime2 - mtime1) < 0.001 else 'diferentes'}")
print(f"Hashes: {'iguales' if hash1 == hash2 else 'diferentes'}")
would_skip_traditional = (size1 == size2) and (abs(mtime2 - mtime1) < 0.001)
would_skip_hash = hash1 == hash2
print(
f"\nSin hash: {'SALTARÍA procesamiento' if would_skip_traditional else 'procesaría'}"
)
print(
f"Con hash: {'saltaría procesamiento' if would_skip_hash else 'PROCESARÍA'}"
)
if would_skip_traditional and not would_skip_hash:
print("🎯 El hash evita saltar un archivo que SÍ cambió")
return True
return False
if __name__ == "__main__":
print("Ejecutando tests de detección de cambios por hash...\n")
test1_result = test_subtle_changes()
test2_result = test_same_size_different_content()
print("\n" + "=" * 50)
print("=== RESUMEN ===")
print(
f"Test 1 (cambios sutiles): {'HASH ÚTIL' if test1_result else 'ambos métodos iguales'}"
)
print(
f"Test 2 (mismo tamaño): {'HASH ÚTIL' if test2_result else 'ambos métodos iguales'}"
)
if test1_result or test2_result:
print("\n✅ El hash SHA256 mejora la detección de cambios")
print("✅ Evita regeneraciones innecesarias Y perdida de cambios reales")
else:
print("\n✓ Ambos métodos funcionaron igual en estos tests")

View File

@ -1,6 +1,7 @@
{
"path": "D:\\Trabajo\\VM\\45 - HENKEL - VM Auto Changeover\\ExportTia",
"path": "C:\\Trabajo\\SIDEL\\09 - SAE452 - Diet as Regular - San Giorgio in Bosco\\ExportTia",
"history": [
"C:\\Trabajo\\SIDEL\\09 - SAE452 - Diet as Regular - San Giorgio in Bosco\\ExportTia",
"D:\\Trabajo\\VM\\45 - HENKEL - VM Auto Changeover\\ExportTia",
"C:\\Trabajo\\SIDEL\\09 - SAE452 - Diet as Regular - San Giorgio in Bosco\\Reporte\\TiaExport"
]

10188
data/log.txt

File diff suppressed because it is too large Load Diff