362e089be0
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).
179 lines
5.4 KiB
Python
179 lines
5.4 KiB
Python
"""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())
|