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
This commit is contained in:
@@ -0,0 +1,178 @@
|
||||
"""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())
|
||||
Reference in New Issue
Block a user