""" LadderToSCL - Conversor de Siemens LAD/FUP XML a SCL Este script convierte archivos XML de Siemens TIA Portal (LAD/FUP) a código SCL equivalente. Utiliza una arquitectura modular para facilitar el mantenimiento y la extensión. """ # ToUpload/x0_main.py import argparse import subprocess import os import sys import locale import glob import time import traceback import json import datetime # <-- NUEVO: Para timestamps script_root = os.path.dirname( os.path.dirname(os.path.dirname(os.path.dirname(__file__))) ) sys.path.append(script_root) from backend.script_utils import load_configuration # --- Funciones (get_console_encoding - sin cambios) --- def get_console_encoding(): try: return locale.getpreferredencoding(False) except Exception: return "cp1252" CONSOLE_ENCODING = get_console_encoding() # <-- NUEVO: Importar format_variable_name (necesario para predecir nombre de salida) --> try: current_dir = os.path.dirname(os.path.abspath(__file__)) if current_dir not in sys.path: sys.path.insert(0, current_dir) from generators.generator_utils import format_variable_name print("INFO: format_variable_name importado desde generators.generator_utils") except ImportError: print( "ADVERTENCIA: No se pudo importar format_variable_name desde generators. Usando copia local." ) import re def format_variable_name(name): if not name: return "_INVALID_NAME_" if name.startswith('"') and name.endswith('"'): return name prefix = "#" if name.startswith("#") else "" if prefix: name = name[1:] if name and name[0].isdigit(): name = "_" + name name = re.sub(r"[^a-zA-Z0-9_]", "_", name) return prefix + name # <-- NUEVO: Función de Logging --> LOG_FILENAME = "log.txt" def log_message(message, log_file_handle, also_print=True): """Escribe un mensaje en el archivo log y opcionalmente en la consola.""" timestamp = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S.%f")[ :-3 ] # Incluye milisegundos log_line = f"{timestamp} - {message}" try: log_file_handle.write(log_line + "\n") log_file_handle.flush() # Asegurar escritura inmediata except Exception as e: # Fallback si falla escritura en log print(f"{timestamp} - LOGGING ERROR: {e}", file=sys.stderr) print(f"{timestamp} - ORIGINAL MSG: {message}", file=sys.stderr) if also_print: print(message) # Imprimir mensaje original en consola # <-- FIN NUEVO --> # <-- MODIFICADO: run_script para aceptar log_file_handle --> def run_script(script_name, xml_arg, log_file_handle, *extra_args): """Runs a given script, logs output, and returns success status.""" script_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), script_name) python_executable = sys.executable command = [python_executable, script_path, os.path.abspath(xml_arg)] command.extend(extra_args) # Loguear el comando que se va a ejecutar log_message( f"--- Running {script_name} with arguments: {[os.path.relpath(arg) if isinstance(arg, str) and os.path.exists(arg) else arg for arg in command[2:]]} ---", log_file_handle, ) try: result = subprocess.run( command, check=True, capture_output=True, text=True, encoding=CONSOLE_ENCODING, errors="replace", ) stdout_clean = result.stdout.strip() if result.stdout else "" stderr_clean = result.stderr.strip() if result.stderr else "" # Loguear stdout si existe if stdout_clean: log_message( f"--- Stdout ({script_name}) ---", log_file_handle, also_print=False ) # Loguear encabezado log_message( stdout_clean, log_file_handle, also_print=True ) # Loguear y mostrar contenido log_message( f"--- End Stdout ({script_name}) ---", log_file_handle, also_print=False ) # Loguear fin # Loguear stderr si existe if stderr_clean: # Usar log_message también para stderr, pero imprimir en consola como error log_message( f"--- Stderr ({script_name}) ---", log_file_handle, also_print=False ) # Loguear encabezado log_message( stderr_clean, log_file_handle, also_print=False ) # Loguear contenido log_message( f"--- End Stderr ({script_name}) ---", log_file_handle, also_print=False ) # Loguear fin # Imprimir stderr en la consola de error estándar print(f"--- Stderr ({script_name}) ---", file=sys.stderr) print(stderr_clean, file=sys.stderr) print("--------------------------", file=sys.stderr) return True # Éxito except FileNotFoundError: error_msg = f"Error: Script '{script_path}' or Python executable '{python_executable}' not found." log_message(error_msg, log_file_handle, also_print=False) # Loguear error print(error_msg, file=sys.stderr) # Mostrar error en consola return False except subprocess.CalledProcessError as e: error_msg = f"Error running {script_name}: Script returned non-zero exit code {e.returncode}." log_message(error_msg, log_file_handle, also_print=False) # Loguear error print(error_msg, file=sys.stderr) # Mostrar error en consola stdout_decoded = e.stdout.strip() if e.stdout else "" stderr_decoded = e.stderr.strip() if e.stderr else "" if stdout_decoded: log_message( f"--- Stdout ({script_name} - Error) ---", log_file_handle, also_print=False, ) log_message(stdout_decoded, log_file_handle, also_print=False) log_message( f"--- End Stdout ({script_name} - Error) ---", log_file_handle, also_print=False, ) print(f"--- Stdout ({script_name}) ---", file=sys.stderr) print(stdout_decoded, file=sys.stderr) if stderr_decoded: log_message( f"--- Stderr ({script_name} - Error) ---", log_file_handle, also_print=False, ) log_message(stderr_decoded, log_file_handle, also_print=False) log_message( f"--- End Stderr ({script_name} - Error) ---", log_file_handle, also_print=False, ) print(f"--- Stderr ({script_name}) ---", file=sys.stderr) print(stderr_decoded, file=sys.stderr) print("--------------------------", file=sys.stderr) return False except Exception as e: error_msg = f"An unexpected error occurred while running {script_name}: {e}" log_message(error_msg, log_file_handle, also_print=False) # Loguear error traceback_str = traceback.format_exc() log_message( traceback_str, log_file_handle, also_print=False ) # Loguear traceback print(error_msg, file=sys.stderr) # Mostrar error en consola traceback.print_exc(file=sys.stderr) # Mostrar traceback en consola return False # --- Función check_skip_status (sin cambios en su lógica interna) --- def check_skip_status( xml_filepath, processed_json_filepath, final_output_dir, log_f ): # Añadido log_f status = {"skip_x1_x2": False, "skip_x3": False} can_check_x3 = False if not os.path.exists(processed_json_filepath): return status stored_mtime = None stored_size = None block_name = None block_type = None processed_json_mtime = None try: processed_json_mtime = os.path.getmtime(processed_json_filepath) with open(processed_json_filepath, "r", encoding="utf-8") as f: data = json.load(f) stored_mtime = data.get("source_xml_mod_time") stored_size = data.get("source_xml_size") block_name = data.get("block_name") block_type = data.get("block_type") except Exception as e: log_message( f"Advertencia: Error leyendo JSON procesado {processed_json_filepath}: {e}. No se saltará.", log_f, also_print=False, ) return status if stored_mtime is None or stored_size is None: can_check_x3 = block_name is not None and block_type is not None else: try: current_xml_mtime = os.path.getmtime(xml_filepath) current_xml_size = os.path.getsize(xml_filepath) time_match = abs(stored_mtime - current_xml_mtime) < 0.001 size_match = stored_size == current_xml_size if time_match and size_match: status["skip_x1_x2"] = True can_check_x3 = True except OSError as e: log_message( f"Advertencia: Error obteniendo metadatos XML para {xml_filepath}: {e}. No se saltará x1/x2.", log_f, also_print=False, ) can_check_x3 = block_name is not None and block_type is not None if status["skip_x1_x2"] and can_check_x3: try: expected_extension = ( ".md" if block_type in ["PlcUDT", "PlcTagTable"] else ".scl" ) final_filename = format_variable_name(block_name) + expected_extension final_output_path = os.path.join(final_output_dir, final_filename) if os.path.exists(final_output_path): final_output_mtime = os.path.getmtime(final_output_path) if final_output_mtime >= processed_json_mtime: status["skip_x3"] = True except Exception as e: log_message( f"Advertencia: Error determinando estado de salto x3 para {block_name or 'desconocido'}: {e}. No se saltará x3.", log_f, also_print=False, ) return status # --- Constantes --- AGGREGATED_FILENAME = "full_project_representation.md" SCL_OUTPUT_DIRNAME = "scl_output" XREF_OUTPUT_DIRNAME = "xref_output" # --- Bloque Principal --- if __name__ == "__main__": configs = load_configuration() working_directory = configs.get("working_directory") # <-- MODIFICADO: Abrir archivo log --> log_filepath = os.path.join( os.path.dirname(os.path.abspath(__file__)), LOG_FILENAME ) with open( log_filepath, "w", encoding="utf-8" ) as log_f: # Usar 'a' para añadir al log log_message("=" * 40 + " LOG START " + "=" * 40, log_f) # --- PARTE 1: BUSCAR ARCHIVOS --- base_search_dir = "XML Project" script_dir = os.path.dirname(os.path.abspath(__file__)) xml_project_dir = os.path.join(script_dir, base_search_dir) log_message( f"Buscando archivos XML recursivamente en: '{xml_project_dir}'", log_f ) if not os.path.isdir(xml_project_dir): log_message( f"Error: El directorio '{xml_project_dir}' no existe.", log_f, also_print=False, ) print( f"Error: El directorio '{xml_project_dir}' no existe.", file=sys.stderr ) sys.exit(1) search_pattern = os.path.join(xml_project_dir, "**", "*.xml") xml_files_found = glob.glob(search_pattern, recursive=True) if not xml_files_found: log_message( f"No se encontraron archivos XML en '{xml_project_dir}' o sus subdirectorios.", log_f, ) sys.exit(0) log_message( f"Se encontraron {len(xml_files_found)} archivos XML para procesar:", log_f ) xml_files_found.sort() [ log_message(f" - {os.path.relpath(xml_file, script_dir)}", log_f) for xml_file in xml_files_found ] # --- Directorios de salida --- scl_output_dir = os.path.join(xml_project_dir, SCL_OUTPUT_DIRNAME) xref_output_dir = os.path.join(xml_project_dir, XREF_OUTPUT_DIRNAME) # --- PARTE 2: PROCESAMIENTO INDIVIDUAL (x1, x2, x3) --- log_message("\n--- Fase 1: Procesamiento Individual (x1, x2, x3) ---", log_f) script1 = "x1_to_json.py" script2 = "x2_process.py" script3 = "x3_generate_scl.py" file_status = {} processed_count = 0 skipped_full_count = 0 failed_count = 0 skipped_partial_count = 0 for xml_filepath in xml_files_found: relative_path = os.path.relpath(xml_filepath, script_dir) log_message(f"\n--- Procesando archivo: {relative_path} ---", log_f) status = {"x1_ok": None, "x2_ok": None, "x3_ok": None} file_status[relative_path] = status base_filename = os.path.splitext(os.path.basename(xml_filepath))[0] parsing_dir = os.path.join(os.path.dirname(xml_filepath), "parsing") processed_json_filepath = os.path.join( parsing_dir, f"{base_filename}_processed.json" ) # 1. Comprobar estado de salto skip_info = check_skip_status( xml_filepath, processed_json_filepath, scl_output_dir, log_f ) # Pasar log_f skip_x1_x2 = skip_info["skip_x1_x2"] skip_x3 = skip_info["skip_x3"] # 2. Ejecutar/Saltar x1 if skip_x1_x2: log_message( f"--- SALTANDO x1 para: {relative_path} (archivo XML no modificado y JSON procesado existe)", log_f, ) status["x1_ok"] = True else: if run_script(script1, xml_filepath, log_f): # Pasar log_f # Mensaje ya logueado por run_script status["x1_ok"] = True else: log_message( f"--- {script1} FALLÓ para: {relative_path} ---", log_f, also_print=False, ) # Ya impreso por run_script status["x1_ok"] = False failed_count += 1 continue # 3. Ejecutar/Saltar x2 if skip_x1_x2: log_message( f"--- SALTANDO x2 para: {relative_path} (razón anterior)", log_f ) status["x2_ok"] = True else: if run_script(script2, xml_filepath, log_f): # Pasar log_f status["x2_ok"] = True else: log_message( f"--- {script2} FALLÓ para: {relative_path} ---", log_f, also_print=False, ) status["x2_ok"] = False failed_count += 1 continue # 4. Ejecutar/Saltar x3 if skip_x3: # Solo puede ser True si skip_x1_x2 era True log_message( f"--- SALTANDO x3 para: {relative_path} (archivo de salida en '{SCL_OUTPUT_DIRNAME}' está actualizado)", log_f, ) status["x3_ok"] = True skipped_full_count += 1 processed_count += 1 else: if skip_x1_x2: skipped_partial_count += 1 # Se saltó x1/x2 pero se ejecuta x3 if run_script( script3, xml_filepath, log_f, xml_project_dir ): # Pasar log_f y project_root_dir status["x3_ok"] = True processed_count += 1 else: log_message( f"--- {script3} FALLÓ para: {relative_path} ---", log_f, also_print=False, ) status["x3_ok"] = False failed_count += 1 continue # --- PARTE 3: EJECUTAR x4 (Referencias Cruzadas) --- log_message( f"\n--- Fase 2: Ejecutando x4_cross_reference.py (salida en '{XREF_OUTPUT_DIRNAME}/') ---", log_f, ) script4 = "x4_cross_reference.py" run_x4 = True success_x4 = False can_run_x4 = any(s["x1_ok"] and s["x2_ok"] for s in file_status.values()) if not can_run_x4: log_message( "Advertencia: Ningún archivo completó x1/x2. Saltando x4.", log_f ) run_x4 = False script4_path = os.path.join(script_dir, script4) if not os.path.exists(script4_path): log_message( f"Advertencia: Script '{script4}' no encontrado. Saltando x4.", log_f ) run_x4 = False if run_x4: log_message( f"Ejecutando {script4} sobre: {xml_project_dir}, salida en: {xref_output_dir}", log_f, ) success_x4 = run_script( script4, xml_project_dir, log_f, "-o", xref_output_dir ) # Pasar log_f if not success_x4: log_message(f"--- {script4} FALLÓ. ---", log_f, also_print=False) # Mensaje de éxito ya logueado por run_script else: log_message("Fase 2 (x4) omitida.", log_f) # --- PARTE 4: EJECUTAR x5 (Agregación) --- log_message(f"\n--- Fase 3: Ejecutando x5_aggregate.py ---", log_f) script5 = "x5_aggregate.py" run_x5 = True success_x5 = False can_run_x5 = any(s["x3_ok"] for s in file_status.values()) if not can_run_x5: log_message("Advertencia: Ningún archivo completó x3. Saltando x5.", log_f) run_x5 = False script5_path = os.path.join(script_dir, script5) if not os.path.exists(script5_path): log_message( f"Advertencia: Script '{script5}' no encontrado. Saltando x5.", log_f ) run_x5 = False if run_x5: output_agg_file = os.path.join(xml_project_dir, AGGREGATED_FILENAME) log_message( f"Ejecutando {script5} sobre: {xml_project_dir}, salida en: {output_agg_file}", log_f, ) success_x5 = run_script( script5, xml_project_dir, log_f, "-o", output_agg_file ) # Pasar log_f if not success_x5: log_message(f"--- {script5} FALLÓ. ---", log_f, also_print=False) # Mensaje de éxito ya logueado por run_script else: log_message("Fase 3 (x5) omitida.", log_f) # --- PARTE 5: RESUMEN FINAL --- log_message( "\n" + "-" * 20 + " Resumen Final del Procesamiento Completo " + "-" * 20, log_f, ) log_message(f"Total de archivos XML encontrados: {len(xml_files_found)}", log_f) log_message( f"Archivos procesados/actualizados exitosamente (x1-x3): {processed_count}", log_f, ) log_message( f"Archivos completamente saltados (x1, x2, x3): {skipped_full_count}", log_f ) log_message( f"Archivos parcialmente saltados (x1, x2 saltados; x3 ejecutado): {skipped_partial_count}", log_f, ) log_message(f"Archivos fallidos (en x1, x2 o x3): {failed_count}", log_f) if failed_count > 0: log_message("Archivos fallidos:", log_f) for f, s in file_status.items(): if not ( s.get("x1_ok", False) and s.get("x2_ok", False) and s.get("x3_ok", False) ): failed_step = ( "x1" if not s.get("x1_ok", False) else ("x2" if not s.get("x2_ok", False) else "x3") ) log_message(f" - {f} (falló en {failed_step})", log_f) log_message( f"Fase 2 (Generación XRef - x4): {'Completada' if run_x4 and success_x4 else ('Fallida' if run_x4 and not success_x4 else 'Omitida')}", log_f, ) log_message( f"Fase 3 (Agregación - x5): {'Completada' if run_x5 and success_x5 else ('Fallida' if run_x5 and not success_x5 else 'Omitida')}", log_f, ) log_message("-" * (80), log_f) has_errors = ( failed_count > 0 or (run_x4 and not success_x4) or (run_x5 and not success_x5) ) # Mensaje final en consola final_console_message = "Proceso finalizado exitosamente." exit_code = 0 if has_errors: final_console_message = "Proceso finalizado con errores." exit_code = 1 log_message(final_console_message, log_f) # Loguear mensaje final print( f"\n{final_console_message} Consulta '{LOG_FILENAME}' para detalles." ) # Mostrar mensaje en consola log_message("="*41 + " LOG END " + "="*42, log_f) # <-- NUEVO: Flush explícito antes de salir --> try: log_f.flush() os.fsync(log_f.fileno()) # Intenta forzar escritura a disco (puede no funcionar en todos los OS) except Exception as flush_err: print(f"Advertencia: Error durante flush/fsync final del log: {flush_err}", file=sys.stderr) # <-- FIN NUEVO --> print(f"\n{final_console_message} Consulta '{LOG_FILENAME}' para detalles.") sys.exit(exit_code) # Salir con el código apropiado