166 lines
6.2 KiB
Python
166 lines
6.2 KiB
Python
"""Hermes Hub — Self-Healing Launcher Bootstrap and Crash Handler.
|
||
|
||
Responsibilities:
|
||
1. Dependency Verification & Self-Healing:
|
||
Checks for required packages (customtkinter, pillow, psutil, pyyaml, requests).
|
||
If any package is missing, attempts non-blocking silent auto-installation into the active Python environment.
|
||
2. Pre-UI Crash Logging:
|
||
Ensures all startup lifecycle stages and any early unhandled exceptions are written to
|
||
logs/startup.log with full traceback before GUI initialization.
|
||
3. Native User Feedback:
|
||
If a fatal crash occurs before a window can be displayed, shows a native Windows error dialog
|
||
pointing to the exact log file location instead of silent process termination.
|
||
"""
|
||
from __future__ import annotations
|
||
|
||
import ctypes
|
||
import datetime
|
||
import os
|
||
import subprocess
|
||
import sys
|
||
import traceback
|
||
from pathlib import Path
|
||
from typing import Dict, List, Tuple
|
||
|
||
|
||
def get_startup_log_path() -> Path:
|
||
"""Resolve startup.log path safely without external dependencies."""
|
||
try:
|
||
from antigravity_provider import paths
|
||
return paths.get_startup_log_file()
|
||
except Exception:
|
||
local_app = os.environ.get("LOCALAPPDATA", "")
|
||
base = Path(local_app) / "hermes" if local_app else Path.home() / ".hermes"
|
||
return base / "logs" / "startup.log"
|
||
|
||
|
||
def log_startup(msg: str) -> None:
|
||
"""Append a timestamped log entry to startup.log."""
|
||
try:
|
||
p = get_startup_log_path()
|
||
p.parent.mkdir(parents=True, exist_ok=True)
|
||
ts = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
||
with open(p, "a", encoding="utf-8") as f:
|
||
f.write(f"[{ts}] {msg}\n")
|
||
except Exception:
|
||
pass
|
||
|
||
|
||
def show_native_error(title: str, message: str) -> None:
|
||
"""Display a native Windows error modal (or fallback to stderr on POSIX)."""
|
||
if sys.platform == "win32":
|
||
try:
|
||
MB_ICONERROR = 0x10
|
||
MB_OK = 0x0
|
||
ctypes.windll.user32.MessageBoxW(0, message, title, MB_ICONERROR | MB_OK)
|
||
return
|
||
except Exception:
|
||
pass
|
||
print(f"[{title}] {message}", file=sys.stderr)
|
||
|
||
|
||
REQUIRED_PACKAGES: Dict[str, str] = {
|
||
"customtkinter": "customtkinter>=6.0.0",
|
||
"PIL": "pillow>=10.0.0",
|
||
"psutil": "psutil>=5.9.0",
|
||
"yaml": "pyyaml>=6.0.1",
|
||
}
|
||
|
||
|
||
def check_missing_dependencies() -> List[str]:
|
||
"""Check which required UI / system packages are currently unimportable."""
|
||
missing = []
|
||
for mod_name, pkg_spec in REQUIRED_PACKAGES.items():
|
||
try:
|
||
__import__(mod_name)
|
||
except ImportError:
|
||
missing.append(pkg_spec)
|
||
return missing
|
||
|
||
|
||
def self_heal_dependencies(missing_packages: List[str]) -> Tuple[bool, str]:
|
||
"""Attempt pip install for missing packages in the current Python executable environment."""
|
||
if not missing_packages:
|
||
return True, "All dependencies present"
|
||
|
||
log_startup(f"Missing dependencies detected: {missing_packages}. Starting self-healing...")
|
||
try:
|
||
cmd = [
|
||
sys.executable,
|
||
"-m",
|
||
"pip",
|
||
"install",
|
||
"--no-warn-script-location",
|
||
] + missing_packages
|
||
|
||
res = subprocess.run(
|
||
cmd,
|
||
capture_output=True,
|
||
text=True,
|
||
timeout=90,
|
||
)
|
||
if res.returncode == 0:
|
||
log_startup("Self-healing successful. Re-verifying package imports...")
|
||
still_missing = check_missing_dependencies()
|
||
if not still_missing:
|
||
log_startup("All packages verified after self-healing.")
|
||
return True, "Self-healing completed successfully"
|
||
else:
|
||
err = f"Packages still missing after install: {still_missing}"
|
||
log_startup(err)
|
||
return False, err
|
||
else:
|
||
err = f"pip install exited with code {res.returncode}: {res.stderr.strip()}"
|
||
log_startup(err)
|
||
return False, err
|
||
except Exception as exc:
|
||
err = f"Self-healing exception: {type(exc).__name__}: {exc}"
|
||
log_startup(err)
|
||
return False, err
|
||
|
||
|
||
def bootstrap_and_launch() -> None:
|
||
"""Bootstrap entry point: log startup, verify dependencies, and launch Hermes Hub GUI."""
|
||
log_startup("=== Hermes Hub Launcher Bootstrap initiated ===")
|
||
log_startup(f"Python: {sys.executable} (version {sys.version.split()[0]})")
|
||
log_startup(f"Working Directory: {os.getcwd()}")
|
||
|
||
# 1. Dependency verification & self-healing
|
||
missing = check_missing_dependencies()
|
||
if missing:
|
||
log_startup(f"Pre-flight dependency check: missing {missing}")
|
||
ok, detail = self_heal_dependencies(missing)
|
||
if not ok:
|
||
log_path = get_startup_log_path()
|
||
show_native_error(
|
||
"Hermes Hub — Ошибка компонентов",
|
||
"Не удалось автоматически установить необходимые компоненты графического интерфейса (customtkinter / Pillow / psutil).\n\n"
|
||
f"Детали ошибки: {detail}\n\n"
|
||
f"Лог запуска: {log_path}\n\n"
|
||
"Вы можете установить их вручную командой:\n"
|
||
f"{sys.executable} -m pip install customtkinter pillow psutil pyyaml",
|
||
)
|
||
sys.exit(1)
|
||
|
||
# 2. Launch GUI with full exception capture
|
||
try:
|
||
log_startup("Importing hermes_hub_app module...")
|
||
from antigravity_provider.router.hermes_hub_app import launch_hub
|
||
|
||
log_startup("Executing launch_hub()...")
|
||
launch_hub()
|
||
log_startup("Hermes Hub GUI closed normally.")
|
||
except Exception as exc:
|
||
tb = traceback.format_exc()
|
||
log_startup(f"FATAL EXCEPTION during launch:\n{tb}")
|
||
log_path = get_startup_log_path()
|
||
show_native_error(
|
||
"Hermes Hub — Критическая ошибка при запуске",
|
||
f"Произошла ошибка при запуске интерфейса Hermes Hub:\n\n{exc}\n\n"
|
||
f"Полный текст ошибки записан в лог:\n{log_path}",
|
||
)
|
||
sys.exit(1)
|
||
|
||
|
||
if __name__ == "__main__":
|
||
bootstrap_and_launch()
|