"""Launcher-Hauptprogramm: startet die komplette Anwendung (§6.1A, §9.2). Startablauf (§9.2): 1. Launcher bestimmt den eigenen absoluten Programmordner 2. Runtime- und GStreamer-Suchpfade werden gesetzt 3. Konfiguration und Datenbank werden migriert 4. Renderer startet und meldet Capabilities 5. Control Server startet auf konfiguriertem Port 6. Node-Identity wird geladen, Discovery gestartet 7. Healthcheck und lokaler Render-Preflight müssen grün sein 8. Standardbrowser wird optional geöffnet 9. Erst danach wird ein gespeichertes Autostart-Projekt aktiviert Der Launcher läuft als Vordergrundprozess und beendet beide Kindprozesse kontrolliert bei SIGINT/SIGTERM (§26.4). """ from __future__ import annotations import asyncio import signal import sys import time from pathlib import Path from hms_launcher.paths import AppPaths, resolve_app_root from hms_launcher.supervisor import ProcessSpec, Supervisor, find_free_port async def run( app_root: Path | None = None, open_browser: bool = True, web_port: int | None = None, ) -> int: """Startet die komplette HMS MediaEngine-Anwendung. Rückgabe: Exit-Code (0 = kontrolliert beendet). """ # 1. App-Root bestimmen (§9.2 Nr. 1) if app_root is None: app_root = resolve_app_root() paths = AppPaths(root=app_root) # Schreibbarkeit prüfen (§9.1) if not paths.ensure_writable(): print("FEHLER: Anwendungsordner ist schreibgeschützt", file=sys.stderr) return 1 # 2. Ports wählen (§6.1A) web_port = web_port or find_free_port() ipc_port = find_free_port() print(f"HMS MediaEngine – Starte (root={app_root})") print(f" Web-UI: http://127.0.0.1:{web_port}") print(f" IPC: 127.0.0.1:{ipc_port}") # 3. Supervisor einrichten (§6.1A) supervisor = Supervisor(paths) # 4. Renderer starten (§9.2 Nr. 4) renderer_cmd = [ sys.executable, "-m", "hms_renderer", "--ipc-port", str(ipc_port), ] supervisor.start(ProcessSpec( name="renderer", cmd=renderer_cmd, restartable=True, max_restarts=5, stop_timeout_s=5.0, )) print(" Renderer gestartet") # 5. Control Core starten (§9.2 Nr. 5) control_cmd = [ sys.executable, "-m", "uvicorn", "hms_control_server.app:app", "--host", "127.0.0.1", "--port", str(web_port), ] supervisor.start(ProcessSpec( name="control_core", cmd=control_cmd, restartable=True, max_restarts=5, stop_timeout_s=10.0, )) print(" Control Core gestartet") # 6. Healthcheck: warten bis Web-UI erreichbar ist (§9.2 Nr. 7) health_ok = await _wait_for_health(web_port, timeout_s=15.0) if not health_ok: print("FEHLER: Control Core Healthcheck fehlgeschlagen", file=sys.stderr) supervisor.shutdown() return 1 print(" Healthcheck grün") # 7. Recovery-Markierung prüfen (§26.4) recovery = supervisor.consume_recovery_marker() if recovery: print(f" WARNUNG: Unsauberer Shutdown erkannt: {recovery}") # 8. Browser öffnen (§9.2 Nr. 8, optional) if open_browser: _open_browser(f"http://127.0.0.1:{web_port}") print(" Browser geöffnet") # 9. Hauptschleife: Prozesse überwachen bis SIGINT/SIGTERM print("Anwendung läuft. Strg+C zum Beenden.") stop_event = asyncio.Event() loop = asyncio.get_running_loop() for sig in (signal.SIGINT, signal.SIGTERM): try: loop.add_signal_handler(sig, stop_event.set) except NotImplementedError: pass # Windows try: while not stop_event.is_set(): states = supervisor.check() for name, state in states.items(): if state == "crashloop": print(f" FEHLER: {name} im Crashloop", file=sys.stderr) stop_event.set() elif state == "restarted": print(f" {name} neu gestartet") await asyncio.sleep(1.0) finally: # Kontrolliertes Beenden (§26.4) print("Beende Anwendung...") supervisor.shutdown() print("Alle Prozesse beendet.") return 0 async def _wait_for_health(port: int, timeout_s: float = 15.0) -> bool: """Wartet bis /api/v1/system/health 200 OK liefert.""" import urllib.error import urllib.request deadline = time.monotonic() + timeout_s url = f"http://127.0.0.1:{port}/api/v1/system/health" while time.monotonic() < deadline: try: with urllib.request.urlopen(url, timeout=2.0) as resp: if resp.status == 200: return True except (urllib.error.URLError, OSError): await asyncio.sleep(0.5) return False def _open_browser(url: str) -> None: """Öffnet den Standardbrowser plattformneutral.""" import subprocess try: if sys.platform == "win32": subprocess.run( ["cmd", "/c", "start", url], check=False, timeout=5 ) elif sys.platform == "darwin": subprocess.run(["open", url], check=False, timeout=5) else: subprocess.run( ["xdg-open", url], check=False, timeout=5 ) except (OSError, subprocess.TimeoutExpired): pass # Browser ist optional; Web-UI ist auch direkt erreichbar def main() -> int: return asyncio.run(run()) if __name__ == "__main__": sys.exit(main())