Files
hms-mediaengine/_entwicklung/apps/launcher/hms_launcher/__main__.py
T
HMS MediaEngine Agent 362e089be0 AUFGERAUMT: Root auf 10 sichtbare Elemente reduziert
Der Nutzer hat recht: Der Ordner war voller Entwicklungs-Muell.
Jetzt ist sauber getrennt:

ROOT (was der Nutzer sieht und braucht):
- run.py                     = das Programm
- hms_app/                   = der Anwendungscode
- HMS MediaEngine.app        = macOS Doppelklick-Starter
- HMS-Start.vbs              = Windows Doppelklick-Starter
- HMS-Install.vbs             = Windows Erst-Installation
- HMS-Mac-Install.command     = macOS Homebrew-Installation
- HMS-Portable-Install.command = macOS Portable-Installation (16GB-Fix)
- installer_gui.py           = grafischer Installer
- launcher.pyw + launcher_core.py = interne Start-Logik
- LIESMICH.txt               = 10-Zeilen-Kurzanleitung
- .gitignore

_entwicklung/ (alles andere, NICHT benoetigt):
- packages/ apps/ native/ plugins/ tools/ schemas/ tests/ docs/
  build/ fixture_profiles/
- PLAN.md STATUS.md ERRORS.md TEST_REPORT.md CHANGELOG.md README.md
- pyproject.toml uv.lock setup_*.sh/ps1 make_mac_app.py

Diese Trennung gilt ab sofort fuer alle Commits. Der Nutzer kann
_entwicklung/ loeschen wenn er Platz braucht - die App laeuft ohne.

Verifiziert: App startet nach Aufraeumen unveraendert (Health 200).
2026-09-11 23:44:06 +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())