Files
CloudRecoveryAS/runner.py

189 lines
6.4 KiB
Python

"""Punto de entrada principal de la aplicación."""
import argparse
import os
import sys
import traceback
from pathlib import Path
ROOT_DIR = Path(__file__).parent.resolve()
if not getattr(sys, "frozen", False):
sys.path.insert(0, str(ROOT_DIR))
def parse_args(argv: list[str] | None = None) -> argparse.Namespace:
parser = argparse.ArgumentParser(description="CloudRestoreAS")
parser.add_argument(
"--minimized",
action="store_true",
help="Inicia minimizado en la bandeja del sistema",
)
parser.add_argument(
"--start-engine",
action="store_true",
help="Inicia el motor de restauración automáticamente",
)
parser.add_argument(
"--headless",
action="store_true",
help="Fuerza el modo sin interfaz (Qt offscreen) para servidores sin display",
)
return parser.parse_args(argv)
def _ensure_qt_platform(headless: bool = False) -> str:
"""
Selecciona el plugin de plataforma Qt en Linux. En un servidor headless (sin
DISPLAY/WAYLAND_DISPLAY) o con --headless, usa 'offscreen' para que la app
arranque y el motor trabaje sin X; con display usa el default ('xcb'). No toca
nada si el usuario ya fijó QT_QPA_PLATFORM, ni en Windows/macOS.
Devuelve la plataforma forzada ("offscreen") o "" si se deja el default.
"""
if sys.platform in ("win32", "darwin"):
return ""
if os.environ.get("QT_QPA_PLATFORM"):
return os.environ["QT_QPA_PLATFORM"]
has_display = bool(os.environ.get("DISPLAY") or os.environ.get("WAYLAND_DISPLAY"))
if headless or not has_display:
os.environ["QT_QPA_PLATFORM"] = "offscreen"
return "offscreen"
return ""
def _crash_log_targets() -> list[Path]:
"""Ubicaciones candidatas para el crash log, de más a menos accesible."""
targets: list[Path] = []
try:
targets.append(Path(sys.executable).resolve().parent / "CloudRestoreAS-crash.log")
except Exception:
pass
base = os.environ.get("LOCALAPPDATA") or os.environ.get("APPDATA") or os.environ.get("HOME")
if base:
targets.append(Path(base) / "CloudRestoreAS" / "crash.log")
import tempfile
targets.append(Path(tempfile.gettempdir()) / "CloudRestoreAS-crash.log")
return targets
def _write_crash_log(text: str) -> Path | None:
"""Escribe el detalle del crash en la primera ubicación escribible."""
for path in _crash_log_targets():
try:
path.parent.mkdir(parents=True, exist_ok=True)
path.write_text(text, encoding="utf-8")
return path
except Exception:
continue
return None
def _show_fatal(exc: BaseException) -> None:
"""Hace VISIBLE un fallo de arranque: crash log + diálogo (o stderr)."""
tb = "".join(traceback.format_exception(type(exc), exc, exc.__traceback__))
detail = f"CloudRestoreAS no pudo iniciar.\n\n{type(exc).__name__}: {exc}\n\n{tb}"
crash_path = _write_crash_log(detail)
try:
sys.stderr.write(detail)
except Exception:
pass
try:
# Sin display, forzar offscreen para que crear la QApplication no falle
# también aquí (el diálogo no se verá, pero el crash log ya quedó escrito).
_ensure_qt_platform()
from PySide6.QtWidgets import QApplication, QMessageBox
app = QApplication.instance() or QApplication(sys.argv)
box = QMessageBox()
box.setIcon(QMessageBox.Icon.Critical)
box.setWindowTitle("CloudRestoreAS — Error al iniciar")
box.setText("La aplicación no pudo iniciar.")
info = str(exc) or type(exc).__name__
if crash_path:
info += f"\n\nDetalle guardado en:\n{crash_path}"
box.setInformativeText(info)
box.setDetailedText(tb)
box.exec()
except Exception:
# Sin GUI disponible: el crash log y stderr ya quedaron escritos.
pass
def _run() -> int:
"""Arranque real. Los imports están diferidos para que main() capture cualquier fallo."""
# Bootstrap y ODBC antes de importar módulos que usan pyodbc.
from app.config.bootstrap import ensure_runtime_layout
from app.config.odbc_setup import configure_odbc_environment
ensure_runtime_layout()
configure_odbc_environment()
from PySide6.QtWidgets import QApplication
from PySide6.QtCore import Qt
from app.config import get_launch_options, initialize_env, is_panel_configured
from app.ui.main_window import MainWindow
from app.ui.tray_assets import load_tray_icon
from app.utils.logger import app_logger
args = parse_args()
qt_platform = _ensure_qt_platform(headless=args.headless)
launch = get_launch_options()
initialize_env()
panel_ok = is_panel_configured()
start_engine = args.start_engine or launch.start_engine
app_logger.info("=" * 80)
app_logger.info("CloudRestoreAS - Iniciando aplicación")
app_logger.info(f"Directorio app: {ROOT_DIR}")
if qt_platform == "offscreen":
app_logger.info("Modo: sin display → plataforma Qt 'offscreen' (motor headless)")
else:
app_logger.info("Modo: ventana siempre visible; cerrar minimiza a bandeja")
if start_engine:
app_logger.info("Modo: motor auto-inicio")
if not panel_ok:
app_logger.warning(
"Servicio PANEL_BASES_ANEXO24 no configurado en config/.env — "
"complete CLOUDRESTORE_PANEL_* para enrutamiento automático."
)
app_logger.info("=" * 80)
app = QApplication(sys.argv)
app.setApplicationName("CloudRestoreAS")
app.setOrganizationName("Aduanasoft")
app.setAttribute(Qt.ApplicationAttribute.AA_EnableHighDpiScaling)
app.setQuitOnLastWindowClosed(False)
app.setWindowIcon(load_tray_icon())
window = MainWindow(
start_engine=start_engine,
panel_configured=panel_ok,
)
# La ventana SIEMPRE se muestra al iniciar; cerrar (X) la manda a la bandeja.
window.show()
window.activateWindow()
window.raise_()
app_logger.info("Ventana principal mostrada")
exit_code = app.exec()
app_logger.info(f"Aplicación finalizada con código: {exit_code}")
return exit_code
def main():
"""Punto de entrada: ejecuta _run() y hace visible cualquier fallo de arranque."""
try:
sys.exit(_run())
except SystemExit:
raise
except BaseException as exc: # capturamos TODO en el arranque: nunca morir en silencio
_show_fatal(exc)
sys.exit(1)
if __name__ == "__main__":
main()