feat: Implement script execution and stopping functionality

- Added a new method to stop running scripts in the ScriptExecutor class, allowing graceful termination of scripts.
- Updated the ConfigurationManager class to handle script stopping requests and manage running processes.
- Enhanced the frontend JavaScript to include stop buttons for scripts, updating their state based on execution status.
- Introduced a mechanism to track running scripts and update UI elements accordingly.
- Improved logging for script execution and stopping events.
This commit is contained in:
Miguel 2025-08-23 10:51:38 +02:00
parent fdc48375ad
commit 5ed4d9391e
8 changed files with 648 additions and 189 deletions

18
app.py
View File

@ -140,6 +140,24 @@ def execute_script():
return jsonify({"error": error_msg}) return jsonify({"error": error_msg})
@app.route("/api/stop_script", methods=["POST"])
def stop_script():
try:
script_group = request.json["group"]
script_name = request.json["script"]
# Detener el script en ejecución
result = config_manager.stop_script(
script_group, script_name, broadcast_message
)
return jsonify(result)
except Exception as e:
error_msg = f"Error deteniendo script: {str(e)}"
broadcast_message(error_msg)
return jsonify({"error": error_msg})
@app.route("/") @app.route("/")
def index(): def index():
script_groups = config_manager.get_script_groups() script_groups = config_manager.get_script_groups()

View File

@ -8,8 +8,8 @@
"cronologia_file": "cronologia.md" "cronologia_file": "cronologia.md"
}, },
"level3": { "level3": {
"cronologia_file": "Planning - emails", "cronologia_file": "emails",
"input_directory": "C:\\Trabajo\\SIDEL\\PROJECTs Planning\\Emails" "input_directory": "C:/Users/migue/OneDrive/Miguel/Obsidean/General/Notas/Miguel/Contable/2025/EmailsOriginales"
}, },
"working_directory": "C:\\Users\\migue\\OneDrive\\Miguel\\Obsidean\\Trabajo\\VM\\04-SIDEL\\0 - PROJECTS Description\\PLANNING" "working_directory": "C:\\Users\\migue\\OneDrive\\Miguel\\Obsidean\\General\\Notas\\Miguel\\Contable\\2025"
} }

View File

@ -1,10 +1,12 @@
{ {
"path": "C:\\Users\\migue\\OneDrive\\Miguel\\Obsidean\\Trabajo\\VM\\04-SIDEL\\0 - PROJECTS Description\\PLANNING", "path": "C:\\Users\\migue\\OneDrive\\Miguel\\Obsidean\\General\\Notas\\Miguel\\Contable\\2025",
"history": [ "history": [
"C:\\Users\\migue\\OneDrive\\Miguel\\Obsidean\\General\\Notas\\Miguel\\Contable\\2025",
"C:\\Users\\migue\\OneDrive\\Miguel\\Obsidean\\Trabajo\\VM\\03-VM\\45 - HENKEL - VM Auto Changeover",
"D:\\Trabajo\\VM\\45 - HENKEL - VM Auto Changeover\\Entregado por VM\\01 - 26-07-2025 Max - Emails",
"C:\\Users\\migue\\OneDrive\\Miguel\\Obsidean\\Trabajo\\VM\\04-SIDEL\\0 - PROJECTS Description\\PLANNING", "C:\\Users\\migue\\OneDrive\\Miguel\\Obsidean\\Trabajo\\VM\\04-SIDEL\\0 - PROJECTS Description\\PLANNING",
"C:\\Users\\migue\\OneDrive\\Miguel\\Obsidean\\Trabajo\\VM\\04-SIDEL\\17 - E5.006880 - Modifica O&U - RSC098", "C:\\Users\\migue\\OneDrive\\Miguel\\Obsidean\\Trabajo\\VM\\04-SIDEL\\17 - E5.006880 - Modifica O&U - RSC098",
"C:\\Trabajo\\SIDEL\\17 - E5.006880 - Modifica O&U - RSC098\\Reporte\\Emails", "C:\\Trabajo\\SIDEL\\17 - E5.006880 - Modifica O&U - RSC098\\Reporte\\Emails",
"C:\\Trabajo\\SIDEL\\17 - E5.006880 - Modifica O&U - RSC098", "C:\\Trabajo\\SIDEL\\17 - E5.006880 - Modifica O&U - RSC098"
"D:\\Trabajo\\VM\\45 - HENKEL - VM Auto Changeover\\Entregado por VM\\01 - 26-07-2025 Max - Emails"
] ]
} }

View File

@ -8,6 +8,7 @@ from tkinter import filedialog
import os import os
import sys import sys
import traceback import traceback
import time
from pathlib import Path from pathlib import Path
script_root = os.path.dirname( script_root = os.path.dirname(
@ -30,6 +31,7 @@ SUPPORTED_TIA_VERSIONS = {
CROSS_REF_FILTER = 1 CROSS_REF_FILTER = 1
MAX_REOPEN_ATTEMPTS = 5 # Número máximo de re-aperturas permitidas para evitar bucles infinitos MAX_REOPEN_ATTEMPTS = 5 # Número máximo de re-aperturas permitidas para evitar bucles infinitos
BLOCK_TIMEOUT_SECONDS = 120 # Referencia de tiempo esperado para el procesamiento de cada bloque (para logging)
class PortalDisposedException(Exception): class PortalDisposedException(Exception):
"""Excepción lanzada cuando TIA Portal se ha cerrado inesperadamente o un objeto ha sido descartado.""" """Excepción lanzada cuando TIA Portal se ha cerrado inesperadamente o un objeto ha sido descartado."""
@ -122,11 +124,42 @@ def select_project_file():
sys.exit(0) sys.exit(0)
return file_path return file_path
# Normalizar nombres de bloque/tabla/udt para comparaciones consistentes
def _normalize_name(name: str) -> str: def _normalize_name(name: str) -> str:
"""Normaliza un nombre quitando espacios laterales y convirtiendo a minúsculas.""" """Normaliza un nombre quitando espacios laterales y convirtiendo a minúsculas."""
return name.strip().lower() return name.strip().lower()
def _export_block_with_timeout(block, blocks_cr_path, block_name, timeout_seconds=BLOCK_TIMEOUT_SECONDS):
"""
Exporta las referencias cruzadas de un bloque con monitoreo de tiempo.
Note: TIA Portal Openness no permite operaciones multi-hilo, por lo que
implementamos un timeout conceptual que al menos registra cuánto tiempo toma.
Returns:
bool: True si se exportó exitosamente
"""
start_time = time.time()
try:
# Realizar la exportación de forma directa (sin hilos debido a restricciones de TIA)
block.export_cross_references(
target_directorypath=str(blocks_cr_path),
filter=CROSS_REF_FILTER,
)
elapsed_time = time.time() - start_time
# Verificar si excedió el tiempo esperado (aunque ya terminó)
if elapsed_time > timeout_seconds:
print(f" ADVERTENCIA: El bloque tardó {elapsed_time:.2f}s (>{timeout_seconds}s esperado)")
return True
except Exception as e:
elapsed_time = time.time() - start_time
print(f" Tiempo transcurrido antes del error: {elapsed_time:.2f} segundos")
raise e
def export_plc_cross_references(plc, export_base_dir, exported_blocks=None, problematic_blocks=None): def export_plc_cross_references(plc, export_base_dir, exported_blocks=None, problematic_blocks=None):
"""Exports cross-references for various elements from a given PLC. """Exports cross-references for various elements from a given PLC.
Parámetros Parámetros
@ -151,6 +184,7 @@ def export_plc_cross_references(plc, export_base_dir, exported_blocks=None, prob
# --- Export Program Block Cross-References --- # --- Export Program Block Cross-References ---
blocks_cr_exported = 0 blocks_cr_exported = 0
blocks_cr_skipped = 0 blocks_cr_skipped = 0
current_block_name = None # Track current block being processed
print(f"\n[PLC: {plc_name}] Exportando referencias cruzadas de bloques de programa...") print(f"\n[PLC: {plc_name}] Exportando referencias cruzadas de bloques de programa...")
blocks_cr_path = plc_export_dir / "ProgramBlocks_CR" blocks_cr_path = plc_export_dir / "ProgramBlocks_CR"
blocks_cr_path.mkdir(exist_ok=True) blocks_cr_path.mkdir(exist_ok=True)
@ -159,8 +193,19 @@ def export_plc_cross_references(plc, export_base_dir, exported_blocks=None, prob
try: try:
program_blocks = plc.get_program_blocks() program_blocks = plc.get_program_blocks()
print(f" Se encontraron {len(program_blocks)} bloques de programa.") print(f" Se encontraron {len(program_blocks)} bloques de programa.")
# Show which blocks will be skipped from the start
if problematic_blocks:
skipped_names = []
for block in program_blocks:
if _normalize_name(block.get_name()) in problematic_blocks:
skipped_names.append(block.get_name())
if skipped_names:
print(f" Bloques que serán omitidos (problemáticos previos): {', '.join(skipped_names)}")
for block in program_blocks: for block in program_blocks:
block_name = block.get_name() block_name = block.get_name()
current_block_name = block_name # Update current block being processed
norm_block = _normalize_name(block_name) norm_block = _normalize_name(block_name)
if norm_block in problematic_blocks: if norm_block in problematic_blocks:
print(f" Omitiendo bloque problemático previamente detectado: {block_name}") print(f" Omitiendo bloque problemático previamente detectado: {block_name}")
@ -168,14 +213,18 @@ def export_plc_cross_references(plc, export_base_dir, exported_blocks=None, prob
continue continue
if norm_block in exported_blocks: if norm_block in exported_blocks:
# Ya exportado en un intento anterior, no repetir # Ya exportado en un intento anterior, no repetir
print(f" Omitiendo bloque ya exportado: {block_name}")
continue continue
print(f" Procesando bloque: {block_name}...") print(f" Procesando bloque: {block_name}...")
try: try:
print(f" Exportando referencias cruzadas para {block_name}...") print(f" Exportando referencias cruzadas para {block_name}...")
block.export_cross_references( start_time = time.time()
target_directorypath=str(blocks_cr_path),
filter=CROSS_REF_FILTER, # Usar la función con monitoreo de tiempo
) _export_block_with_timeout(block, blocks_cr_path, block_name)
elapsed_time = time.time() - start_time
print(f" Exportación completada en {elapsed_time:.2f} segundos")
blocks_cr_exported += 1 blocks_cr_exported += 1
exported_blocks.add(norm_block) exported_blocks.add(norm_block)
except RuntimeError as block_ex: except RuntimeError as block_ex:
@ -189,10 +238,10 @@ def export_plc_cross_references(plc, export_base_dir, exported_blocks=None, prob
f" ERROR GENERAL al exportar referencias cruzadas para el bloque {block_name}: {block_ex}" f" ERROR GENERAL al exportar referencias cruzadas para el bloque {block_name}: {block_ex}"
) )
traceback.print_exc() traceback.print_exc()
problematic_blocks.add(norm_block) # Always mark as problematic
blocks_cr_skipped += 1 blocks_cr_skipped += 1
if _is_disposed_exception(block_ex): if _is_disposed_exception(block_ex):
# Escalamos para que el script pueda re-abrir el Portal y omitir el bloque # Escalamos para que el script pueda re-abrir el Portal y omitir el bloque
problematic_blocks.add(norm_block)
raise PortalDisposedException(block_ex, failed_block=block_name) raise PortalDisposedException(block_ex, failed_block=block_name)
print( print(
f" Resumen de exportación de referencias cruzadas de bloques: Exportados={blocks_cr_exported}, Omitidos/Errores={blocks_cr_skipped}" f" Resumen de exportación de referencias cruzadas de bloques: Exportados={blocks_cr_exported}, Omitidos/Errores={blocks_cr_skipped}"
@ -204,8 +253,12 @@ def export_plc_cross_references(plc, export_base_dir, exported_blocks=None, prob
except Exception as e: except Exception as e:
print(f" ERROR al acceder a los bloques de programa para exportar referencias cruzadas: {e}") print(f" ERROR al acceder a los bloques de programa para exportar referencias cruzadas: {e}")
traceback.print_exc() traceback.print_exc()
problematic_blocks.add(_normalize_name(e.__str__())) # If we know which block was being processed, mark it as problematic
raise PortalDisposedException(e) if current_block_name:
problematic_blocks.add(_normalize_name(current_block_name))
raise PortalDisposedException(e, failed_block=current_block_name)
else:
raise PortalDisposedException(e)
# --- Export PLC Tag Table Cross-References --- # --- Export PLC Tag Table Cross-References ---
tags_cr_exported = 0 tags_cr_exported = 0
@ -424,6 +477,11 @@ if __name__ == "__main__":
working_directory = configs.get("working_directory") working_directory = configs.get("working_directory")
print("--- Exportador de Referencias Cruzadas de TIA Portal ---") print("--- Exportador de Referencias Cruzadas de TIA Portal ---")
print(f"Configuración:")
print(f" - Tiempo esperado por bloque: {BLOCK_TIMEOUT_SECONDS} segundos (para logging)")
print(f" - Máximo intentos de reapertura: {MAX_REOPEN_ATTEMPTS}")
print(f" - Filtro de referencias cruzadas: {CROSS_REF_FILTER}")
print("")
# Validate working directory # Validate working directory
if not working_directory or not os.path.isdir(working_directory): if not working_directory or not os.path.isdir(working_directory):
@ -484,8 +542,12 @@ if __name__ == "__main__":
reopen_attempts += 1 reopen_attempts += 1
failed_block = pd_ex.failed_block failed_block = pd_ex.failed_block
if failed_block: if failed_block:
problematic_blocks.add(_normalize_name(failed_block)) norm_failed_block = _normalize_name(failed_block)
problematic_blocks.add(norm_failed_block)
skipped_blocks_report.append(failed_block) skipped_blocks_report.append(failed_block)
print(f"Marcando bloque problemático: {failed_block}")
else:
print("Error general detectado sin bloque específico identificado")
if reopen_attempts > MAX_REOPEN_ATTEMPTS: if reopen_attempts > MAX_REOPEN_ATTEMPTS:
print( print(
@ -520,6 +582,7 @@ if __name__ == "__main__":
if skipped_blocks_report: if skipped_blocks_report:
print(f"\nBloques problemáticos para el PLC '{plc_name}': {', '.join(set(skipped_blocks_report))}") print(f"\nBloques problemáticos para el PLC '{plc_name}': {', '.join(set(skipped_blocks_report))}")
print(f"Total de bloques problemáticos registrados: {len(problematic_blocks)}")
print("\nProceso de exportación de referencias cruzadas completado.") print("\nProceso de exportación de referencias cruzadas completado.")

View File

@ -1,111 +1,206 @@
[15:34:16] Iniciando ejecución de x1.py en C:\Users\migue\OneDrive\Miguel\Obsidean\Trabajo\VM\04-SIDEL\0 - PROJECTS Description\PLANNING... [10:48:07] Iniciando ejecución de x4.py en D:\Trabajo\VM\45 - HENKEL - VM Auto Changeover\ExportTia...
[15:34:16] ✅ Configuración cargada exitosamente [10:48:07] --- Exportador de Referencias Cruzadas de TIA Portal ---
[15:34:16] Working/Output directory: C:\Users\migue\OneDrive\Miguel\Obsidean\Trabajo\VM\04-SIDEL\0 - PROJECTS Description\PLANNING [10:48:07] Configuración:
[15:34:16] Input directory: C:\Trabajo\SIDEL\PROJECTs Planning\Emails [10:48:07] - Tiempo esperado por bloque: 120 segundos (para logging)
[15:34:16] Output file: C:\Users\migue\OneDrive\Miguel\Obsidean\Trabajo\VM\04-SIDEL\0 - PROJECTS Description\PLANNING\emails.md [10:48:07] - Máximo intentos de reapertura: 5
[15:34:16] Attachments directory: C:\Users\migue\OneDrive\Miguel\Obsidean\Trabajo\VM\04-SIDEL\0 - PROJECTS Description\PLANNING\adjuntos [10:48:07] - Filtro de referencias cruzadas: 1
[15:34:16] Beautify rules file: D:\Proyectos\Scripts\ParamManagerScripts\backend\script_groups\EmailCrono\config\beautify_rules.json [10:48:16] --- ERRORES ---
[15:34:16] Found 1 .eml files [10:48:16] 2025-08-23 10:44:50,965 [13] ERROR Siemens.TiaPortal.OpennessApi18.Implementations.ProgramBlock ExportCrossReferences -
[15:34:16] Creando cronología nueva (archivo se sobrescribirá) [10:48:16] Siemens.TiaPortal.OpennessContracts.OpennessAccessException: Cross-thread operation is not valid in Openness within STA.
[15:34:16] ============================================================ [10:48:16] Traceback (most recent call last):
[15:34:16] Processing file: C:\Trabajo\SIDEL\PROJECTs Planning\Emails\Planning attività 08-08-2025 Teknors.eml [10:48:16] File "D:\Proyectos\Scripts\ParamManagerScripts\backend\script_groups\ObtainIOFromProjectTia\x4.py", line 241, in export_plc_cross_references
[15:34:16] 📧 Abriendo archivo: C:\Trabajo\SIDEL\PROJECTs Planning\Emails\Planning attività 08-08-2025 Teknors.eml [10:48:16] _export_block_with_timeout(block, blocks_cr_path, block_name)
[15:34:16] ✉️ Mensaje extraído: [10:48:16] File "D:\Proyectos\Scripts\ParamManagerScripts\backend\script_groups\ObtainIOFromProjectTia\x4.py", line 173, in _export_block_with_timeout
[15:34:16] - Subject: Planning attività 08-08-2025 Teknors [10:48:16] raise result["error"]
[15:34:16] - Remitente: "Passera, Alessandro" <Alessandro.Passera@sidel.com> [10:48:16] File "D:\Proyectos\Scripts\ParamManagerScripts\backend\script_groups\ObtainIOFromProjectTia\x4.py", line 151, in export_worker
[15:34:16] - Fecha: 2025-08-08 13:06:58 [10:48:16] block.export_cross_references(
[15:34:16] - Adjuntos: 0 archivos [10:48:16] ValueError: OpennessAccessException: Cross-thread operation is not valid in Openness within STA.
[15:34:16] - Contenido: 20738 caracteres [10:48:16] 2025-08-23 10:44:50,986 [14] ERROR Siemens.TiaPortal.OpennessApi18.Implementations.ProgramBlock ExportCrossReferences -
[15:34:16] - Hash generado: ba7f9f899c63a04a454f2e2a9d50856c [10:48:16] Siemens.TiaPortal.OpennessContracts.OpennessAccessException: Cross-thread operation is not valid in Openness within STA.
[15:34:16] 📧 Procesamiento completado: 1 mensajes extraídos [10:48:16] Traceback (most recent call last):
[15:34:16] Extracted 1 messages from Planning attività 08-08-2025 Teknors.eml [10:48:16] File "D:\Proyectos\Scripts\ParamManagerScripts\backend\script_groups\ObtainIOFromProjectTia\x4.py", line 241, in export_plc_cross_references
[15:34:16] --- Msg 1/1 from Planning attività 08-08-2025 Teknors.eml --- [10:48:16] _export_block_with_timeout(block, blocks_cr_path, block_name)
[15:34:16] Remitente: Passera, Alessandro [10:48:16] File "D:\Proyectos\Scripts\ParamManagerScripts\backend\script_groups\ObtainIOFromProjectTia\x4.py", line 173, in _export_block_with_timeout
[15:34:16] Fecha: 2025-08-08 13:06:58 [10:48:16] raise result["error"]
[15:34:16] Subject: Planning attività 08-08-2025 Teknors [10:48:16] File "D:\Proyectos\Scripts\ParamManagerScripts\backend\script_groups\ObtainIOFromProjectTia\x4.py", line 151, in export_worker
[15:34:16] Hash: ba7f9f899c63a04a454f2e2a9d50856c [10:48:16] block.export_cross_references(
[15:34:16] Adjuntos: [] [10:48:16] ValueError: OpennessAccessException: Cross-thread operation is not valid in Openness within STA.
[15:34:16] ✓ NUEVO mensaje - Agregando a la cronología [10:48:16] 2025-08-23 10:44:50,988 [15] ERROR Siemens.TiaPortal.OpennessApi18.Implementations.ProgramBlock ExportCrossReferences -
[15:34:16] Estadísticas de procesamiento: [10:48:16] Siemens.TiaPortal.OpennessContracts.OpennessAccessException: Cross-thread operation is not valid in Openness within STA.
[15:34:16] - Total mensajes encontrados: 1 [10:48:16] Traceback (most recent call last):
[15:34:16] - Mensajes únicos añadidos: 1 [10:48:16] File "D:\Proyectos\Scripts\ParamManagerScripts\backend\script_groups\ObtainIOFromProjectTia\x4.py", line 241, in export_plc_cross_references
[15:34:16] - Mensajes duplicados ignorados: 0 [10:48:16] _export_block_with_timeout(block, blocks_cr_path, block_name)
[15:34:16] Writing 1 messages to C:\Users\migue\OneDrive\Miguel\Obsidean\Trabajo\VM\04-SIDEL\0 - PROJECTS Description\PLANNING\emails.md [10:48:16] File "D:\Proyectos\Scripts\ParamManagerScripts\backend\script_groups\ObtainIOFromProjectTia\x4.py", line 173, in _export_block_with_timeout
[15:34:16] ✅ Cronología guardada exitosamente en: C:\Users\migue\OneDrive\Miguel\Obsidean\Trabajo\VM\04-SIDEL\0 - PROJECTS Description\PLANNING\emails.md [10:48:16] raise result["error"]
[15:34:16] 📊 Total de mensajes en la cronología: 1 [10:48:16] File "D:\Proyectos\Scripts\ParamManagerScripts\backend\script_groups\ObtainIOFromProjectTia\x4.py", line 151, in export_worker
[15:34:16] Ejecución de x1.py finalizada (success). Duración: 0:00:00.249985. [10:48:16] block.export_cross_references(
[15:34:16] Log completo guardado en: D:\Proyectos\Scripts\ParamManagerScripts\backend\script_groups\EmailCrono\.log\log_x1.txt [10:48:16] ValueError: OpennessAccessException: Cross-thread operation is not valid in Openness within STA.
[15:35:04] Iniciando ejecución de x1.py en C:\Users\migue\OneDrive\Miguel\Obsidean\Trabajo\VM\04-SIDEL\0 - PROJECTS Description\PLANNING... [10:48:16] 2025-08-23 10:44:50,990 [16] ERROR Siemens.TiaPortal.OpennessApi18.Implementations.ProgramBlock ExportCrossReferences -
[15:35:04] ✅ Configuración cargada exitosamente [10:48:16] Siemens.TiaPortal.OpennessContracts.OpennessAccessException: Cross-thread operation is not valid in Openness within STA.
[15:35:04] Working/Output directory: C:\Users\migue\OneDrive\Miguel\Obsidean\Trabajo\VM\04-SIDEL\0 - PROJECTS Description\PLANNING [10:48:16] Traceback (most recent call last):
[15:35:04] Input directory: C:\Trabajo\SIDEL\PROJECTs Planning\Emails [10:48:16] File "D:\Proyectos\Scripts\ParamManagerScripts\backend\script_groups\ObtainIOFromProjectTia\x4.py", line 241, in export_plc_cross_references
[15:35:04] Output file: C:\Users\migue\OneDrive\Miguel\Obsidean\Trabajo\VM\04-SIDEL\0 - PROJECTS Description\PLANNING\Planning - emails.md [10:48:16] _export_block_with_timeout(block, blocks_cr_path, block_name)
[15:35:04] Attachments directory: C:\Users\migue\OneDrive\Miguel\Obsidean\Trabajo\VM\04-SIDEL\0 - PROJECTS Description\PLANNING\adjuntos [10:48:16] File "D:\Proyectos\Scripts\ParamManagerScripts\backend\script_groups\ObtainIOFromProjectTia\x4.py", line 173, in _export_block_with_timeout
[15:35:04] Beautify rules file: D:\Proyectos\Scripts\ParamManagerScripts\backend\script_groups\EmailCrono\config\beautify_rules.json [10:48:16] raise result["error"]
[15:35:04] Found 1 .eml files [10:48:16] File "D:\Proyectos\Scripts\ParamManagerScripts\backend\script_groups\ObtainIOFromProjectTia\x4.py", line 151, in export_worker
[15:35:04] Creando cronología nueva (archivo se sobrescribirá) [10:48:16] block.export_cross_references(
[15:35:04] ============================================================ [10:48:16] ValueError: OpennessAccessException: Cross-thread operation is not valid in Openness within STA.
[15:35:04] Processing file: C:\Trabajo\SIDEL\PROJECTs Planning\Emails\Planning attività 08-08-2025 Teknors.eml [10:48:16] 2025-08-23 10:44:50,992 [17] ERROR Siemens.TiaPortal.OpennessApi18.Implementations.ProgramBlock ExportCrossReferences -
[15:35:04] 📧 Abriendo archivo: C:\Trabajo\SIDEL\PROJECTs Planning\Emails\Planning attività 08-08-2025 Teknors.eml [10:48:16] Siemens.TiaPortal.OpennessContracts.OpennessAccessException: Cross-thread operation is not valid in Openness within STA.
[15:35:04] ✉️ Mensaje extraído: [10:48:16] Traceback (most recent call last):
[15:35:04] - Subject: Planning attività 08-08-2025 Teknors [10:48:16] --- FIN ERRORES ---
[15:35:04] - Remitente: "Passera, Alessandro" <Alessandro.Passera@sidel.com> [10:48:16] Ejecución de x4.py finalizada (error). Duración: 0:04:08.869202. Se detectaron errores (ver log).
[15:35:04] - Fecha: 2025-08-08 13:06:58 [10:48:16] Log completo guardado en: D:\Proyectos\Scripts\ParamManagerScripts\backend\script_groups\ObtainIOFromProjectTia\.log\log_x4.txt
[15:35:04] - Adjuntos: 0 archivos [10:48:19] Versión de TIA Portal detectada: 18.0 (de la extensión .ap18)
[15:35:04] - Contenido: 20738 caracteres [10:48:19] Proyecto seleccionado: D:/Trabajo/VM/45 - HENKEL - VM Auto Changeover/InLavoro/PLC/Second Test/93064_TL25_Q1_25_V18/93064_TL25_Q1_25_V18.ap18
[15:35:04] - Hash generado: ba7f9f899c63a04a454f2e2a9d50856c [10:48:19] Usando directorio base de exportación: D:\Trabajo\VM\45 - HENKEL - VM Auto Changeover\ExportTia
[15:35:04] 📧 Procesamiento completado: 1 mensajes extraídos [10:48:19] Conectando a TIA Portal V18.0...
[15:35:04] Extracted 1 messages from Planning attività 08-08-2025 Teknors.eml [10:48:19] 2025-08-23 10:48:19,421 [1] INFO Siemens.TiaPortal.OpennessApi18.Implementations.Global OpenPortal - Start TIA Portal, please acknowledge the security dialog.
[15:35:04] --- Msg 1/1 from Planning attività 08-08-2025 Teknors.eml --- [10:48:19] 2025-08-23 10:48:19,432 [1] INFO Siemens.TiaPortal.OpennessApi18.Implementations.Global OpenPortal - With user interface
[15:35:04] Remitente: Passera, Alessandro [10:48:33] Conectado a TIA Portal.
[15:35:04] Fecha: 2025-08-08 13:06:58 [10:48:33] 2025-08-23 10:48:33,909 [1] INFO Siemens.TiaPortal.OpennessApi18.Implementations.Portal GetProcessId - Process id: 35128
[15:35:04] Subject: Planning attività 08-08-2025 Teknors [10:48:33] ID del proceso del Portal: 35128
[15:35:04] Hash: ba7f9f899c63a04a454f2e2a9d50856c [10:48:34] 2025-08-23 10:48:34,166 [1] INFO Siemens.TiaPortal.OpennessApi18.Implementations.Portal OpenProject - Open project... D:/Trabajo/VM/45 - HENKEL - VM Auto Changeover/InLavoro/PLC/Second Test/93064_TL25_Q1_25_V18/93064_TL25_Q1_25_V18.ap18
[15:35:04] Adjuntos: [] [10:48:45] 2025-08-23 10:48:45,562 [1] INFO Siemens.TiaPortal.OpennessApi18.Implementations.Project GetPlcs - Found plc PLC_TL25_Q1 with parent name S71500/ET200MP station_1
[15:35:04] ✓ NUEVO mensaje - Agregando a la cronología [10:48:52] Se encontraron 1 PLC(s). Iniciando proceso de exportación de referencias cruzadas...
[15:35:04] Estadísticas de procesamiento: [10:48:52] --- Procesando PLC: PLC_TL25_Q1 ---
[15:35:04] - Total mensajes encontrados: 1 [10:48:52] [PLC: PLC_TL25_Q1] Exportando referencias cruzadas de bloques de programa...
[15:35:04] - Mensajes únicos añadidos: 1 [10:48:52] Destino: D:\Trabajo\VM\45 - HENKEL - VM Auto Changeover\ExportTia\PLC_TL25_Q1\ProgramBlocks_CR
[15:35:04] - Mensajes duplicados ignorados: 0 [10:48:52] Se encontraron 233 bloques de programa.
[15:35:04] Writing 1 messages to C:\Users\migue\OneDrive\Miguel\Obsidean\Trabajo\VM\04-SIDEL\0 - PROJECTS Description\PLANNING\Planning - emails.md [10:48:52] Procesando bloque: ProDiagOB...
[15:35:04] ✅ Cronología guardada exitosamente en: C:\Users\migue\OneDrive\Miguel\Obsidean\Trabajo\VM\04-SIDEL\0 - PROJECTS Description\PLANNING\Planning - emails.md [10:48:52] Exportando referencias cruzadas para ProDiagOB...
[15:35:04] 📊 Total de mensajes en la cronología: 1 [10:48:53] Exportación completada en 0.51 segundos
[15:35:04] Ejecución de x1.py finalizada (success). Duración: 0:00:00.245126. [10:48:53] Procesando bloque: Rt_Enable_RemoteFormatChange...
[15:35:04] Log completo guardado en: D:\Proyectos\Scripts\ParamManagerScripts\backend\script_groups\EmailCrono\.log\log_x1.txt [10:48:53] Exportando referencias cruzadas para Rt_Enable_RemoteFormatChange...
[15:35:10] Iniciando ejecución de x1.py en C:\Users\migue\OneDrive\Miguel\Obsidean\Trabajo\VM\04-SIDEL\0 - PROJECTS Description\PLANNING... [10:48:53] Exportación completada en 0.36 segundos
[15:35:11] ✅ Configuración cargada exitosamente [10:48:53] Procesando bloque: Rt_PopUp_RemoteFormatChange...
[15:35:11] Working/Output directory: C:\Users\migue\OneDrive\Miguel\Obsidean\Trabajo\VM\04-SIDEL\0 - PROJECTS Description\PLANNING [10:48:53] Exportando referencias cruzadas para Rt_PopUp_RemoteFormatChange...
[15:35:11] Input directory: C:\Trabajo\SIDEL\PROJECTs Planning\Emails [10:48:53] Exportación completada en 0.05 segundos
[15:35:11] Output file: C:\Users\migue\OneDrive\Miguel\Obsidean\Trabajo\VM\04-SIDEL\0 - PROJECTS Description\PLANNING\Planning - emails.md [10:48:53] Procesando bloque: Rt_LoadRemoteRecipe...
[15:35:11] Attachments directory: C:\Users\migue\OneDrive\Miguel\Obsidean\Trabajo\VM\04-SIDEL\0 - PROJECTS Description\PLANNING\adjuntos [10:48:53] Exportando referencias cruzadas para Rt_LoadRemoteRecipe...
[15:35:11] Beautify rules file: D:\Proyectos\Scripts\ParamManagerScripts\backend\script_groups\EmailCrono\config\beautify_rules.json [10:48:53] Exportación completada en 0.05 segundos
[15:35:11] Found 1 .eml files [10:48:53] Procesando bloque: Rt_RestartRemoteFormatChange...
[15:35:11] Creando cronología nueva (archivo se sobrescribirá) [10:48:53] Exportando referencias cruzadas para Rt_RestartRemoteFormatChange...
[15:35:11] ============================================================ [10:48:53] Exportación completada en 0.04 segundos
[15:35:11] Processing file: C:\Trabajo\SIDEL\PROJECTs Planning\Emails\Planning attività 08-08-2025 Teknors.eml [10:48:53] Procesando bloque: CounterManagementQE1_D...
[15:35:11] 📧 Abriendo archivo: C:\Trabajo\SIDEL\PROJECTs Planning\Emails\Planning attività 08-08-2025 Teknors.eml [10:48:53] Exportando referencias cruzadas para CounterManagementQE1_D...
[15:35:11] ✉️ Mensaje extraído: [10:48:54] Exportación completada en 0.45 segundos
[15:35:11] - Subject: Planning attività 08-08-2025 Teknors [10:48:54] Procesando bloque: CounterManagementQE1_G...
[15:35:11] - Remitente: "Passera, Alessandro" <Alessandro.Passera@sidel.com> [10:48:54] Exportando referencias cruzadas para CounterManagementQE1_G...
[15:35:11] - Fecha: 2025-08-08 13:06:58 [10:48:54] Exportación completada en 0.14 segundos
[15:35:11] - Adjuntos: 0 archivos [10:48:54] Procesando bloque: FormatManagementQE1_G...
[15:35:11] - Contenido: 20738 caracteres [10:48:54] Exportando referencias cruzadas para FormatManagementQE1_G...
[15:35:11] - Hash generado: ba7f9f899c63a04a454f2e2a9d50856c [10:48:56] Exportación completada en 1.72 segundos
[15:35:11] 📧 Procesamiento completado: 1 mensajes extraídos [10:48:56] Procesando bloque: FormatManagementQE1_D...
[15:35:11] Extracted 1 messages from Planning attività 08-08-2025 Teknors.eml [10:48:56] Exportando referencias cruzadas para FormatManagementQE1_D...
[15:35:11] --- Msg 1/1 from Planning attività 08-08-2025 Teknors.eml --- [10:48:57] Exportación completada en 1.36 segundos
[15:35:11] Remitente: Passera, Alessandro [10:48:57] Procesando bloque: Default_SupervisionFB...
[15:35:11] Fecha: 2025-08-08 13:06:58 [10:48:57] Exportando referencias cruzadas para Default_SupervisionFB...
[15:35:11] Subject: Planning attività 08-08-2025 Teknors [10:48:57] Exportación completada en 0.05 segundos
[15:35:11] Hash: ba7f9f899c63a04a454f2e2a9d50856c [10:48:57] Procesando bloque: 1000_FC Program Manager...
[15:35:11] Adjuntos: [] [10:48:57] Exportando referencias cruzadas para 1000_FC Program Manager...
[15:35:11] ✓ NUEVO mensaje - Agregando a la cronología [10:48:57] Exportación completada en 0.15 segundos
[15:35:11] Estadísticas de procesamiento: [10:48:57] Procesando bloque: 1001_FC Gateway Data Read...
[15:35:11] - Total mensajes encontrados: 1 [10:48:57] Exportando referencias cruzadas para 1001_FC Gateway Data Read...
[15:35:11] - Mensajes únicos añadidos: 1 [10:48:57] Exportación completada en 0.14 segundos
[15:35:11] - Mensajes duplicados ignorados: 0 [10:48:57] Procesando bloque: 1002_FC Data Read conversion...
[15:35:11] Writing 1 messages to C:\Users\migue\OneDrive\Miguel\Obsidean\Trabajo\VM\04-SIDEL\0 - PROJECTS Description\PLANNING\Planning - emails.md [10:48:57] Exportando referencias cruzadas para 1002_FC Data Read conversion...
[15:35:11] ✅ Cronología guardada exitosamente en: C:\Users\migue\OneDrive\Miguel\Obsidean\Trabajo\VM\04-SIDEL\0 - PROJECTS Description\PLANNING\Planning - emails.md [10:48:58] Exportación completada en 0.34 segundos
[15:35:11] 📊 Total de mensajes en la cronología: 1 [10:48:58] Procesando bloque: 1003_FC Remote Control Read...
[15:35:11] Ejecución de x1.py finalizada (success). Duración: 0:00:00.264072. [10:48:58] Exportando referencias cruzadas para 1003_FC Remote Control Read...
[15:35:11] Log completo guardado en: D:\Proyectos\Scripts\ParamManagerScripts\backend\script_groups\EmailCrono\.log\log_x1.txt [10:48:58] Exportación completada en 0.20 segundos
[10:48:58] Procesando bloque: 1010_FC Alarms...
[10:48:58] Exportando referencias cruzadas para 1010_FC Alarms...
[10:49:00] Exportación completada en 1.67 segundos
[10:49:00] Procesando bloque: 1020_FC Format Parameters...
[10:49:00] Exportando referencias cruzadas para 1020_FC Format Parameters...
[10:49:00] Exportación completada en 0.18 segundos
[10:49:00] Procesando bloque: 1021_FC Area Parameters...
[10:49:00] Exportando referencias cruzadas para 1021_FC Area Parameters...
[10:49:00] Exportación completada en 0.42 segundos
[10:49:00] Procesando bloque: 1030_FC Aut/Man selection...
[10:49:00] Exportando referencias cruzadas para 1030_FC Aut/Man selection...
[10:49:00] Exportación completada en 0.10 segundos
[10:49:00] Procesando bloque: 1032_FC Manual function...
[10:49:00] Exportando referencias cruzadas para 1032_FC Manual function...
[10:49:01] Exportación completada en 0.43 segundos
[10:49:01] Procesando bloque: 1035_FC Automatic Cycle...
[10:49:01] Exportando referencias cruzadas para 1035_FC Automatic Cycle...
[10:49:01] Exportación completada en 0.19 segundos
[10:49:01] Procesando bloque: 1036_FC Area Cycle...
[10:49:01] Exportando referencias cruzadas para 1036_FC Area Cycle...
[10:49:02] Exportación completada en 1.02 segundos
[10:49:02] Procesando bloque: 1050_FC HMI...
[10:49:02] Exportando referencias cruzadas para 1050_FC HMI...
[10:49:03] Exportación completada en 0.63 segundos
[10:49:03] Procesando bloque: 1090_FC Alarms to SV...
[10:49:03] Exportando referencias cruzadas para 1090_FC Alarms to SV...
[10:49:03] Exportación completada en 0.76 segundos
[10:49:03] Procesando bloque: 1100_FC Remote Control Write...
[10:49:03] Exportando referencias cruzadas para 1100_FC Remote Control Write...
[10:49:04] Exportación completada en 0.19 segundos
[10:49:04] Procesando bloque: 1101_FC Data Write conversion...
[10:49:04] Exportando referencias cruzadas para 1101_FC Data Write conversion...
[10:49:04] Exportación completada en 0.37 segundos
[10:49:04] Procesando bloque: 1102_FC Gateway Data Write...
[10:49:04] Exportando referencias cruzadas para 1102_FC Gateway Data Write...
[10:49:04] Exportación completada en 0.15 segundos
[10:49:04] Procesando bloque: Default_SupervisionDB...
[10:49:04] Exportando referencias cruzadas para Default_SupervisionDB...
[10:49:04] Exportación completada en 0.24 segundos
[10:49:04] Procesando bloque: DB Gateway...
[10:49:04] Exportando referencias cruzadas para DB Gateway...
[10:49:24] Tiempo transcurrido antes del error: 19.46 segundos
[10:49:24] ERROR GENERAL al exportar referencias cruzadas para el bloque DB Gateway: OpennessAccessException: Unexpected exception.
[10:49:24] Object name: 'Siemens.Engineering.CrossReference.SourceObjectComposition'.
[10:49:24] ERROR al acceder a los bloques de programa para exportar referencias cruzadas: OpennessAccessException: Access to a disposed object of type 'Siemens.Engineering.SW.Blocks.GlobalDB' is not possible.
[10:49:24] Marcando bloque problemático: DB Gateway
[10:49:24] Cerrando instancia actual de TIA Portal...
[10:49:24] 2025-08-23 10:49:24,232 [1] INFO Siemens.TiaPortal.OpennessApi18.Implementations.Portal ClosePortal - Close TIA Portal
[10:49:24] Re-abriendo TIA Portal (intento 1/5)...
[10:49:24] Conectando a TIA Portal V18.0...
[10:49:24] 2025-08-23 10:49:24,281 [1] INFO Siemens.TiaPortal.OpennessApi18.Implementations.Global OpenPortal - Start TIA Portal, please acknowledge the security dialog.
[10:49:24] 2025-08-23 10:49:24,282 [1] INFO Siemens.TiaPortal.OpennessApi18.Implementations.Global OpenPortal - With user interface
[10:49:39] Conectado a TIA Portal.
[10:49:39] 2025-08-23 10:49:39,458 [1] INFO Siemens.TiaPortal.OpennessApi18.Implementations.Portal GetProcessId - Process id: 30020
[10:49:39] ID del proceso del Portal: 30020
[10:49:39] 2025-08-23 10:49:39,602 [1] INFO Siemens.TiaPortal.OpennessApi18.Implementations.Portal OpenProject - Open project... D:/Trabajo/VM/45 - HENKEL - VM Auto Changeover/InLavoro/PLC/Second Test/93064_TL25_Q1_25_V18/93064_TL25_Q1_25_V18.ap18
[10:49:50] 2025-08-23 10:49:50,032 [1] INFO Siemens.TiaPortal.OpennessApi18.Implementations.Project GetPlcs - Found plc PLC_TL25_Q1 with parent name S71500/ET200MP station_1
[10:49:57] --- Procesando PLC: PLC_TL25_Q1 ---
[10:49:57] [PLC: PLC_TL25_Q1] Exportando referencias cruzadas de bloques de programa...
[10:49:57] Destino: D:\Trabajo\VM\45 - HENKEL - VM Auto Changeover\ExportTia\PLC_TL25_Q1\ProgramBlocks_CR
[10:49:58] Se encontraron 233 bloques de programa.
[10:49:58] Bloques que serán omitidos (problemáticos previos): DB Gateway
[10:49:58] Omitiendo bloque ya exportado: ProDiagOB
[10:49:58] Omitiendo bloque ya exportado: Rt_Enable_RemoteFormatChange
[10:49:58] Omitiendo bloque ya exportado: Rt_PopUp_RemoteFormatChange
[10:49:58] Omitiendo bloque ya exportado: Rt_LoadRemoteRecipe
[10:49:58] Omitiendo bloque ya exportado: Rt_RestartRemoteFormatChange
[10:49:58] Omitiendo bloque ya exportado: CounterManagementQE1_D
[10:49:58] Omitiendo bloque ya exportado: CounterManagementQE1_G
[10:49:58] Omitiendo bloque ya exportado: FormatManagementQE1_G
[10:49:58] Omitiendo bloque ya exportado: FormatManagementQE1_D
[10:49:58] Omitiendo bloque ya exportado: Default_SupervisionFB
[10:49:58] Omitiendo bloque ya exportado: 1000_FC Program Manager
[10:49:58] Omitiendo bloque ya exportado: 1001_FC Gateway Data Read
[10:49:58] Omitiendo bloque ya exportado: 1002_FC Data Read conversion
[10:49:58] Omitiendo bloque ya exportado: 1003_FC Remote Control Read
[10:49:58] Omitiendo bloque ya exportado: 1010_FC Alarms
[10:49:58] Omitiendo bloque ya exportado: 1020_FC Format Parameters
[10:49:58] Omitiendo bloque ya exportado: 1021_FC Area Parameters
[10:49:58] Omitiendo bloque ya exportado: 1030_FC Aut/Man selection
[10:49:58] Omitiendo bloque ya exportado: 1032_FC Manual function
[10:49:58] Omitiendo bloque ya exportado: 1035_FC Automatic Cycle
[10:49:58] Omitiendo bloque ya exportado: 1036_FC Area Cycle
[10:49:58] Omitiendo bloque ya exportado: 1050_FC HMI
[10:49:58] Omitiendo bloque ya exportado: 1090_FC Alarms to SV
[10:49:58] Omitiendo bloque ya exportado: 1100_FC Remote Control Write
[10:49:58] Omitiendo bloque ya exportado: 1101_FC Data Write conversion
[10:49:58] Omitiendo bloque ya exportado: 1102_FC Gateway Data Write
[10:49:58] Omitiendo bloque ya exportado: Default_SupervisionDB
[10:49:58] Omitiendo bloque problemático previamente detectado: DB Gateway
[10:49:58] Procesando bloque: DB LinePar...
[10:49:58] Exportando referencias cruzadas para DB LinePar...
[10:49:59] Exportación completada en 0.93 segundos
[10:49:59] Procesando bloque: DB MotorPar...
[10:49:59] Exportando referencias cruzadas para DB MotorPar...

View File

@ -1,7 +1,7 @@
import os import os
import json import json
from typing import Dict, Any, List, Optional from typing import Dict, Any, List, Optional
import re # Necesario para extraer docstring import re # Necesario para extraer docstring
# Import the new modules # Import the new modules
from .logger import Logger from .logger import Logger
@ -13,7 +13,7 @@ from .script_executor import ScriptExecutor
# Keep time for execution throttling state # Keep time for execution throttling state
import time import time
from datetime import datetime # Needed for append_log timestamp if we keep it here from datetime import datetime # Needed for append_log timestamp if we keep it here
# --- ConfigurationManager Class --- # --- ConfigurationManager Class ---
@ -35,18 +35,31 @@ class ConfigurationManager:
self.min_execution_interval = 1 self.min_execution_interval = 1
# Instantiate handlers/managers # Instantiate handlers/managers
self.logger = Logger(os.path.join(self.data_path, "log.txt")) # Pass log path to Logger self.logger = Logger(
self.dir_manager = DirectoryManager(self.script_groups_path, self._set_working_directory_internal) os.path.join(self.data_path, "log.txt")
) # Pass log path to Logger
self.dir_manager = DirectoryManager(
self.script_groups_path, self._set_working_directory_internal
)
self.group_manager = GroupManager(self.script_groups_path) self.group_manager = GroupManager(self.script_groups_path)
self.schema_handler = SchemaHandler(self.data_path, self.script_groups_path, self._get_working_directory_internal) self.schema_handler = SchemaHandler(
self.config_handler = ConfigHandler(self.data_path, self.script_groups_path, self._get_working_directory_internal, self.schema_handler) self.data_path,
self.script_groups_path,
self._get_working_directory_internal,
)
self.config_handler = ConfigHandler(
self.data_path,
self.script_groups_path,
self._get_working_directory_internal,
self.schema_handler,
)
self.script_executor = ScriptExecutor( self.script_executor = ScriptExecutor(
self.script_groups_path, self.script_groups_path,
self.dir_manager, self.dir_manager,
self.config_handler, self.config_handler,
self.logger, # Pass the central logger instance self.logger, # Pass the central logger instance
self._get_execution_state_internal, self._get_execution_state_internal,
self._set_last_execution_time_internal self._set_last_execution_time_internal,
) )
# --- Internal Callbacks/Getters for Sub-Managers --- # --- Internal Callbacks/Getters for Sub-Managers ---
@ -59,9 +72,11 @@ class ConfigurationManager:
data_json_path = os.path.join(path, "data.json") data_json_path = os.path.join(path, "data.json")
if not os.path.exists(data_json_path): if not os.path.exists(data_json_path):
try: try:
with open(data_json_path, 'w', encoding='utf-8') as f: with open(data_json_path, "w", encoding="utf-8") as f:
json.dump({}, f) json.dump({}, f)
print(f"Info: Created empty data.json in new working directory: {data_json_path}") print(
f"Info: Created empty data.json in new working directory: {data_json_path}"
)
except Exception as e: except Exception as e:
print(f"Warning: Could not create data.json in {path}: {e}") print(f"Warning: Could not create data.json in {path}: {e}")
else: else:
@ -73,7 +88,10 @@ class ConfigurationManager:
def _get_execution_state_internal(self) -> Dict[str, Any]: def _get_execution_state_internal(self) -> Dict[str, Any]:
"""Provides execution throttling state to ScriptExecutor.""" """Provides execution throttling state to ScriptExecutor."""
return {"last_time": self.last_execution_time, "interval": self.min_execution_interval} return {
"last_time": self.last_execution_time,
"interval": self.min_execution_interval,
}
def _set_last_execution_time_internal(self, exec_time: float): def _set_last_execution_time_internal(self, exec_time: float):
"""Callback for ScriptExecutor to update the last execution time.""" """Callback for ScriptExecutor to update the last execution time."""
@ -127,9 +145,13 @@ class ConfigurationManager:
details.setdefault("author", "Unknown") details.setdefault("author", "Unknown")
return details return details
def update_group_description(self, group: str, data: Dict[str, Any]) -> Dict[str, str]: def update_group_description(
self, group: str, data: Dict[str, Any]
) -> Dict[str, str]:
"""Update the description file for a specific group.""" """Update the description file for a specific group."""
description_path = os.path.join(self.script_groups_path, group, "description.json") description_path = os.path.join(
self.script_groups_path, group, "description.json"
)
try: try:
os.makedirs(os.path.dirname(description_path), exist_ok=True) os.makedirs(os.path.dirname(description_path), exist_ok=True)
with open(description_path, "w", encoding="utf-8") as f: with open(description_path, "w", encoding="utf-8") as f:
@ -174,14 +196,14 @@ class ConfigurationManager:
group_path = self._get_group_path(group_id) group_path = self._get_group_path(group_id)
if not group_path: if not group_path:
return None return None
return os.path.join(group_path, 'scripts_description.json') return os.path.join(group_path, "scripts_description.json")
def _load_script_descriptions(self, group_id: str) -> Dict[str, Any]: def _load_script_descriptions(self, group_id: str) -> Dict[str, Any]:
"""Carga las descripciones de scripts desde scripts_description.json.""" """Carga las descripciones de scripts desde scripts_description.json."""
path = self._get_script_descriptions_path(group_id) path = self._get_script_descriptions_path(group_id)
if path and os.path.exists(path): if path and os.path.exists(path):
try: try:
with open(path, 'r', encoding='utf-8') as f: with open(path, "r", encoding="utf-8") as f:
return json.load(f) return json.load(f)
except json.JSONDecodeError: except json.JSONDecodeError:
print(f"Error: JSON inválido en {path}") print(f"Error: JSON inválido en {path}")
@ -191,13 +213,17 @@ class ConfigurationManager:
return {} return {}
return {} return {}
def _save_script_descriptions(self, group_id: str, descriptions: Dict[str, Any]) -> bool: def _save_script_descriptions(
self, group_id: str, descriptions: Dict[str, Any]
) -> bool:
"""Guarda las descripciones de scripts en scripts_description.json.""" """Guarda las descripciones de scripts en scripts_description.json."""
path = self._get_script_descriptions_path(group_id) path = self._get_script_descriptions_path(group_id)
if path: if path:
try: try:
os.makedirs(os.path.dirname(path), exist_ok=True) # Asegura que el directorio del grupo existe os.makedirs(
with open(path, 'w', encoding='utf-8') as f: os.path.dirname(path), exist_ok=True
) # Asegura que el directorio del grupo existe
with open(path, "w", encoding="utf-8") as f:
json.dump(descriptions, f, indent=4, ensure_ascii=False) json.dump(descriptions, f, indent=4, ensure_ascii=False)
return True return True
except Exception as e: except Exception as e:
@ -208,15 +234,26 @@ class ConfigurationManager:
def _extract_short_description(self, script_path: str) -> str: def _extract_short_description(self, script_path: str) -> str:
"""Extrae la primera línea del docstring de un script Python.""" """Extrae la primera línea del docstring de un script Python."""
try: try:
with open(script_path, 'r', encoding='utf-8') as f: with open(script_path, "r", encoding="utf-8") as f:
content = f.read() content = f.read()
# Buscar docstring al inicio del archivo """...""" o '''...''' # Buscar docstring al inicio del archivo """...""" o '''...'''
match = re.match(r'^\s*("""(.*?)"""|\'\'\'(.*?)\'\'\')', content, re.DOTALL | re.MULTILINE) match = re.match(
r'^\s*("""(.*?)"""|\'\'\'(.*?)\'\'\')',
content,
re.DOTALL | re.MULTILINE,
)
if match: if match:
# Obtener el contenido del docstring (grupo 2 o 3) # Obtener el contenido del docstring (grupo 2 o 3)
docstring = match.group(2) or match.group(3) docstring = match.group(2) or match.group(3)
# Tomar la primera línea no vacía # Tomar la primera línea no vacía
first_line = next((line.strip() for line in docstring.strip().splitlines() if line.strip()), None) first_line = next(
(
line.strip()
for line in docstring.strip().splitlines()
if line.strip()
),
None,
)
return first_line if first_line else "Sin descripción corta." return first_line if first_line else "Sin descripción corta."
except Exception as e: except Exception as e:
print(f"Error extrayendo descripción de {script_path}: {e}") print(f"Error extrayendo descripción de {script_path}: {e}")
@ -234,36 +271,52 @@ class ConfigurationManager:
try: try:
# Listar archivos .py en el directorio del grupo # Listar archivos .py en el directorio del grupo
script_files = [f for f in os.listdir(group_path) if f.endswith('.py') and os.path.isfile(os.path.join(group_path, f))] script_files = [
f
for f in os.listdir(group_path)
if f.endswith(".py") and os.path.isfile(os.path.join(group_path, f))
]
for filename in script_files: for filename in script_files:
script_path = os.path.join(group_path, filename) script_path = os.path.join(group_path, filename)
if filename not in descriptions: if filename not in descriptions:
print(f"Script '{filename}' no encontrado en descripciones, auto-populando.") print(
f"Script '{filename}' no encontrado en descripciones, auto-populando."
)
short_desc = self._extract_short_description(script_path) short_desc = self._extract_short_description(script_path)
descriptions[filename] = { descriptions[filename] = {
"display_name": filename.replace('.py', ''), # Nombre por defecto "display_name": filename.replace(
".py", ""
), # Nombre por defecto
"short_description": short_desc, "short_description": short_desc,
"long_description": "", "long_description": "",
"hidden": False "hidden": False,
} }
updated = True updated = True
# Añadir a la lista si no está oculto # Añadir a la lista si no está oculto
details = descriptions[filename] details = descriptions[filename]
if not details.get('hidden', False): if not details.get("hidden", False):
scripts_details.append({ scripts_details.append(
"filename": filename, # Nombre real del archivo {
"display_name": details.get("display_name", filename.replace('.py', '')), "filename": filename, # Nombre real del archivo
"short_description": details.get("short_description", "Sin descripción corta."), "display_name": details.get(
"long_description": details.get("long_description", "") # Añadir descripción larga "display_name", filename.replace(".py", "")
}) ),
"short_description": details.get(
"short_description", "Sin descripción corta."
),
"long_description": details.get(
"long_description", ""
), # Añadir descripción larga
}
)
if updated: if updated:
self._save_script_descriptions(group, descriptions) self._save_script_descriptions(group, descriptions)
# Ordenar por display_name para consistencia # Ordenar por display_name para consistencia
scripts_details.sort(key=lambda x: x['display_name']) scripts_details.sort(key=lambda x: x["display_name"])
return scripts_details return scripts_details
except FileNotFoundError: except FileNotFoundError:
@ -276,49 +329,88 @@ class ConfigurationManager:
"""Obtiene los detalles completos de un script específico.""" """Obtiene los detalles completos de un script específico."""
descriptions = self._load_script_descriptions(group_id) descriptions = self._load_script_descriptions(group_id)
# Devolver detalles o un diccionario por defecto si no existe (aunque list_scripts debería crearlo) # Devolver detalles o un diccionario por defecto si no existe (aunque list_scripts debería crearlo)
return descriptions.get(script_filename, { return descriptions.get(
"display_name": script_filename.replace('.py', ''), script_filename,
"short_description": "No encontrado.", {
"long_description": "", "display_name": script_filename.replace(".py", ""),
"hidden": False "short_description": "No encontrado.",
}) "long_description": "",
"hidden": False,
},
)
def update_script_details(self, group_id: str, script_filename: str, details: Dict[str, Any]) -> Dict[str, str]: def update_script_details(
self, group_id: str, script_filename: str, details: Dict[str, Any]
) -> Dict[str, str]:
"""Actualiza los detalles de un script específico.""" """Actualiza los detalles de un script específico."""
descriptions = self._load_script_descriptions(group_id) descriptions = self._load_script_descriptions(group_id)
if script_filename in descriptions: if script_filename in descriptions:
# Asegurarse de que los campos esperados están presentes y actualizar # Asegurarse de que los campos esperados están presentes y actualizar
descriptions[script_filename]["display_name"] = details.get("display_name", descriptions[script_filename].get("display_name", script_filename.replace('.py', ''))) descriptions[script_filename]["display_name"] = details.get(
descriptions[script_filename]["short_description"] = details.get("short_description", descriptions[script_filename].get("short_description", "")) # Actualizar descripción corta "display_name",
descriptions[script_filename]["long_description"] = details.get("long_description", descriptions[script_filename].get("long_description", "")) descriptions[script_filename].get(
descriptions[script_filename]["hidden"] = details.get("hidden", descriptions[script_filename].get("hidden", False)) "display_name", script_filename.replace(".py", "")
),
)
descriptions[script_filename]["short_description"] = details.get(
"short_description",
descriptions[script_filename].get("short_description", ""),
) # Actualizar descripción corta
descriptions[script_filename]["long_description"] = details.get(
"long_description",
descriptions[script_filename].get("long_description", ""),
)
descriptions[script_filename]["hidden"] = details.get(
"hidden", descriptions[script_filename].get("hidden", False)
)
if self._save_script_descriptions(group_id, descriptions): if self._save_script_descriptions(group_id, descriptions):
return {"status": "success"} return {"status": "success"}
else: else:
return {"status": "error", "message": "Fallo al guardar las descripciones de los scripts."} return {
"status": "error",
"message": "Fallo al guardar las descripciones de los scripts.",
}
else: else:
# Intentar crear la entrada si el script existe pero no está en el JSON (caso raro) # Intentar crear la entrada si el script existe pero no está en el JSON (caso raro)
group_path = self._get_group_path(group_id) group_path = self._get_group_path(group_id)
script_path = os.path.join(group_path, script_filename) if group_path else None script_path = (
os.path.join(group_path, script_filename) if group_path else None
)
if script_path and os.path.exists(script_path): if script_path and os.path.exists(script_path):
print(f"Advertencia: El script '{script_filename}' existe pero no estaba en descriptions.json. Creando entrada.") print(
short_desc = self._extract_short_description(script_path) f"Advertencia: El script '{script_filename}' existe pero no estaba en descriptions.json. Creando entrada."
descriptions[script_filename] = { )
"display_name": details.get("display_name", script_filename.replace('.py', '')), short_desc = self._extract_short_description(script_path)
"short_description": short_desc, # Usar la extraída descriptions[script_filename] = {
"display_name": details.get(
"display_name", script_filename.replace(".py", "")
),
"short_description": short_desc, # Usar la extraída
"long_description": details.get("long_description", ""), "long_description": details.get("long_description", ""),
"hidden": details.get("hidden", False) "hidden": details.get("hidden", False),
} }
if self._save_script_descriptions(group_id, descriptions): if self._save_script_descriptions(group_id, descriptions):
return {"status": "success"} return {"status": "success"}
else: else:
return {"status": "error", "message": "Fallo al guardar las descripciones de los scripts después de crear la entrada."} return {
"status": "error",
"message": "Fallo al guardar las descripciones de los scripts después de crear la entrada.",
}
else: else:
return {"status": "error", "message": f"Script '{script_filename}' no encontrado en las descripciones ni en el sistema de archivos."} return {
"status": "error",
"message": f"Script '{script_filename}' no encontrado en las descripciones ni en el sistema de archivos.",
}
def execute_script( def execute_script(
self, group: str, script_name: str, broadcast_fn=None self, group: str, script_name: str, broadcast_fn=None
) -> Dict[str, Any]: ) -> Dict[str, Any]:
# ScriptExecutor uses callbacks to get/set execution state # ScriptExecutor uses callbacks to get/set execution state
return self.script_executor.execute_script(group, script_name, broadcast_fn) return self.script_executor.execute_script(group, script_name, broadcast_fn)
def stop_script(
self, group: str, script_name: str, broadcast_fn=None
) -> Dict[str, Any]:
# Delegar al ScriptExecutor para detener el script
return self.script_executor.stop_script(group, script_name, broadcast_fn)

View File

@ -33,6 +33,10 @@ class ScriptExecutor:
self._get_exec_state = get_exec_state_func self._get_exec_state = get_exec_state_func
self._set_last_exec_time = set_last_exec_time_func self._set_last_exec_time = set_last_exec_time_func
# Diccionario para rastrear procesos en ejecución
# Key: f"{group}:{script_name}", Value: subprocess.Popen object
self.running_processes = {}
def execute_script( def execute_script(
self, self,
group: str, group: str,
@ -118,6 +122,7 @@ class ScriptExecutor:
stderr_capture = "" stderr_capture = ""
process = None process = None
start_time = datetime.now() start_time = datetime.now()
process_key = f"{group}:{script_name}"
try: try:
if broadcast_fn: if broadcast_fn:
@ -141,6 +146,9 @@ class ScriptExecutor:
creationflags=creation_flags, creationflags=creation_flags,
) )
# Registrar el proceso en ejecución
self.running_processes[process_key] = process
while True: while True:
line = process.stdout.readline() line = process.stdout.readline()
if not line and process.poll() is not None: if not line and process.poll() is not None:
@ -232,7 +240,74 @@ class ScriptExecutor:
return {"status": "error", "error": error_msg, "traceback": traceback_info} return {"status": "error", "error": error_msg, "traceback": traceback_info}
finally: finally:
# Remover el proceso del registro cuando termine
if process_key in self.running_processes:
del self.running_processes[process_key]
if process and process.stderr: if process and process.stderr:
process.stderr.close() process.stderr.close()
if process and process.stdout: if process and process.stdout:
process.stdout.close() process.stdout.close()
def stop_script(
self,
group: str,
script_name: str,
broadcast_fn: Optional[Callable[[str], None]] = None,
) -> Dict[str, Any]:
"""
Detiene un script en ejecución.
"""
process_key = f"{group}:{script_name}"
if process_key not in self.running_processes:
msg = f"El script {script_name} no está ejecutándose actualmente"
self.app_logger.append_log(f"Warning: {msg}")
if broadcast_fn:
broadcast_fn(msg)
return {"status": "error", "error": "Script not running"}
process = self.running_processes[process_key]
try:
# Verificar que el proceso aún esté vivo
if process.poll() is not None:
# El proceso ya terminó naturalmente
del self.running_processes[process_key]
msg = f"El script {script_name} ya había terminado"
self.app_logger.append_log(f"Info: {msg}")
if broadcast_fn:
broadcast_fn(msg)
return {"status": "already_finished", "message": msg}
# Intentar terminar el proceso suavemente
process.terminate()
# Esperar un poco para ver si termina suavemente
try:
process.wait(timeout=5) # Esperar 5 segundos
msg = f"Script {script_name} detenido correctamente"
self.app_logger.append_log(f"Info: {msg}")
if broadcast_fn:
broadcast_fn(msg)
return {"status": "success", "message": msg}
except subprocess.TimeoutExpired:
# Si no termina suavemente, forzar la terminación
process.kill()
process.wait() # Esperar a que termine definitivamente
msg = f"Script {script_name} forzado a terminar"
self.app_logger.append_log(f"Warning: {msg}")
if broadcast_fn:
broadcast_fn(msg)
return {"status": "forced_kill", "message": msg}
except Exception as e:
error_msg = f"Error al detener el script {script_name}: {str(e)}"
self.app_logger.append_log(f"ERROR: {error_msg}")
if broadcast_fn:
broadcast_fn(error_msg)
return {"status": "error", "error": error_msg}
finally:
# Asegurarse de que el proceso se elimine del registro
if process_key in self.running_processes:
del self.running_processes[process_key]

View File

@ -1,5 +1,8 @@
let currentGroup; let currentGroup;
// Registro de procesos en ejecución para scripts de configuración
let runningConfigScripts = new Set();
// Initialize WebSocket connection // Initialize WebSocket connection
let socket = null; // Define socket en un alcance accesible (p.ej., globalmente o en el scope del módulo) let socket = null; // Define socket en un alcance accesible (p.ej., globalmente o en el scope del módulo)
@ -206,11 +209,20 @@ async function loadScripts(group) {
</div> </div>
<div class="flex items-center gap-2 flex-shrink-0"> <div class="flex items-center gap-2 flex-shrink-0">
<div class="flex flex-col items-center"> <div class="flex flex-col items-center">
<button data-filename="${script.filename}" <div class="flex gap-1">
class="bg-green-500 hover:bg-green-600 text-white px-3 py-1 rounded text-sm w-24 text-center execute-button"> <button data-filename="${script.filename}"
Ejecutar class="bg-green-500 hover:bg-green-600 text-white px-2 py-1 rounded text-sm execute-button"
</button> title="Ejecutar script">
<div class="text-xs text-gray-500 mt-1 truncate w-24 text-center" title="${script.filename}">${script.filename}</div>
</button>
<button data-filename="${script.filename}"
class="bg-red-500 hover:bg-red-600 text-white px-2 py-1 rounded text-sm stop-button disabled:opacity-50 disabled:cursor-not-allowed"
disabled
title="Detener script">
</button>
</div>
<div class="text-xs text-gray-500 mt-1 truncate w-20 text-center" title="${script.filename}">${script.filename}</div>
</div> </div>
<button data-group="${group}" data-filename="${script.filename}" <button data-group="${group}" data-filename="${script.filename}"
class="p-1 rounded text-gray-500 hover:bg-gray-200 hover:text-gray-700 edit-button" title="Editar Detalles"> class="p-1 rounded text-gray-500 hover:bg-gray-200 hover:text-gray-700 edit-button" title="Editar Detalles">
@ -228,6 +240,11 @@ async function loadScripts(group) {
executeScript(script.filename); executeScript(script.filename);
}); });
const stopButton = div.querySelector('.stop-button');
stopButton.addEventListener('click', () => {
stopScript(script.filename);
});
const editButton = div.querySelector('.edit-button'); const editButton = div.querySelector('.edit-button');
editButton.addEventListener('click', () => { editButton.addEventListener('click', () => {
editScriptDetails(group, script.filename); editScriptDetails(group, script.filename);
@ -255,6 +272,10 @@ async function executeScript(scriptName) {
// REMOVE this line - let the backend log the start via WebSocket // REMOVE this line - let the backend log the start via WebSocket
// addLogLine(`\nEjecutando script: ${scriptName}...\n`); // addLogLine(`\nEjecutando script: ${scriptName}...\n`);
// Marcar script como en ejecución
runningConfigScripts.add(scriptName);
updateScriptButtons(scriptName, true);
try { try {
const response = await fetch('/api/execute_script', { const response = await fetch('/api/execute_script', {
method: 'POST', method: 'POST',
@ -268,6 +289,10 @@ async function executeScript(scriptName) {
console.error(`Error initiating script execution request: ${response.status} ${response.statusText}`, errorText); console.error(`Error initiating script execution request: ${response.status} ${response.statusText}`, errorText);
// Log only the request error, not script execution errors which come via WebSocket // Log only the request error, not script execution errors which come via WebSocket
addLogLine(`\nError al iniciar la petición del script: ${response.status} ${errorText}\n`); addLogLine(`\nError al iniciar la petición del script: ${response.status} ${errorText}\n`);
// Desmarcar script si falló el inicio
runningConfigScripts.delete(scriptName);
updateScriptButtons(scriptName, false);
return; // Stop if the request failed return; // Stop if the request failed
} }
@ -280,9 +305,95 @@ async function executeScript(scriptName) {
// Script output and final status/errors will arrive via WebSocket messages // Script output and final status/errors will arrive via WebSocket messages
// handled by socket.onmessage -> addLogLine // handled by socket.onmessage -> addLogLine
// Nota: El script se desmarcará cuando termine (en el backend deberíamos enviar una señal de finalización)
// Por ahora, lo desmarcaremos después de un tiempo o cuando el usuario haga clic en stop
} catch (error) { } catch (error) {
console.error('Error in executeScript fetch:', error); console.error('Error in executeScript fetch:', error);
addLogLine(`\nError de red o JavaScript al intentar ejecutar el script: ${error.message}\n`); addLogLine(`\nError de red o JavaScript al intentar ejecutar el script: ${error.message}\n`);
// Desmarcar script si hubo error
runningConfigScripts.delete(scriptName);
updateScriptButtons(scriptName, false);
}
}
// Stop a script
async function stopScript(scriptName) {
try {
addLogLine(`\nDeteniendo script: ${scriptName}...\n`);
const response = await fetch('/api/stop_script', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ group: currentGroup, script: scriptName })
});
if (!response.ok) {
const errorText = await response.text();
console.error(`Error stopping script: ${response.status} ${response.statusText}`, errorText);
addLogLine(`\nError al detener el script: ${response.status} ${errorText}\n`);
return;
}
const result = await response.json();
if (result.error) {
addLogLine(`\nError al detener script: ${result.error}\n`);
} else {
addLogLine(`\nScript ${scriptName} detenido con éxito.\n`);
}
// Desmarcar script como en ejecución
runningConfigScripts.delete(scriptName);
updateScriptButtons(scriptName, false);
} catch (error) {
console.error('Error stopping script:', error);
addLogLine(`\nError de red al intentar detener el script: ${error.message}\n`);
}
}
// Actualizar estado de botones (ejecutar/stop) para un script
function updateScriptButtons(scriptName, isRunning) {
const scriptItem = document.querySelector(`[data-filename="${scriptName}"]`).closest('.script-item');
if (!scriptItem) return;
const executeButton = scriptItem.querySelector('.execute-button');
const stopButton = scriptItem.querySelector('.stop-button');
if (isRunning) {
executeButton.disabled = true;
executeButton.classList.add('opacity-50', 'cursor-not-allowed');
stopButton.disabled = false;
stopButton.classList.remove('opacity-50', 'cursor-not-allowed');
} else {
executeButton.disabled = false;
executeButton.classList.remove('opacity-50', 'cursor-not-allowed');
stopButton.disabled = true;
stopButton.classList.add('opacity-50', 'cursor-not-allowed');
}
}
// Función para detectar cuando un script ha terminado mediante mensajes de WebSocket
function handleScriptCompletion(message) {
// Buscar patrones que indiquen que un script ha terminado
const completionPatterns = [
/Ejecución de (.+?) finalizada/,
/Script (.+?) detenido/,
/ERROR FATAL.*?en (.+?):/
];
for (const pattern of completionPatterns) {
const match = message.match(pattern);
if (match) {
const scriptName = match[1];
if (runningConfigScripts.has(scriptName)) {
runningConfigScripts.delete(scriptName);
updateScriptButtons(scriptName, false);
console.log(`Script ${scriptName} marcado como terminado`);
}
break;
}
} }
} }
@ -1078,6 +1189,9 @@ function addLogLine(message) {
// Append the cleaned message + a newline for display separation. // Append the cleaned message + a newline for display separation.
logArea.innerHTML += cleanMessage + '\n'; logArea.innerHTML += cleanMessage + '\n';
logArea.scrollTop = logArea.scrollHeight; // Ensure scroll to bottom logArea.scrollTop = logArea.scrollHeight; // Ensure scroll to bottom
// Detectar finalización de scripts
handleScriptCompletion(cleanMessage);
} }
} }