572 lines
22 KiB
Python
572 lines
22 KiB
Python
"""
|
|
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")
|
|
script_dir = os.path.dirname(os.path.abspath(__file__))
|
|
|
|
# <-- 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 ---
|
|
# <-- MODIFICADO: Apuntar al subdirectorio 'PLC' dentro del working_directory -->
|
|
plc_subdir_name = "PLC" # Nombre estándar del subdirectorio de TIA Portal
|
|
xml_project_dir = os.path.join(working_directory, plc_subdir_name)
|
|
|
|
log_message(
|
|
f"Directorio de trabajo base configurado: '{working_directory}'", log_f
|
|
)
|
|
log_message(
|
|
f"Buscando archivos XML recursivamente en el subdirectorio: '{xml_project_dir}'", log_f
|
|
)
|
|
|
|
# Verificar si el directorio PLC existe
|
|
if not os.path.isdir(xml_project_dir):
|
|
log_message(
|
|
f"Error: El subdirectorio '{plc_subdir_name}' no existe dentro de '{working_directory}'. "
|
|
f"Se esperaba encontrar la estructura del proyecto TIA Portal en '{xml_project_dir}'.",
|
|
log_f,
|
|
also_print=False,
|
|
)
|
|
print(
|
|
f"Error: El subdirectorio '{plc_subdir_name}' no existe dentro de '{working_directory}'. "
|
|
f"Asegúrese de que la ruta del directorio de trabajo apunte a la carpeta que *contiene* la carpeta '{plc_subdir_name}'.", 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, working_directory)}", log_f) # Mostrar ruta relativa al working_directory original
|
|
for xml_file in xml_files_found
|
|
]
|
|
|
|
# --- Directorios de salida ---
|
|
# Estos directorios ahora se crearán DENTRO de xml_project_dir (es decir, dentro de 'PLC')
|
|
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 i, xml_filepath in enumerate(xml_files_found):
|
|
relative_path = os.path.relpath(xml_filepath, working_directory)
|
|
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 el directorio: {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:
|
|
# El archivo agregado se guarda en el working_directory original, un nivel por encima de xml_project_dir
|
|
output_agg_file = os.path.join(working_directory, AGGREGATED_FILENAME)
|
|
log_message(
|
|
f"Ejecutando {script5} sobre el directorio: {xml_project_dir}, salida agregada 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 -->
|
|
|
|
# Mensaje final ya impreso antes del flush
|
|
sys.exit(exit_code) # Salir con el código apropiado
|