From 5ed4d9391e5787b47852d5c8d04ea04c1c0382f4 Mon Sep 17 00:00:00 2001 From: Miguel Date: Sat, 23 Aug 2025 10:51:38 +0200 Subject: [PATCH] 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. --- app.py | 18 + .../EmailCrono/script_config.json | 6 +- .../script_groups/EmailCrono/work_dir.json | 8 +- .../ObtainIOFromProjectTia/x4.py | 81 ++++- data/log.txt | 317 ++++++++++++------ lib/config_manager.py | 208 ++++++++---- lib/script_executor.py | 75 +++++ static/js/scripts.js | 124 ++++++- 8 files changed, 648 insertions(+), 189 deletions(-) diff --git a/app.py b/app.py index e0862ec..70275b4 100644 --- a/app.py +++ b/app.py @@ -140,6 +140,24 @@ def execute_script(): 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("/") def index(): script_groups = config_manager.get_script_groups() diff --git a/backend/script_groups/EmailCrono/script_config.json b/backend/script_groups/EmailCrono/script_config.json index 10e3fb1..2c698bd 100644 --- a/backend/script_groups/EmailCrono/script_config.json +++ b/backend/script_groups/EmailCrono/script_config.json @@ -8,8 +8,8 @@ "cronologia_file": "cronologia.md" }, "level3": { - "cronologia_file": "Planning - emails", - "input_directory": "C:\\Trabajo\\SIDEL\\PROJECTs Planning\\Emails" + "cronologia_file": "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" } \ No newline at end of file diff --git a/backend/script_groups/EmailCrono/work_dir.json b/backend/script_groups/EmailCrono/work_dir.json index b515089..a686c85 100644 --- a/backend/script_groups/EmailCrono/work_dir.json +++ b/backend/script_groups/EmailCrono/work_dir.json @@ -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": [ + "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\\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", - "D:\\Trabajo\\VM\\45 - HENKEL - VM Auto Changeover\\Entregado por VM\\01 - 26-07-2025 Max - Emails" + "C:\\Trabajo\\SIDEL\\17 - E5.006880 - Modifica O&U - RSC098" ] } \ No newline at end of file diff --git a/backend/script_groups/ObtainIOFromProjectTia/x4.py b/backend/script_groups/ObtainIOFromProjectTia/x4.py index 4a302e7..cd3a0be 100644 --- a/backend/script_groups/ObtainIOFromProjectTia/x4.py +++ b/backend/script_groups/ObtainIOFromProjectTia/x4.py @@ -8,6 +8,7 @@ from tkinter import filedialog import os import sys import traceback +import time from pathlib import Path script_root = os.path.dirname( @@ -30,6 +31,7 @@ SUPPORTED_TIA_VERSIONS = { CROSS_REF_FILTER = 1 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): """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) return file_path -# Normalizar nombres de bloque/tabla/udt para comparaciones consistentes def _normalize_name(name: str) -> str: """Normaliza un nombre quitando espacios laterales y convirtiendo a minúsculas.""" 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): """Exports cross-references for various elements from a given PLC. Parámetros @@ -151,6 +184,7 @@ def export_plc_cross_references(plc, export_base_dir, exported_blocks=None, prob # --- Export Program Block Cross-References --- blocks_cr_exported = 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...") blocks_cr_path = plc_export_dir / "ProgramBlocks_CR" 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: program_blocks = plc.get_program_blocks() 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: block_name = block.get_name() + current_block_name = block_name # Update current block being processed norm_block = _normalize_name(block_name) if norm_block in problematic_blocks: 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 if norm_block in exported_blocks: # Ya exportado en un intento anterior, no repetir + print(f" Omitiendo bloque ya exportado: {block_name}") continue print(f" Procesando bloque: {block_name}...") try: print(f" Exportando referencias cruzadas para {block_name}...") - block.export_cross_references( - target_directorypath=str(blocks_cr_path), - filter=CROSS_REF_FILTER, - ) + start_time = time.time() + + # 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 exported_blocks.add(norm_block) 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}" ) traceback.print_exc() + problematic_blocks.add(norm_block) # Always mark as problematic blocks_cr_skipped += 1 if _is_disposed_exception(block_ex): # 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) print( 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: print(f" ERROR al acceder a los bloques de programa para exportar referencias cruzadas: {e}") traceback.print_exc() - problematic_blocks.add(_normalize_name(e.__str__())) - raise PortalDisposedException(e) + # If we know which block was being processed, mark it as problematic + 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 --- tags_cr_exported = 0 @@ -424,6 +477,11 @@ if __name__ == "__main__": working_directory = configs.get("working_directory") 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 if not working_directory or not os.path.isdir(working_directory): @@ -484,8 +542,12 @@ if __name__ == "__main__": reopen_attempts += 1 failed_block = pd_ex.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) + print(f"Marcando bloque problemático: {failed_block}") + else: + print("Error general detectado sin bloque específico identificado") if reopen_attempts > MAX_REOPEN_ATTEMPTS: print( @@ -520,6 +582,7 @@ if __name__ == "__main__": if 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.") diff --git a/data/log.txt b/data/log.txt index 644fa26..bb4a622 100644 --- a/data/log.txt +++ b/data/log.txt @@ -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... -[15:34:16] ✅ Configuración cargada exitosamente -[15:34:16] Working/Output directory: C:\Users\migue\OneDrive\Miguel\Obsidean\Trabajo\VM\04-SIDEL\0 - PROJECTS Description\PLANNING -[15:34:16] Input directory: C:\Trabajo\SIDEL\PROJECTs Planning\Emails -[15:34:16] Output file: C:\Users\migue\OneDrive\Miguel\Obsidean\Trabajo\VM\04-SIDEL\0 - PROJECTS Description\PLANNING\emails.md -[15:34:16] Attachments directory: C:\Users\migue\OneDrive\Miguel\Obsidean\Trabajo\VM\04-SIDEL\0 - PROJECTS Description\PLANNING\adjuntos -[15:34:16] Beautify rules file: D:\Proyectos\Scripts\ParamManagerScripts\backend\script_groups\EmailCrono\config\beautify_rules.json -[15:34:16] Found 1 .eml files -[15:34:16] Creando cronología nueva (archivo se sobrescribirá) -[15:34:16] ============================================================ -[15:34:16] Processing file: C:\Trabajo\SIDEL\PROJECTs Planning\Emails\Planning attività 08-08-2025 Teknors.eml -[15:34:16] 📧 Abriendo archivo: C:\Trabajo\SIDEL\PROJECTs Planning\Emails\Planning attività 08-08-2025 Teknors.eml -[15:34:16] ✉️ Mensaje extraído: -[15:34:16] - Subject: Planning attività 08-08-2025 Teknors -[15:34:16] - Remitente: "Passera, Alessandro" -[15:34:16] - Fecha: 2025-08-08 13:06:58 -[15:34:16] - Adjuntos: 0 archivos -[15:34:16] - Contenido: 20738 caracteres -[15:34:16] - Hash generado: ba7f9f899c63a04a454f2e2a9d50856c -[15:34:16] 📧 Procesamiento completado: 1 mensajes extraídos -[15:34:16] Extracted 1 messages from Planning attività 08-08-2025 Teknors.eml -[15:34:16] --- Msg 1/1 from Planning attività 08-08-2025 Teknors.eml --- -[15:34:16] Remitente: Passera, Alessandro -[15:34:16] Fecha: 2025-08-08 13:06:58 -[15:34:16] Subject: Planning attività 08-08-2025 Teknors -[15:34:16] Hash: ba7f9f899c63a04a454f2e2a9d50856c -[15:34:16] Adjuntos: [] -[15:34:16] ✓ NUEVO mensaje - Agregando a la cronología -[15:34:16] Estadísticas de procesamiento: -[15:34:16] - Total mensajes encontrados: 1 -[15:34:16] - Mensajes únicos añadidos: 1 -[15:34:16] - Mensajes duplicados ignorados: 0 -[15:34:16] Writing 1 messages to C:\Users\migue\OneDrive\Miguel\Obsidean\Trabajo\VM\04-SIDEL\0 - PROJECTS Description\PLANNING\emails.md -[15:34:16] ✅ Cronología guardada exitosamente en: C:\Users\migue\OneDrive\Miguel\Obsidean\Trabajo\VM\04-SIDEL\0 - PROJECTS Description\PLANNING\emails.md -[15:34:16] 📊 Total de mensajes en la cronología: 1 -[15:34:16] Ejecución de x1.py finalizada (success). Duración: 0:00:00.249985. -[15:34:16] Log completo guardado en: D:\Proyectos\Scripts\ParamManagerScripts\backend\script_groups\EmailCrono\.log\log_x1.txt -[15:35:04] Iniciando ejecución de x1.py en C:\Users\migue\OneDrive\Miguel\Obsidean\Trabajo\VM\04-SIDEL\0 - PROJECTS Description\PLANNING... -[15:35:04] ✅ Configuración cargada exitosamente -[15:35:04] Working/Output directory: C:\Users\migue\OneDrive\Miguel\Obsidean\Trabajo\VM\04-SIDEL\0 - PROJECTS Description\PLANNING -[15:35:04] Input directory: C:\Trabajo\SIDEL\PROJECTs Planning\Emails -[15:35:04] Output file: C:\Users\migue\OneDrive\Miguel\Obsidean\Trabajo\VM\04-SIDEL\0 - PROJECTS Description\PLANNING\Planning - emails.md -[15:35:04] Attachments directory: C:\Users\migue\OneDrive\Miguel\Obsidean\Trabajo\VM\04-SIDEL\0 - PROJECTS Description\PLANNING\adjuntos -[15:35:04] Beautify rules file: D:\Proyectos\Scripts\ParamManagerScripts\backend\script_groups\EmailCrono\config\beautify_rules.json -[15:35:04] Found 1 .eml files -[15:35:04] Creando cronología nueva (archivo se sobrescribirá) -[15:35:04] ============================================================ -[15:35:04] Processing file: C:\Trabajo\SIDEL\PROJECTs Planning\Emails\Planning attività 08-08-2025 Teknors.eml -[15:35:04] 📧 Abriendo archivo: C:\Trabajo\SIDEL\PROJECTs Planning\Emails\Planning attività 08-08-2025 Teknors.eml -[15:35:04] ✉️ Mensaje extraído: -[15:35:04] - Subject: Planning attività 08-08-2025 Teknors -[15:35:04] - Remitente: "Passera, Alessandro" -[15:35:04] - Fecha: 2025-08-08 13:06:58 -[15:35:04] - Adjuntos: 0 archivos -[15:35:04] - Contenido: 20738 caracteres -[15:35:04] - Hash generado: ba7f9f899c63a04a454f2e2a9d50856c -[15:35:04] 📧 Procesamiento completado: 1 mensajes extraídos -[15:35:04] Extracted 1 messages from Planning attività 08-08-2025 Teknors.eml -[15:35:04] --- Msg 1/1 from Planning attività 08-08-2025 Teknors.eml --- -[15:35:04] Remitente: Passera, Alessandro -[15:35:04] Fecha: 2025-08-08 13:06:58 -[15:35:04] Subject: Planning attività 08-08-2025 Teknors -[15:35:04] Hash: ba7f9f899c63a04a454f2e2a9d50856c -[15:35:04] Adjuntos: [] -[15:35:04] ✓ NUEVO mensaje - Agregando a la cronología -[15:35:04] Estadísticas de procesamiento: -[15:35:04] - Total mensajes encontrados: 1 -[15:35:04] - Mensajes únicos añadidos: 1 -[15:35:04] - Mensajes duplicados ignorados: 0 -[15:35:04] Writing 1 messages to C:\Users\migue\OneDrive\Miguel\Obsidean\Trabajo\VM\04-SIDEL\0 - PROJECTS Description\PLANNING\Planning - emails.md -[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 -[15:35:04] 📊 Total de mensajes en la cronología: 1 -[15:35:04] Ejecución de x1.py finalizada (success). Duración: 0:00:00.245126. -[15:35:04] Log completo guardado en: D:\Proyectos\Scripts\ParamManagerScripts\backend\script_groups\EmailCrono\.log\log_x1.txt -[15:35:10] Iniciando ejecución de x1.py en C:\Users\migue\OneDrive\Miguel\Obsidean\Trabajo\VM\04-SIDEL\0 - PROJECTS Description\PLANNING... -[15:35:11] ✅ Configuración cargada exitosamente -[15:35:11] Working/Output directory: C:\Users\migue\OneDrive\Miguel\Obsidean\Trabajo\VM\04-SIDEL\0 - PROJECTS Description\PLANNING -[15:35:11] Input directory: C:\Trabajo\SIDEL\PROJECTs Planning\Emails -[15:35:11] Output file: C:\Users\migue\OneDrive\Miguel\Obsidean\Trabajo\VM\04-SIDEL\0 - PROJECTS Description\PLANNING\Planning - emails.md -[15:35:11] Attachments directory: C:\Users\migue\OneDrive\Miguel\Obsidean\Trabajo\VM\04-SIDEL\0 - PROJECTS Description\PLANNING\adjuntos -[15:35:11] Beautify rules file: D:\Proyectos\Scripts\ParamManagerScripts\backend\script_groups\EmailCrono\config\beautify_rules.json -[15:35:11] Found 1 .eml files -[15:35:11] Creando cronología nueva (archivo se sobrescribirá) -[15:35:11] ============================================================ -[15:35:11] Processing file: C:\Trabajo\SIDEL\PROJECTs Planning\Emails\Planning attività 08-08-2025 Teknors.eml -[15:35:11] 📧 Abriendo archivo: C:\Trabajo\SIDEL\PROJECTs Planning\Emails\Planning attività 08-08-2025 Teknors.eml -[15:35:11] ✉️ Mensaje extraído: -[15:35:11] - Subject: Planning attività 08-08-2025 Teknors -[15:35:11] - Remitente: "Passera, Alessandro" -[15:35:11] - Fecha: 2025-08-08 13:06:58 -[15:35:11] - Adjuntos: 0 archivos -[15:35:11] - Contenido: 20738 caracteres -[15:35:11] - Hash generado: ba7f9f899c63a04a454f2e2a9d50856c -[15:35:11] 📧 Procesamiento completado: 1 mensajes extraídos -[15:35:11] Extracted 1 messages from Planning attività 08-08-2025 Teknors.eml -[15:35:11] --- Msg 1/1 from Planning attività 08-08-2025 Teknors.eml --- -[15:35:11] Remitente: Passera, Alessandro -[15:35:11] Fecha: 2025-08-08 13:06:58 -[15:35:11] Subject: Planning attività 08-08-2025 Teknors -[15:35:11] Hash: ba7f9f899c63a04a454f2e2a9d50856c -[15:35:11] Adjuntos: [] -[15:35:11] ✓ NUEVO mensaje - Agregando a la cronología -[15:35:11] Estadísticas de procesamiento: -[15:35:11] - Total mensajes encontrados: 1 -[15:35:11] - Mensajes únicos añadidos: 1 -[15:35:11] - Mensajes duplicados ignorados: 0 -[15:35:11] Writing 1 messages to C:\Users\migue\OneDrive\Miguel\Obsidean\Trabajo\VM\04-SIDEL\0 - PROJECTS Description\PLANNING\Planning - emails.md -[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 -[15:35:11] 📊 Total de mensajes en la cronología: 1 -[15:35:11] Ejecución de x1.py finalizada (success). Duración: 0:00:00.264072. -[15:35:11] Log completo guardado en: D:\Proyectos\Scripts\ParamManagerScripts\backend\script_groups\EmailCrono\.log\log_x1.txt +[10:48:07] Iniciando ejecución de x4.py en D:\Trabajo\VM\45 - HENKEL - VM Auto Changeover\ExportTia... +[10:48:07] --- Exportador de Referencias Cruzadas de TIA Portal --- +[10:48:07] Configuración: +[10:48:07] - Tiempo esperado por bloque: 120 segundos (para logging) +[10:48:07] - Máximo intentos de reapertura: 5 +[10:48:07] - Filtro de referencias cruzadas: 1 +[10:48:16] --- ERRORES --- +[10:48:16] 2025-08-23 10:44:50,965 [13] ERROR Siemens.TiaPortal.OpennessApi18.Implementations.ProgramBlock ExportCrossReferences - +[10:48:16] Siemens.TiaPortal.OpennessContracts.OpennessAccessException: Cross-thread operation is not valid in Openness within STA. +[10:48:16] Traceback (most recent call last): +[10:48:16] File "D:\Proyectos\Scripts\ParamManagerScripts\backend\script_groups\ObtainIOFromProjectTia\x4.py", line 241, in export_plc_cross_references +[10:48:16] _export_block_with_timeout(block, blocks_cr_path, block_name) +[10:48:16] File "D:\Proyectos\Scripts\ParamManagerScripts\backend\script_groups\ObtainIOFromProjectTia\x4.py", line 173, in _export_block_with_timeout +[10:48:16] raise result["error"] +[10:48:16] File "D:\Proyectos\Scripts\ParamManagerScripts\backend\script_groups\ObtainIOFromProjectTia\x4.py", line 151, in export_worker +[10:48:16] block.export_cross_references( +[10:48:16] ValueError: OpennessAccessException: Cross-thread operation is not valid in Openness within STA. +[10:48:16] 2025-08-23 10:44:50,986 [14] ERROR Siemens.TiaPortal.OpennessApi18.Implementations.ProgramBlock ExportCrossReferences - +[10:48:16] Siemens.TiaPortal.OpennessContracts.OpennessAccessException: Cross-thread operation is not valid in Openness within STA. +[10:48:16] Traceback (most recent call last): +[10:48:16] File "D:\Proyectos\Scripts\ParamManagerScripts\backend\script_groups\ObtainIOFromProjectTia\x4.py", line 241, in export_plc_cross_references +[10:48:16] _export_block_with_timeout(block, blocks_cr_path, block_name) +[10:48:16] File "D:\Proyectos\Scripts\ParamManagerScripts\backend\script_groups\ObtainIOFromProjectTia\x4.py", line 173, in _export_block_with_timeout +[10:48:16] raise result["error"] +[10:48:16] File "D:\Proyectos\Scripts\ParamManagerScripts\backend\script_groups\ObtainIOFromProjectTia\x4.py", line 151, in export_worker +[10:48:16] block.export_cross_references( +[10:48:16] ValueError: OpennessAccessException: Cross-thread operation is not valid in Openness within STA. +[10:48:16] 2025-08-23 10:44:50,988 [15] ERROR Siemens.TiaPortal.OpennessApi18.Implementations.ProgramBlock ExportCrossReferences - +[10:48:16] Siemens.TiaPortal.OpennessContracts.OpennessAccessException: Cross-thread operation is not valid in Openness within STA. +[10:48:16] Traceback (most recent call last): +[10:48:16] File "D:\Proyectos\Scripts\ParamManagerScripts\backend\script_groups\ObtainIOFromProjectTia\x4.py", line 241, in export_plc_cross_references +[10:48:16] _export_block_with_timeout(block, blocks_cr_path, block_name) +[10:48:16] File "D:\Proyectos\Scripts\ParamManagerScripts\backend\script_groups\ObtainIOFromProjectTia\x4.py", line 173, in _export_block_with_timeout +[10:48:16] raise result["error"] +[10:48:16] File "D:\Proyectos\Scripts\ParamManagerScripts\backend\script_groups\ObtainIOFromProjectTia\x4.py", line 151, in export_worker +[10:48:16] block.export_cross_references( +[10:48:16] ValueError: OpennessAccessException: Cross-thread operation is not valid in Openness within STA. +[10:48:16] 2025-08-23 10:44:50,990 [16] ERROR Siemens.TiaPortal.OpennessApi18.Implementations.ProgramBlock ExportCrossReferences - +[10:48:16] Siemens.TiaPortal.OpennessContracts.OpennessAccessException: Cross-thread operation is not valid in Openness within STA. +[10:48:16] Traceback (most recent call last): +[10:48:16] File "D:\Proyectos\Scripts\ParamManagerScripts\backend\script_groups\ObtainIOFromProjectTia\x4.py", line 241, in export_plc_cross_references +[10:48:16] _export_block_with_timeout(block, blocks_cr_path, block_name) +[10:48:16] File "D:\Proyectos\Scripts\ParamManagerScripts\backend\script_groups\ObtainIOFromProjectTia\x4.py", line 173, in _export_block_with_timeout +[10:48:16] raise result["error"] +[10:48:16] File "D:\Proyectos\Scripts\ParamManagerScripts\backend\script_groups\ObtainIOFromProjectTia\x4.py", line 151, in export_worker +[10:48:16] block.export_cross_references( +[10:48:16] ValueError: OpennessAccessException: Cross-thread operation is not valid in Openness within STA. +[10:48:16] 2025-08-23 10:44:50,992 [17] ERROR Siemens.TiaPortal.OpennessApi18.Implementations.ProgramBlock ExportCrossReferences - +[10:48:16] Siemens.TiaPortal.OpennessContracts.OpennessAccessException: Cross-thread operation is not valid in Openness within STA. +[10:48:16] Traceback (most recent call last): +[10:48:16] --- FIN ERRORES --- +[10:48:16] Ejecución de x4.py finalizada (error). Duración: 0:04:08.869202. Se detectaron errores (ver log). +[10:48:16] Log completo guardado en: D:\Proyectos\Scripts\ParamManagerScripts\backend\script_groups\ObtainIOFromProjectTia\.log\log_x4.txt +[10:48:19] Versión de TIA Portal detectada: 18.0 (de la extensión .ap18) +[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 +[10:48:19] Usando directorio base de exportación: D:\Trabajo\VM\45 - HENKEL - VM Auto Changeover\ExportTia +[10:48:19] Conectando a TIA Portal V18.0... +[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. +[10:48:19] 2025-08-23 10:48:19,432 [1] INFO Siemens.TiaPortal.OpennessApi18.Implementations.Global OpenPortal - With user interface +[10:48:33] Conectado a TIA Portal. +[10:48:33] 2025-08-23 10:48:33,909 [1] INFO Siemens.TiaPortal.OpennessApi18.Implementations.Portal GetProcessId - Process id: 35128 +[10:48:33] ID del proceso del Portal: 35128 +[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 +[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 +[10:48:52] Se encontraron 1 PLC(s). Iniciando proceso de exportación de referencias cruzadas... +[10:48:52] --- Procesando PLC: PLC_TL25_Q1 --- +[10:48:52] [PLC: PLC_TL25_Q1] Exportando referencias cruzadas de bloques de programa... +[10:48:52] Destino: D:\Trabajo\VM\45 - HENKEL - VM Auto Changeover\ExportTia\PLC_TL25_Q1\ProgramBlocks_CR +[10:48:52] Se encontraron 233 bloques de programa. +[10:48:52] Procesando bloque: ProDiagOB... +[10:48:52] Exportando referencias cruzadas para ProDiagOB... +[10:48:53] Exportación completada en 0.51 segundos +[10:48:53] Procesando bloque: Rt_Enable_RemoteFormatChange... +[10:48:53] Exportando referencias cruzadas para Rt_Enable_RemoteFormatChange... +[10:48:53] Exportación completada en 0.36 segundos +[10:48:53] Procesando bloque: Rt_PopUp_RemoteFormatChange... +[10:48:53] Exportando referencias cruzadas para Rt_PopUp_RemoteFormatChange... +[10:48:53] Exportación completada en 0.05 segundos +[10:48:53] Procesando bloque: Rt_LoadRemoteRecipe... +[10:48:53] Exportando referencias cruzadas para Rt_LoadRemoteRecipe... +[10:48:53] Exportación completada en 0.05 segundos +[10:48:53] Procesando bloque: Rt_RestartRemoteFormatChange... +[10:48:53] Exportando referencias cruzadas para Rt_RestartRemoteFormatChange... +[10:48:53] Exportación completada en 0.04 segundos +[10:48:53] Procesando bloque: CounterManagementQE1_D... +[10:48:53] Exportando referencias cruzadas para CounterManagementQE1_D... +[10:48:54] Exportación completada en 0.45 segundos +[10:48:54] Procesando bloque: CounterManagementQE1_G... +[10:48:54] Exportando referencias cruzadas para CounterManagementQE1_G... +[10:48:54] Exportación completada en 0.14 segundos +[10:48:54] Procesando bloque: FormatManagementQE1_G... +[10:48:54] Exportando referencias cruzadas para FormatManagementQE1_G... +[10:48:56] Exportación completada en 1.72 segundos +[10:48:56] Procesando bloque: FormatManagementQE1_D... +[10:48:56] Exportando referencias cruzadas para FormatManagementQE1_D... +[10:48:57] Exportación completada en 1.36 segundos +[10:48:57] Procesando bloque: Default_SupervisionFB... +[10:48:57] Exportando referencias cruzadas para Default_SupervisionFB... +[10:48:57] Exportación completada en 0.05 segundos +[10:48:57] Procesando bloque: 1000_FC Program Manager... +[10:48:57] Exportando referencias cruzadas para 1000_FC Program Manager... +[10:48:57] Exportación completada en 0.15 segundos +[10:48:57] Procesando bloque: 1001_FC Gateway Data Read... +[10:48:57] Exportando referencias cruzadas para 1001_FC Gateway Data Read... +[10:48:57] Exportación completada en 0.14 segundos +[10:48:57] Procesando bloque: 1002_FC Data Read conversion... +[10:48:57] Exportando referencias cruzadas para 1002_FC Data Read conversion... +[10:48:58] Exportación completada en 0.34 segundos +[10:48:58] Procesando bloque: 1003_FC Remote Control Read... +[10:48:58] Exportando referencias cruzadas para 1003_FC Remote Control Read... +[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... diff --git a/lib/config_manager.py b/lib/config_manager.py index 52f2da1..3d05c54 100644 --- a/lib/config_manager.py +++ b/lib/config_manager.py @@ -1,7 +1,7 @@ import os import json from typing import Dict, Any, List, Optional -import re # Necesario para extraer docstring +import re # Necesario para extraer docstring # Import the new modules from .logger import Logger @@ -13,7 +13,7 @@ from .script_executor import ScriptExecutor # Keep time for execution throttling state 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 --- @@ -35,18 +35,31 @@ class ConfigurationManager: self.min_execution_interval = 1 # Instantiate handlers/managers - self.logger = Logger(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.logger = Logger( + 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.schema_handler = SchemaHandler(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.schema_handler = SchemaHandler( + 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_groups_path, self.dir_manager, self.config_handler, - self.logger, # Pass the central logger instance + self.logger, # Pass the central logger instance self._get_execution_state_internal, - self._set_last_execution_time_internal + self._set_last_execution_time_internal, ) # --- Internal Callbacks/Getters for Sub-Managers --- @@ -59,9 +72,11 @@ class ConfigurationManager: data_json_path = os.path.join(path, "data.json") if not os.path.exists(data_json_path): 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) - 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: print(f"Warning: Could not create data.json in {path}: {e}") else: @@ -73,7 +88,10 @@ class ConfigurationManager: def _get_execution_state_internal(self) -> Dict[str, Any]: """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): """Callback for ScriptExecutor to update the last execution time.""" @@ -127,9 +145,13 @@ class ConfigurationManager: details.setdefault("author", "Unknown") 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.""" - 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: os.makedirs(os.path.dirname(description_path), exist_ok=True) with open(description_path, "w", encoding="utf-8") as f: @@ -174,14 +196,14 @@ class ConfigurationManager: group_path = self._get_group_path(group_id) if not group_path: 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]: """Carga las descripciones de scripts desde scripts_description.json.""" path = self._get_script_descriptions_path(group_id) if path and os.path.exists(path): try: - with open(path, 'r', encoding='utf-8') as f: + with open(path, "r", encoding="utf-8") as f: return json.load(f) except json.JSONDecodeError: print(f"Error: JSON inválido en {path}") @@ -191,13 +213,17 @@ class ConfigurationManager: 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.""" path = self._get_script_descriptions_path(group_id) if path: try: - os.makedirs(os.path.dirname(path), exist_ok=True) # Asegura que el directorio del grupo existe - with open(path, 'w', encoding='utf-8') as f: + os.makedirs( + 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) return True except Exception as e: @@ -208,15 +234,26 @@ class ConfigurationManager: def _extract_short_description(self, script_path: str) -> str: """Extrae la primera línea del docstring de un script Python.""" try: - with open(script_path, 'r', encoding='utf-8') as f: + with open(script_path, "r", encoding="utf-8") as f: content = f.read() # 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: # Obtener el contenido del docstring (grupo 2 o 3) docstring = match.group(2) or match.group(3) # 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." except Exception as e: print(f"Error extrayendo descripción de {script_path}: {e}") @@ -234,36 +271,52 @@ class ConfigurationManager: try: # 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: script_path = os.path.join(group_path, filename) 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) descriptions[filename] = { - "display_name": filename.replace('.py', ''), # Nombre por defecto + "display_name": filename.replace( + ".py", "" + ), # Nombre por defecto "short_description": short_desc, "long_description": "", - "hidden": False + "hidden": False, } updated = True # Añadir a la lista si no está oculto details = descriptions[filename] - if not details.get('hidden', False): - scripts_details.append({ - "filename": filename, # Nombre real del archivo - "display_name": details.get("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 not details.get("hidden", False): + scripts_details.append( + { + "filename": filename, # Nombre real del archivo + "display_name": details.get( + "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: self._save_script_descriptions(group, descriptions) # 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 except FileNotFoundError: @@ -276,49 +329,88 @@ class ConfigurationManager: """Obtiene los detalles completos de un script específico.""" descriptions = self._load_script_descriptions(group_id) # Devolver detalles o un diccionario por defecto si no existe (aunque list_scripts debería crearlo) - return descriptions.get(script_filename, { - "display_name": script_filename.replace('.py', ''), - "short_description": "No encontrado.", - "long_description": "", - "hidden": False - }) + return descriptions.get( + script_filename, + { + "display_name": script_filename.replace(".py", ""), + "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.""" descriptions = self._load_script_descriptions(group_id) if script_filename in descriptions: # 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]["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)) + descriptions[script_filename]["display_name"] = details.get( + "display_name", + descriptions[script_filename].get( + "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): return {"status": "success"} 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: # Intentar crear la entrada si el script existe pero no está en el JSON (caso raro) 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): - print(f"Advertencia: El script '{script_filename}' existe pero no estaba en descriptions.json. Creando entrada.") - short_desc = self._extract_short_description(script_path) - descriptions[script_filename] = { - "display_name": details.get("display_name", script_filename.replace('.py', '')), - "short_description": short_desc, # Usar la extraída + print( + f"Advertencia: El script '{script_filename}' existe pero no estaba en descriptions.json. Creando entrada." + ) + short_desc = self._extract_short_description(script_path) + 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", ""), - "hidden": details.get("hidden", False) - } - if self._save_script_descriptions(group_id, descriptions): - return {"status": "success"} - else: - return {"status": "error", "message": "Fallo al guardar las descripciones de los scripts después de crear la entrada."} + "hidden": details.get("hidden", False), + } + if self._save_script_descriptions(group_id, descriptions): + return {"status": "success"} + else: + return { + "status": "error", + "message": "Fallo al guardar las descripciones de los scripts después de crear la entrada.", + } 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( self, group: str, script_name: str, broadcast_fn=None ) -> Dict[str, Any]: # ScriptExecutor uses callbacks to get/set execution state 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) diff --git a/lib/script_executor.py b/lib/script_executor.py index 2419ce0..ddc7e53 100644 --- a/lib/script_executor.py +++ b/lib/script_executor.py @@ -33,6 +33,10 @@ class ScriptExecutor: self._get_exec_state = get_exec_state_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( self, group: str, @@ -118,6 +122,7 @@ class ScriptExecutor: stderr_capture = "" process = None start_time = datetime.now() + process_key = f"{group}:{script_name}" try: if broadcast_fn: @@ -141,6 +146,9 @@ class ScriptExecutor: creationflags=creation_flags, ) + # Registrar el proceso en ejecución + self.running_processes[process_key] = process + while True: line = process.stdout.readline() if not line and process.poll() is not None: @@ -232,7 +240,74 @@ class ScriptExecutor: return {"status": "error", "error": error_msg, "traceback": traceback_info} 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: process.stderr.close() if process and process.stdout: 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] diff --git a/static/js/scripts.js b/static/js/scripts.js index 4bafb6b..c696442 100644 --- a/static/js/scripts.js +++ b/static/js/scripts.js @@ -1,5 +1,8 @@ let currentGroup; +// Registro de procesos en ejecución para scripts de configuración +let runningConfigScripts = new Set(); + // Initialize WebSocket connection 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) {
- -
${script.filename}
+
+ + +
+
${script.filename}