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)
This commit is contained in:
@@ -0,0 +1,16 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>CFBundleName</key><string>HMS MediaEngine</string>
|
||||
<key>CFBundleDisplayName</key><string>HMS MediaEngine</string>
|
||||
<key>CFBundleIdentifier</key><string>de.hms.mediaengine</string>
|
||||
<key>CFBundleVersion</key><string>0.1.0</string>
|
||||
<key>CFBundleShortVersionString</key><string>0.1.0</string>
|
||||
<key>CFBundlePackageType</key><string>APPL</string>
|
||||
<key>CFBundleExecutable</key><string>HMS-Launcher</string>
|
||||
<key>LSMinimumSystemVersion</key><string>11.0</string>
|
||||
<key>NSHighResolutionCapable</key><true/>
|
||||
<key>CFBundleInfoDictionaryVersion</key><string>6.0</string>
|
||||
</dict>
|
||||
</plist>
|
||||
+26
@@ -0,0 +1,26 @@
|
||||
#!/bin/bash
|
||||
# HMS MediaEngine App-Start (vom Finder, ohne sichtbares Terminal)
|
||||
DIR="$(cd "$(dirname "$0")/../../.." && pwd)"
|
||||
PY=""
|
||||
for p in /opt/homebrew/bin/python3 /usr/local/bin/python3 python3; do
|
||||
if command -v "$p" >/dev/null 2>&1; then PY="$p"; break; fi
|
||||
done
|
||||
if [ -z "$PY" ]; then
|
||||
osascript -e 'display alert "Python nicht gefunden" message "Bitte HMS-Mac-Install.command im Projektordner einmal ausfuehren - es installiert alles Grafische." as critical' >/dev/null 2>&1
|
||||
exit 1
|
||||
fi
|
||||
cd "$DIR"
|
||||
if "$PY" -c "import gi; gi.require_version('Gst','1.0')" >/dev/null 2>&1; then
|
||||
nohup "$PY" "$DIR/run.py" >>/tmp/hms-mediaengine.log 2>&1 &
|
||||
PORT=8080
|
||||
for i in 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 \
|
||||
21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40; do
|
||||
curl -s -o /dev/null "http://localhost:$PORT/api/health" && break
|
||||
sleep 0.5
|
||||
done
|
||||
open "http://localhost:$PORT"
|
||||
exit 0
|
||||
else
|
||||
nohup "$PY" "$DIR/installer_gui.py" >/tmp/hms-installer.log 2>&1 &
|
||||
exit 0
|
||||
fi
|
||||
@@ -0,0 +1,33 @@
|
||||
' HMS MediaEngine - Erstinstallation (Windows, Doppelklick)
|
||||
' Oeffnet den grafischen Installer (Fenster, kein Tippen noetig).
|
||||
Option Explicit
|
||||
Dim fso, sh, py, cand, dir_, candidates
|
||||
Set fso = CreateObject("Scripting.FileSystemObject")
|
||||
Set sh = CreateObject("WScript.Shell")
|
||||
dir_ = fso.GetParentFolderName(WScript.ScriptFullName)
|
||||
|
||||
py = ""
|
||||
On Error Resume Next
|
||||
candidates = Array( _
|
||||
sh.ExpandEnvironmentStrings("%LOCALAPPDATA%") & "\Programs\Python\Python313\pythonw.exe", _
|
||||
sh.ExpandEnvironmentStrings("%LOCALAPPDATA%") & "\Programs\Python\Python312\pythonw.exe", _
|
||||
"C:\Python313\pythonw.exe", _
|
||||
"C:\Python312\pythonw.exe")
|
||||
For Each cand In candidates
|
||||
If py = "" Then
|
||||
If fso.FileExists(cand) Then py = cand
|
||||
End If
|
||||
Next
|
||||
On Error GoTo 0
|
||||
|
||||
If py = "" Then
|
||||
MsgBox "Python fehlt noch." & vbCrLf & vbCrLf & _
|
||||
"Der Browser oeffnet jetzt die offizielle Python-Downloadseite." & vbCrLf & _
|
||||
"Nach der Python-Installation (Haeckchen bei 'Add python.exe to PATH'!)" & vbCrLf & _
|
||||
"bitte HMS-Install.vbs erneut doppelklicken.", 64, "HMS MediaEngine"
|
||||
sh.Run "cmd /c start https://www.python.org/downloads/", 0, False
|
||||
WScript.Quit 1
|
||||
End If
|
||||
|
||||
sh.CurrentDirectory = dir_
|
||||
sh.Run Chr(34) & py & Chr(34) & " " & Chr(34) & dir_ & "\installer_gui.py" & Chr(34), 1, False
|
||||
Executable
+50
@@ -0,0 +1,50 @@
|
||||
#!/bin/bash
|
||||
# HMS MediaEngine – macOS-Installation per Doppelklick (Finder)
|
||||
# Ablauf: brew pruefen/installieren -> python3+tk pruefen ->
|
||||
# GUI-Installer starten (Passwort-Dialog kommt von macOS selbst).
|
||||
# Falls python3/tkinter fehlt: Fallback-Dialoge nativ per osascript.
|
||||
|
||||
cd "$(dirname "$0")"
|
||||
|
||||
echo "HMS MediaEngine – Installation"
|
||||
|
||||
eingabe() {
|
||||
osascript -e 'display alert "'$1'" message "'$2'" buttons {"Abbrechen","Weiter"} default button "Weiter" as informational' >/dev/null 2>&1
|
||||
[ $? -eq 0 ]
|
||||
}
|
||||
|
||||
# --- Homebrew ---
|
||||
BREW=""
|
||||
for p in /opt/homebrew/bin/brew /usr/local/bin/brew; do
|
||||
if [ -x "$p" ]; then BREW="$p"; break; fi
|
||||
done
|
||||
if [ -z "$BREW" ]; then
|
||||
if eingabe "Homebrew installieren?" "HMS MediaEngine benötigt Homebrew (der Paketmanager für macOS). Es wird jetzt installiert – danach geht es automatisch weiter."; then
|
||||
NONINTERACTIVE=1 /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"
|
||||
BREW=/opt/homebrew/bin/brew
|
||||
else
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
# --- Python 3.12 + tkinter ---
|
||||
"$BREW" list python-tk >/dev/null 2>&1 || {
|
||||
if eingabe "Python-Komponenten installieren?" "Es wird python@3.12 mit tkinter installiert (für die grafische Oberfläche)."; then
|
||||
"$BREW" install python@3.12 python-tk
|
||||
fi
|
||||
}
|
||||
|
||||
# --- GStreamer ---
|
||||
"$BREW" list gstreamer >/dev/null 2>&1 || {
|
||||
if eingabe "GStreamer installieren?" "Es werden GStreamer und alle Video-/Audio-Plugins installiert (ca. 5–10 Minuten)."; then
|
||||
"$BREW" install gstreamer gst-plugins-base gst-plugins-good gst-plugins-bad gst-plugins-ugly gst-libav pygobject
|
||||
fi
|
||||
}
|
||||
|
||||
# --- GUI-Installer starten ---
|
||||
PY=/opt/homebrew/bin/python3
|
||||
if [ ! -x "$PY" ]; then PY=/usr/local/bin/python3; fi
|
||||
|
||||
echo ""
|
||||
echo "Starte grafischen Installer (Fenster öffnet sich)..."
|
||||
"$PY" installer_gui.py
|
||||
@@ -0,0 +1,51 @@
|
||||
' HMS MediaEngine - Windows-Starter (unsichtbar, Doppelklick)
|
||||
' Startet die Engine im Hintergrund (nur Browser sichtbar).
|
||||
' Fehlen Abhaengigkeiten, oeffnet sich der grafische Installer.
|
||||
Option Explicit
|
||||
Dim fso, sh, py, cand, dir_, tmpFile, ts, out
|
||||
Dim candidates(3)
|
||||
Set fso = CreateObject("Scripting.FileSystemObject")
|
||||
Set sh = CreateObject("WScript.Shell")
|
||||
dir_ = fso.GetParentFolderName(WScript.ScriptFullName)
|
||||
|
||||
py = ""
|
||||
candidates(0) = sh.ExpandEnvironmentStrings("%LOCALAPPDATA%") & "\Programs\Python\Python313\pythonw.exe"
|
||||
candidates(1) = sh.ExpandEnvironmentStrings("%LOCALAPPDATA%") & "\Programs\Python\Python312\pythonw.exe"
|
||||
candidates(2) = "C:\Python313\pythonw.exe"
|
||||
candidates(3) = "C:\Python312\pythonw.exe"
|
||||
For Each cand In candidates
|
||||
If py = "" Then
|
||||
If fso.FileExists(cand) Then py = cand
|
||||
End If
|
||||
Next
|
||||
|
||||
If py = "" Then
|
||||
' PATH-Suche VERSTECKT (sh.Exec wuerde ein Konsolenfenster zeigen):
|
||||
' cmd versteckt laufen lassen, Ausgabe in Temp-Datei
|
||||
tmpFile = sh.ExpandEnvironmentStrings("%TEMP%") & "\hms_py_path.txt"
|
||||
On Error Resume Next
|
||||
sh.Run "cmd /c where pythonw > """ & tmpFile & """ 2>nul", 0, True
|
||||
If fso.FileExists(tmpFile) Then
|
||||
Set ts = fso.OpenTextFile(tmpFile, 1)
|
||||
If Not ts.AtEndOfStream Then
|
||||
out = Trim(ts.ReadLine())
|
||||
If Len(out) > 4 Then
|
||||
If fso.FileExists(out) Then py = out
|
||||
End If
|
||||
End If
|
||||
ts.Close
|
||||
fso.DeleteFile tmpFile, True
|
||||
End If
|
||||
On Error GoTo 0
|
||||
End If
|
||||
|
||||
If py = "" Then
|
||||
MsgBox "Python wurde nicht gefunden." & vbCrLf & vbCrLf & _
|
||||
"Bitte einmal 'HMS-Install.vbs' doppelklicken -" & vbCrLf & _
|
||||
"es installiert Python und GStreamer ueber grafische Installer.", _
|
||||
48, "HMS MediaEngine"
|
||||
WScript.Quit 1
|
||||
End If
|
||||
|
||||
sh.CurrentDirectory = dir_
|
||||
sh.Run Chr(34) & py & Chr(34) & " " & Chr(34) & dir_ & "\launcher.pyw" & Chr(34), 0, False
|
||||
@@ -0,0 +1,336 @@
|
||||
#!/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()
|
||||
@@ -0,0 +1,8 @@
|
||||
# Unsichtbarer Windows-Launcher (pythonw ohne Konsolenfenster).
|
||||
# Wird von HMS-Start.vbs aufgerufen.
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
sys.path.insert(0, str(Path(__file__).resolve().parent))
|
||||
from launcher_core import launch
|
||||
sys.exit(launch())
|
||||
@@ -0,0 +1,83 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Gemeinsame Launcher-Logik fuer Windows-Starter und macOS.
|
||||
|
||||
Unsichtbarer Ablauf: GStreamer vorhanden -> Engine im Hintergrund
|
||||
starten, auf die Web-UI warten, Browser oeffnen.
|
||||
Fehlt etwas -> grafischer Installer (installer_gui.py) oeffnen.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import subprocess
|
||||
import sys
|
||||
import time
|
||||
import urllib.request
|
||||
import webbrowser
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path(__file__).resolve().parent
|
||||
IS_WIN = sys.platform == "win32"
|
||||
|
||||
|
||||
def web_port() -> int:
|
||||
try:
|
||||
cfg = json.loads((ROOT / "config.json").read_text("utf-8"))
|
||||
return int(cfg.get("web", {}).get("port", 8080))
|
||||
except Exception: # noqa: BLE001
|
||||
return 8080
|
||||
|
||||
|
||||
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 _spawn(py: str, target: str, hidden: bool) -> None:
|
||||
if IS_WIN and hidden:
|
||||
subprocess.Popen(
|
||||
[py, str(ROOT / target)], cwd=str(ROOT),
|
||||
creationflags=0x08000000, # CREATE_NO_WINDOW
|
||||
stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
|
||||
else:
|
||||
subprocess.Popen(
|
||||
[py, str(ROOT / target)], cwd=str(ROOT),
|
||||
stdout=open("/tmp/hms-mediaengine.log", "ab"),
|
||||
stderr=subprocess.STDOUT, start_new_session=not IS_WIN)
|
||||
|
||||
|
||||
def wait_health(port: int, timeout_s: float = 25.0) -> bool:
|
||||
deadline = time.monotonic() + timeout_s
|
||||
while time.monotonic() < deadline:
|
||||
try:
|
||||
with urllib.request.urlopen(
|
||||
f"http://localhost:{port}/api/health",
|
||||
timeout=1.5) as r:
|
||||
if json.loads(r.read().decode()).get("status") == "ok":
|
||||
return True
|
||||
except Exception: # noqa: BLE001
|
||||
time.sleep(0.4)
|
||||
return False
|
||||
|
||||
|
||||
def launch() -> int:
|
||||
if gst_ok():
|
||||
_spawn(sys.executable, "run.py", hidden=True)
|
||||
port = web_port()
|
||||
if wait_health(port):
|
||||
webbrowser.open(f"http://localhost:{port}")
|
||||
return 0
|
||||
print("[Launcher] Engine antwortete nicht rechtzeitig – "
|
||||
"oeffne Installer.", file=sys.stderr)
|
||||
_spawn(sys.executable, "installer_gui.py", hidden=False)
|
||||
return 1
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(launch())
|
||||
@@ -0,0 +1,77 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Erzeugt 'HMS MediaEngine.app' im Projektordner (macOS App-Bundle).
|
||||
|
||||
Aufruf: python3 make_mac_app.py
|
||||
Danach: 'HMS MediaEngine.app' per Doppelklick im Finder starten.
|
||||
Der Launcher prueft unsichtbar: fehlt GStreamer, oeffnet sich der
|
||||
grafische Installer; ist alles da, startet die Engine im Hintergrund
|
||||
und der Browser oeffnet sich.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path(__file__).resolve().parent
|
||||
APP = ROOT / "HMS MediaEngine.app"
|
||||
|
||||
INFO_PLIST = """<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>CFBundleName</key><string>HMS MediaEngine</string>
|
||||
<key>CFBundleDisplayName</key><string>HMS MediaEngine</string>
|
||||
<key>CFBundleIdentifier</key><string>de.hms.mediaengine</string>
|
||||
<key>CFBundleVersion</key><string>0.1.0</string>
|
||||
<key>CFBundleShortVersionString</key><string>0.1.0</string>
|
||||
<key>CFBundlePackageType</key><string>APPL</string>
|
||||
<key>CFBundleExecutable</key><string>HMS-Launcher</string>
|
||||
<key>LSMinimumSystemVersion</key><string>11.0</string>
|
||||
<key>NSHighResolutionCapable</key><true/>
|
||||
<key>CFBundleInfoDictionaryVersion</key><string>6.0</string>
|
||||
</dict>
|
||||
</plist>
|
||||
"""
|
||||
|
||||
LAUNCHER = r'''#!/bin/bash
|
||||
# HMS MediaEngine App-Start (vom Finder, ohne sichtbares Terminal)
|
||||
DIR="$(cd "$(dirname "$0")/../../.." && pwd)"
|
||||
PY=""
|
||||
for p in /opt/homebrew/bin/python3 /usr/local/bin/python3 python3; do
|
||||
if command -v "$p" >/dev/null 2>&1; then PY="$p"; break; fi
|
||||
done
|
||||
if [ -z "$PY" ]; then
|
||||
osascript -e 'display alert "Python nicht gefunden" message "Bitte HMS-Mac-Install.command im Projektordner einmal ausfuehren - es installiert alles Grafische." as critical' >/dev/null 2>&1
|
||||
exit 1
|
||||
fi
|
||||
cd "$DIR"
|
||||
if "$PY" -c "import gi; gi.require_version('Gst','1.0')" >/dev/null 2>&1; then
|
||||
nohup "$PY" "$DIR/run.py" >>/tmp/hms-mediaengine.log 2>&1 &
|
||||
PORT=8080
|
||||
for i in 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 \
|
||||
21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40; do
|
||||
curl -s -o /dev/null "http://localhost:$PORT/api/health" && break
|
||||
sleep 0.5
|
||||
done
|
||||
open "http://localhost:$PORT"
|
||||
exit 0
|
||||
else
|
||||
nohup "$PY" "$DIR/installer_gui.py" >/tmp/hms-installer.log 2>&1 &
|
||||
exit 0
|
||||
fi
|
||||
'''
|
||||
|
||||
|
||||
def build() -> None:
|
||||
macos = APP / "Contents" / "MacOS"
|
||||
macos.mkdir(parents=True, exist_ok=True)
|
||||
(APP / "Contents" / "Info.plist").write_text(INFO_PLIST, "utf-8")
|
||||
launcher = macos / "HMS-Launcher"
|
||||
launcher.write_text(LAUNCHER, "utf-8")
|
||||
launcher.chmod(0o755)
|
||||
print(f"App erstellt: {APP}")
|
||||
print("Doppelklick im Finder: 'HMS MediaEngine.app'")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
build()
|
||||
Reference in New Issue
Block a user