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:
parent
fdc48375ad
commit
5ed4d9391e
18
app.py
18
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()
|
||||
|
|
|
@ -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"
|
||||
}
|
|
@ -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"
|
||||
]
|
||||
}
|
|
@ -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,7 +253,11 @@ 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__()))
|
||||
# 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 ---
|
||||
|
@ -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.")
|
||||
|
||||
|
|
317
data/log.txt
317
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" <Alessandro.Passera@sidel.com>
|
||||
[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" <Alessandro.Passera@sidel.com>
|
||||
[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" <Alessandro.Passera@sidel.com>
|
||||
[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...
|
||||
|
|
|
@ -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._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({
|
||||
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
|
||||
})
|
||||
"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', ''),
|
||||
return descriptions.get(
|
||||
script_filename,
|
||||
{
|
||||
"display_name": script_filename.replace(".py", ""),
|
||||
"short_description": "No encontrado.",
|
||||
"long_description": "",
|
||||
"hidden": False
|
||||
})
|
||||
"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.")
|
||||
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', '')),
|
||||
"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)
|
||||
"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."}
|
||||
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)
|
||||
|
|
|
@ -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]
|
||||
|
|
|
@ -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) {
|
|||
</div>
|
||||
<div class="flex items-center gap-2 flex-shrink-0">
|
||||
<div class="flex flex-col items-center">
|
||||
<div class="flex gap-1">
|
||||
<button data-filename="${script.filename}"
|
||||
class="bg-green-500 hover:bg-green-600 text-white px-3 py-1 rounded text-sm w-24 text-center execute-button">
|
||||
Ejecutar
|
||||
class="bg-green-500 hover:bg-green-600 text-white px-2 py-1 rounded text-sm execute-button"
|
||||
title="Ejecutar script">
|
||||
▶
|
||||
</button>
|
||||
<div class="text-xs text-gray-500 mt-1 truncate w-24 text-center" title="${script.filename}">${script.filename}</div>
|
||||
<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>
|
||||
<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">
|
||||
|
@ -228,6 +240,11 @@ async function loadScripts(group) {
|
|||
executeScript(script.filename);
|
||||
});
|
||||
|
||||
const stopButton = div.querySelector('.stop-button');
|
||||
stopButton.addEventListener('click', () => {
|
||||
stopScript(script.filename);
|
||||
});
|
||||
|
||||
const editButton = div.querySelector('.edit-button');
|
||||
editButton.addEventListener('click', () => {
|
||||
editScriptDetails(group, script.filename);
|
||||
|
@ -255,6 +272,10 @@ async function executeScript(scriptName) {
|
|||
// REMOVE this line - let the backend log the start via WebSocket
|
||||
// addLogLine(`\nEjecutando script: ${scriptName}...\n`);
|
||||
|
||||
// Marcar script como en ejecución
|
||||
runningConfigScripts.add(scriptName);
|
||||
updateScriptButtons(scriptName, true);
|
||||
|
||||
try {
|
||||
const response = await fetch('/api/execute_script', {
|
||||
method: 'POST',
|
||||
|
@ -268,6 +289,10 @@ async function executeScript(scriptName) {
|
|||
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
|
||||
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
|
||||
}
|
||||
|
||||
|
@ -280,9 +305,95 @@ async function executeScript(scriptName) {
|
|||
// Script output and final status/errors will arrive via WebSocket messages
|
||||
// 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) {
|
||||
console.error('Error in executeScript fetch:', error);
|
||||
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.
|
||||
logArea.innerHTML += cleanMessage + '\n';
|
||||
logArea.scrollTop = logArea.scrollHeight; // Ensure scroll to bottom
|
||||
|
||||
// Detectar finalización de scripts
|
||||
handleScriptCompletion(cleanMessage);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in New Issue