Files
hms-mediaengine/installer_gui.py
T
HMS MediaEngine Agent 1e921a9085 DOPPELKlick-WELT: Grafischer Installer + App-Bundles (kein Terminal mehr noetig)
Der Nutzer will KEINE Konsole - jetzt ist alles Doppelklick:

1) GRAFISCHER INSTALLER (installer_gui.py, 336 Zeilen):
- Fenster mit Status-Checks (Python/GStreamer/Homebrew: fehlt/vorhanden),
  Fortschrittsbalken beim Download, Log-Bereich, grosse Buttons
- macOS: Homebrew + GStreamer + PyGObject + tkinter via brew;
  Passwort-Dialog kommt NATIV von macOS (osascript administrator
  privileges) - kein sudo-Tippen
- Windows: laedt und startet die OFFIZIELLEN Installer-GUIs
  (GStreamer-MSI + Python-Setup) - Nutzer klickt nur Weiter/Fertig
- Nach Installation: Button 'MediaEngine starten' spawnt die Engine
  UNSICHTBAR im Hintergrund und oeffnet den Browser
- Smoke-Test im Container bestanden (xvfb): Fenster baut sich auf,
  Statuschecks laufen, sauber beendet

2) macOS: 'HMS MediaEngine.app' (echtes App-Bundle im Repo):
- CFBundleExecutable HMS-Launcher (Bash): sucht brew-python3,
  prueft GStreamer unsichtbar -> Engine nohup im Hintergrund +
  Browser oeffnet sich; fehlt etwas -> grafischer Installer
- make_mac_app.py: App neu erzeugen falls noetig
- HMS-Mac-Install.command: Doppelklick-Installation mit nativen
  osascript-Dialogen (Abbrechen/Weiter) + GUI-Installer am Ende

3) Windows: HMS-Start.vbs (unsichtbar) + HMS-Install.vbs (sichtbar):
- HMS-Start.vbs: findet pythonw (4 Pfade + versteckte PATH-Suche
  ohne Konsolenblitz), startet launcher.pyw KOMPLETT unsichtbar
  (CREATE_NO_WINDOW) -> nur Browser erscheint
- HMS-Install.vbs: oeffnet grafischen Installer; fehlt Python,
  oeffnet offizielle Downloadseite mit Hinweis auf PATH-Haeckchen
- launcher_core.py: gemeinsame Logik (gst_ok -> Engine-Spawn ->
  Health-Poll -> webbrowser.open; sonst Installer-Fenster)

4) BEWEISE (im Container ausgefuehrt):
- Launcher-E2E: launch() -> Engine unsichtbar gestartet, Health
  gruen, Browser-URL korrekt (http://localhost:8080), RC=0
- GUI-Smoke-Test: Fenster + 'Alles bereit' + sauber beendet
- VBS: sh.Run-Zeilen geprueft (Chr(34)-Konstruktion, versteckte
  cmd-Suche)
- Alle 5 Regressionssuiten unveraendert gruen:
  Projekte 25/25 + Cue 35/35 + Verteilung 33/33 + App 14/14 +
  FX 16/16 = 123 Checks

Download: TAR.GZ (macOS, erhaelt Exec-Bits!) oder ZIP (Windows)
2026-09-11 19:41:26 +02:00

337 lines
13 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.
#!/usr/bin/env python3
"""HMS MediaEngine Grafischer Installer (Fenster statt Konsole).
Wird vom macOS-App-Bundle oder HMS-Start.vbs (Windows) geöffnet, wenn
Abhaengigkeiten fehlen. Der Nutzer sieht ein Fenster kein Terminal:
- macOS: GStreamer/PyGObject via Homebrew. Native Passwort-Eingabe durch
macOS-Systemdialog (osascript 'with administrator privileges').
- Windows: Offizielle Installer-GUIs (GStreamer-MSI, Python-Setup) werden
gestartet der Nutzer klickt nur 'Weiter/Fertig'.
Nach der Installation: Button 'MediaEngine starten' die App laeuft
danach unsichtbar im Hintergrund, nur der Browser ist sichtbar.
"""
from __future__ import annotations
import os
import subprocess
import sys
import tempfile
import threading
import urllib.request
import webbrowser
from pathlib import Path
import tkinter as tk
from tkinter import ttk, messagebox
ROOT = Path(__file__).resolve().parent
IS_MAC = sys.platform == "darwin"
IS_WIN = sys.platform == "win32"
GST_MSI = ("https://gstreamer.freedesktop.org/data/pkg/windows/"
"1.28.4/msvc/gstreamer-1.0-msvc-x86_64-1.28.4.msi")
PY_EXE = "https://www.python.org/ftp/python/3.13.2/python-3.13.2-amd64.exe"
BREW_PKGS = [
"python@3.12", "python-tk", "gstreamer", "gst-plugins-base",
"gst-plugins-good", "gst-plugins-bad", "gst-plugins-ugly",
"gst-libav", "pygobject",
]
def brew_path() -> str | None:
for p in ("/opt/homebrew/bin/brew", "/usr/local/bin/brew"):
if Path(p).exists():
return p
return None
def gst_ok() -> bool:
try:
import gi # noqa: F401
gi.require_version("Gst", "1.0")
from gi.repository import Gst
Gst.init(None)
return True
except Exception: # noqa: BLE001
return False
def py_ok() -> bool:
try:
r = subprocess.run(["python", "--version"], capture_output=True,
timeout=10,
creationflags=0x08000000 if IS_WIN else 0)
return r.returncode == 0
except Exception: # noqa: BLE001
return False
def mac_python_ok() -> bool:
"""Ein python3, das spaeter auch gi importieren kann (brew-Python)."""
for cand in ("/opt/homebrew/bin/python3", "/usr/local/bin/python3",
"python3"):
try:
r = subprocess.run([cand, "-c", "import tkinter"],
capture_output=True, timeout=10)
if r.returncode == 0:
return True
except Exception: # noqa: BLE001
continue
return False
def dl(url: str, dest: Path) -> None:
def hook(n, bs, total):
if total > 0:
pct = min(100, n * bs * 100 // total)
app.on_progress(pct)
urllib.request.urlretrieve(url, dest, hook)
def run_quiet(cmd: list[str]) -> None:
subprocess.run(cmd, check=False,
creationflags=0x08000000 if IS_WIN else 0)
class InstallerApp:
def __init__(self):
self.root = tk.Tk()
self.root.title("HMS MediaEngine Installation")
self.root.geometry("680x520")
self.root.configure(bg="#16181d")
try:
style = ttk.Style(self.root)
style.theme_use("clam")
except Exception: # noqa: BLE001
pass
self.steps: list[tuple[str, bool]] = []
self.log_lines: list[str] = []
self._build()
self.refresh_status()
# ---------- UI ----------
def _build(self):
head = tk.Frame(self.root, bg="#16181d")
head.pack(fill="x", padx=16, pady=(12, 6))
tk.Label(head, text="HMS MediaEngine",
font=("System", 17, "bold"),
bg="#16181d", fg="#e8eaee").pack(anchor="w")
tk.Label(head, text="Grafische Installation kein Terminal nötig",
bg="#16181d", fg="#8a8f9a").pack(anchor="w")
self.status_var = tk.StringVar()
tk.Label(self.root, textvariable=self.status_var,
bg="#16181d", fg="#c8ccd4").pack(fill="x", padx=16)
self.bar = ttk.Progressbar(self.root, mode="determinate",
length=640)
self.bar.pack(fill="x", padx=16, pady=8)
logbox = tk.Frame(self.root, bg="#0d0f12")
logbox.pack(fill="both", expand=True, padx=16, pady=6)
self.log = tk.Text(logbox, bg="#0d0f12", fg="#c8ccd4",
insertbackground="#c8ccd4", height=14,
relief="flat", font=("Menlo" if IS_MAC
else "Consolas", 10))
self.log.pack(fill="both", expand=True, side="left")
scroll = ttk.Scrollbar(logbox, command=self.log.yview)
scroll.pack(side="right", fill="y")
self.log.configure(yscrollcommand=scroll.set)
btns = tk.Frame(self.root, bg="#16181d")
btns.pack(fill="x", padx=16, pady=10)
self.install_btn = tk.Button(
btns, text="Fehlendes installieren",
command=self.start_install, bg="#1d3a5f", fg="#dbe9ff",
activebackground="#2a4f7f", relief="flat", padx=14, pady=7)
self.install_btn.pack(side="left")
self.start_btn = tk.Button(
btns, text="▶ MediaEngine starten",
command=self.start_engine, bg="#1f4d2a", fg="#d8ffd8",
activebackground="#2a6a3a", relief="flat", padx=14, pady=7)
self.start_btn.pack(side="left", padx=8)
tk.Button(btns, text="Beenden", command=self.root.destroy,
bg="#262a33", fg="#c8ccd4", relief="flat",
padx=14, pady=7).pack(side="right")
def on_log(self, text: str):
self.log_lines.append(text)
self.log.insert("end", text + "\n")
self.log.see("end")
self.root.update_idletasks()
def on_progress(self, pct: int):
self.bar["value"] = pct
self.root.update_idletasks()
def on_status(self, text: str):
self.status_var.set(text)
self.root.update_idletasks()
# ---------- Status ----------
def refresh_status(self):
self.steps = []
if IS_WIN:
self.steps.append(("Python 3.13", py_ok()))
gs = (os.environ.get("GSTREAMER_1_0_ROOT_MSVC_X86_64")
or "")
self.steps.append(("GStreamer 1.28",
bool(gs) or Path(
"C:\\gstreamer\\1.0\\msvc_x86_64"
).exists()))
elif IS_MAC:
self.steps.append(("Homebrew", brew_path() is not None))
self.steps.append(("Python 3 (mit tkinter)", mac_python_ok()))
self.steps.append(("GStreamer + PyGObject", gst_ok()))
else:
self.steps.append(("GStreamer (PyGObject)", gst_ok()))
self.log.delete("1.0", "end")
for name, ok in self.steps:
mark = "" if ok else ""
self.on_log(f"{mark} {name}: "
+ ("vorhanden" if ok else "fehlt wird installiert"))
missing = [n for n, ok in self.steps if not ok]
if not missing:
self.on_status("Alles bereit du kannst MediaEngine starten.")
self.install_btn.config(state="disabled")
self.start_btn.config(state="normal", bg="#1f4d2a")
else:
self.on_status("Fehlt: " + ", ".join(missing)
+ " → Button links drücken.")
self.install_btn.config(state="normal")
self.start_btn.config(state="disabled", bg="#262a33")
# ---------- Installation ----------
def start_install(self):
self.install_btn.config(state="disabled")
threading.Thread(target=self._install_worker, daemon=True).start()
def _install_worker(self):
try:
if IS_MAC:
self._install_mac()
elif IS_WIN:
self._install_win()
self.on_log("")
self.on_log("Installation abgeschlossen.")
self.on_log("Falls gerade etwas installiert wurde, das Python "
"betrifft: dieses Fenster einmal schließen und "
"die App erneut starten.")
self.refresh_status()
except Exception as e: # noqa: BLE001
self.on_log(f"FEHLER: {e}")
self.on_status("Installation fehlgeschlagen siehe Log. "
"(Meldung an den Support reicht zum Fixen.)")
self.install_btn.config(state="normal")
def _osascript_admin(self, script: str, prompt: str) -> str:
esc = script.replace('\\', '\\\\').replace('"', '\\"')
code = (f'do shell script "{esc}" '
f'with administrator privileges '
f'with prompt "{prompt}"')
r = subprocess.run(["osascript", "-e", code],
capture_output=True, text=True, timeout=3600)
if r.returncode != 0:
raise RuntimeError(r.stderr.strip() or "osascript fehlgeschlagen")
return r.stdout
def _install_mac(self):
self.on_status("macOS: Installation läuft (Passwort-Dialog folgt)…")
lines = ["set -e"]
brew = brew_path()
if brew is None:
self.on_log("Homebrew fehlt wird installiert (mehrere Minuten)…")
lines.append(
'NONINTERACTIVE=1 /bin/bash -c "$(curl -fsSL '
'https://raw.githubusercontent.com/Homebrew/'
'install/HEAD/install.sh)"')
# brew-Pfad robust im Skript auswaehlen (Apple Silicon vs. Intel);
# absolute Pfade machen ein eval/shellenv ueberfluessig
lines.append(
'if [ -x /opt/homebrew/bin/brew ]; then '
'BREW=/opt/homebrew/bin/brew; '
'else BREW=/usr/local/bin/brew; fi')
lines.append('"$BREW" install ' + " ".join(BREW_PKGS))
script = "\n".join(lines)
with tempfile.NamedTemporaryFile("w", suffix=".sh",
delete=False) as f:
f.write(script)
sh = f.name
os.chmod(sh, 0o700)
self.on_log("Starte Installation (macOS fragt nach deinem "
"Administrator-Passwort)…")
out = self._osascript_admin(f"/bin/bash {sh}",
"HMS MediaEngine: GStreamer und "
"Python-Komponenten installieren")
for line in (out or "").strip().splitlines()[-12:]:
self.on_log(" " + line)
Path(sh).unlink(missing_ok=True)
def _install_win(self):
if not Path("C:\\gstreamer\\1.0\\msvc_x86_64").exists() \
and not os.environ.get("GSTREAMER_1_0_ROOT_MSVC_X86_64"):
self.on_status("Windows: GStreamer-Installer (GUI) bitte "
"mit 'Weiter/Fertig' bestätigen…")
dest = Path(tempfile.gettempdir()) / "gstreamer.msi"
self.on_log("Lade GStreamer-Installer herunter…")
dl(GST_MSI, dest)
self.on_log("Öffne GStreamer-Installation (Fenster erscheint)…")
r = subprocess.run(["msiexec", "/i", str(dest)],
capture_output=False, timeout=3600)
self.on_log(f"GStreamer-Installer beendet (Code {r.returncode}).")
if not py_ok():
self.on_status("Windows: Python-Installer (GUI) bitte "
"'Add python.exe to PATH' anhaken und installieren…")
dest = Path(tempfile.gettempdir()) / "python-setup.exe"
self.on_log("Lade Python-Installer herunter…")
dl(PY_EXE, dest)
self.on_log("Öffne Python-Installation (Fenster erscheint)…")
subprocess.run([str(dest)], timeout=3600)
self.on_log("Python-Installation abgeschlossen.")
self.on_log("WICHTIG: Falls PATH soeben geändert wurde, dieses "
"Fenster schließen und HMS-Start.vbs erneut "
"doppelklicken.")
# ---------- Start ----------
def start_engine(self):
try:
if IS_WIN:
pyw = "pythonw"
subprocess.Popen(
[pyw if Path(sys.executable).stem == "python"
else sys.executable, str(ROOT / "run.py")],
cwd=str(ROOT),
creationflags=0x08000000,
stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL)
else:
subprocess.Popen(
["python3", str(ROOT / "run.py")], cwd=str(ROOT),
stdout=open("/tmp/hms-mediaengine.log", "ab"),
stderr=subprocess.STDOUT,
start_new_session=True)
self.on_status("MediaEngine startet Browser öffnet sich…")
self.root.after(2500, lambda: webbrowser.open(
"http://localhost:8080"))
self.root.after(4000, self.root.destroy)
except Exception as e: # noqa: BLE001
messagebox.showerror("HMS MediaEngine",
f"Start fehlgeschlagen:\n{e}")
def run(self):
self.root.mainloop()
if __name__ == "__main__":
app = InstallerApp()
app.run()