1e921a9085
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)
337 lines
13 KiB
Python
337 lines
13 KiB
Python
#!/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()
|