Files
hms-mediaengine/apps/launcher/hms_launcher/__main__.py
T
HMS MediaEngine Agent f7f52f909d Launcher: vollständiger Startablauf nach §9.2
- run(): App-Root bestimmen, Schreibbarkeit prüfen (§9.1), freie Ports
  wählen (§6.1A), Renderer + Control Core über Supervisor starten
- Healthcheck: wartet bis /api/v1/system/health 200 OK (§9.2 Nr. 7)
- Recovery-Markierung: unsauberer Shutdown wird sichtbar (§26.4)
- Browser: plattformneutral öffnen (Windows/Darwin/Linux, optional)
- Hauptschleife: Supervisor-Überwachung, Crashloop-Erkennung,
  SIGINT/SIGTERM mit kontrolliertem Beiden beider Prozesse (§26.4)
- Exit-Code 0 bei kontrolliertem Beenden, 1 bei Healthcheck-Fehler
2026-09-11 08:38:05 +02:00

179 lines
5.4 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""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())