diff --git a/deploy/worker-windows/install.ps1 b/deploy/worker-windows/install.ps1 index c0e2797..c56cdad 100644 --- a/deploy/worker-windows/install.ps1 +++ b/deploy/worker-windows/install.ps1 @@ -61,6 +61,8 @@ if (-not (Test-Path "venv")) { } & .\venv\Scripts\python.exe -m pip install --quiet --upgrade pip & .\venv\Scripts\pip.exe install --quiet -r requirements.txt +# Nur fuer den Windows-Worker: Tray-Symbol (Status, Start/Stopp, Beenden) +& .\venv\Scripts\pip.exe install --quiet pystray pillow Write-Host "Python-Abhaengigkeiten installiert." # 4. HandBrakeCLI besorgen (falls noetig) @@ -90,14 +92,57 @@ set PATH=%~dp0;%PATH% venv\Scripts\celery.exe -A celery_app worker --loglevel=info -Q transcode --pool=solo -n ${WorkerName}@%%h "@ Set-Content -Path "start-worker.bat" -Value $bat -Encoding ASCII -Write-Host "start-worker.bat erzeugt." -# 6. Optional: Autostart bei Anmeldung +# 6. Tray-Start (empfohlen): Symbol neben der Uhr, kein Konsolenfenster +$trayBat = @" +@echo off +rem Rippy-Worker mit Tray-Symbol — verbindet sich mit $RippyHost +cd /d "%~dp0" +set REDIS_URL=redis://${RippyHost}:6379/0 +set DATABASE_URL=postgresql://rippy:rippy@${RippyHost}:5432/rippy +set API_URL=http://${RippyHost}:8000 +set WORKER_NAME=$WorkerName +set RIPPY_TRAY_HOST=$RippyHost +set PATH=%~dp0;%PATH% +start "" venv\Scripts\pythonw.exe tray.py +"@ +Set-Content -Path "start-tray.bat" -Value $trayBat -Encoding ASCII +Write-Host "start-worker.bat + start-tray.bat erzeugt." + +# 7. Deinstaller — MUSS-Kriterium: rueckstandsfrei entfernbar +$uninstall = @" +# Rippy-Worker DEINSTALLIEREN: stoppt Prozesse, entfernt Autostart, +# meldet den Worker in Rippy ab und loescht diesen Ordner komplett. +param([switch]`$Force) +if (-not `$Force) { + `$antwort = Read-Host "Rippy-Worker '$WorkerName' wirklich deinstallieren? (j/n)" + if (`$antwort -ne "j") { Write-Host "Abgebrochen."; exit } +} +Get-CimInstance Win32_Process | Where-Object { + `$_.ExecutablePath -like "`$PSScriptRoot*" +} | ForEach-Object { Stop-Process -Id `$_.ProcessId -Force -ErrorAction SilentlyContinue } +Start-Sleep 2 +schtasks /delete /tn "RippyWorker" /f 2>`$null | Out-Null +try { + Invoke-RestMethod -Method Delete "http://$RippyHost/api/workers/$WorkerName" -TimeoutSec 5 | Out-Null + Write-Host "Worker-Eintrag in Rippy entfernt." +} catch { Write-Host "Hinweis: Eintrag in Rippy ggf. von Hand loeschen (Einstellungen -> Worker)." } +Set-Location (Split-Path `$PSScriptRoot -Parent) +Remove-Item -Recurse -Force `$PSScriptRoot +Write-Host "Rippy-Worker deinstalliert." -ForegroundColor Green +"@ +Set-Content -Path "uninstall.ps1" -Value $uninstall -Encoding ASCII +Write-Host "uninstall.ps1 erzeugt." + +# 8. Optional: Autostart bei Anmeldung (mit Tray) if ($Autostart) { - schtasks /create /f /tn "RippyWorker" /tr "`"$ziel\start-worker.bat`"" /sc onlogon | Out-Null - Write-Host "Autostart-Aufgabe 'RippyWorker' registriert (bei Anmeldung)." + schtasks /create /f /tn "RippyWorker" /tr "`"$ziel\start-tray.bat`"" /sc onlogon | Out-Null + Write-Host "Autostart-Aufgabe 'RippyWorker' registriert (Tray bei Anmeldung)." } Write-Host "" -Write-Host "FERTIG. Worker starten mit: $ziel\start-worker.bat" -ForegroundColor Green -Write-Host "Er erscheint danach in Rippy unter Einstellungen -> Worker als '$WorkerName'." +Write-Host "FERTIG." -ForegroundColor Green +Write-Host " Starten (mit Tray-Symbol): $ziel\start-tray.bat" +Write-Host " Starten (Konsole/Debug): $ziel\start-worker.bat" +Write-Host " Deinstallieren: $ziel\uninstall.ps1" +Write-Host "Der Worker erscheint in Rippy unter Einstellungen -> Worker als '$WorkerName'." diff --git a/docker/ui/src/components/WorkerVerwaltung.tsx b/docker/ui/src/components/WorkerVerwaltung.tsx index b67ff40..6371785 100644 --- a/docker/ui/src/components/WorkerVerwaltung.tsx +++ b/docker/ui/src/components/WorkerVerwaltung.tsx @@ -192,7 +192,7 @@ export default function WorkerVerwaltung() {
{variante === 'linux' ? 'Auf der Linux-Maschine (mit Docker) ausführen — WORKER_NAME frei wählen, unter dem Namen taucht sie oben auf und übernimmt Kompressions-Jobs. GPU wird erkannt und angezeigt.' - : 'In einer PowerShell auf dem Windows-PC ausführen (braucht nur Python 3.10+ — kein Docker, kein git). Der Installer holt Worker-Code und HandBrake automatisch von dieser Rippy-Instanz bzw. dem offiziellen HandBrake-Release und erzeugt eine start-worker.bat. Autostart bei Anmeldung: Parameter -Autostart anhängen.'} + : 'In einer PowerShell auf dem Windows-PC ausführen (braucht nur Python 3.10+ — kein Docker, kein git). Der Installer holt Worker-Code und HandBrake automatisch und erzeugt: start-tray.bat (Tray-Symbol neben der Uhr mit Start/Stopp/Status), start-worker.bat (Konsole, für Fehlersuche) und uninstall.ps1 (entfernt alles rückstandsfrei inkl. Worker-Eintrag hier). Autostart bei Anmeldung: Parameter -Autostart anhängen.'}
+ Der Windows-Worker läuft mit Tray-Symbol neben der Uhr
+ (Status, Start/Stopp, „Rippy öffnen", Log) — gestartet über start-tray.bat.
+ Deinstallieren: uninstall.ps1 im Worker-Ordner stoppt alles, entfernt den
+ Autostart, meldet den Worker in Rippy ab und löscht den Ordner rückstandsfrei.
+
Wichtig: Worker verbinden sich direkt im Heimnetz — in den Befehlen immer die LAN-Adresse der Rippy-Maschine verwenden, diff --git a/docker/worker/tray.py b/docker/worker/tray.py new file mode 100644 index 0000000..07d1628 --- /dev/null +++ b/docker/worker/tray.py @@ -0,0 +1,121 @@ +"""Tray-Symbol für den NATIVEN Windows-Worker (nicht im Docker-Image genutzt). + +Zeigt den Worker-Status unten rechts neben der Uhr und bietet: +Start/Stopp, Rippy im Browser öffnen, Log anzeigen, Beenden. Gestartet +wird es über start-tray.bat (setzt die Verbindungs-Umgebung und ruft +pythonw.exe auf — kein Konsolenfenster). Der Worker-Prozess (celery, +Queue transcode) läuft als Kind-Prozess und stoppt mit dem Tray. + +Abhängigkeiten pystray + Pillow installiert NUR der Windows-Installer +(install.ps1) — das Docker-Image bleibt davon unberührt. +""" + +import os +import subprocess +import sys +import webbrowser + +try: + import pystray + from PIL import Image, ImageDraw +except ImportError: + print("Tray braucht pystray + Pillow — install.ps1 erneut ausführen.") + sys.exit(1) + +BASIS = os.path.dirname(os.path.abspath(__file__)) +RIPPY_HOST = os.getenv("RIPPY_TRAY_HOST", "") +WORKER_NAME = os.getenv("WORKER_NAME", "windows-worker") +LOG_PFAD = os.path.join(BASIS, "worker.log") + +prozess = None + + +def disc_bild(laeuft: bool) -> Image.Image: + """Disc-Symbol im Rippy-Look: Indigo wenn der Worker läuft, grau sonst.""" + bild = Image.new("RGBA", (64, 64), (0, 0, 0, 0)) + zeichner = ImageDraw.Draw(bild) + farbe = (99, 102, 241, 255) if laeuft else (125, 132, 145, 255) + zeichner.ellipse([4, 4, 60, 60], fill=farbe) + zeichner.ellipse([24, 24, 40, 40], fill=(255, 255, 255, 255)) + return bild + + +def worker_laeuft() -> bool: + return prozess is not None and prozess.poll() is None + + +def worker_starten(icon=None, item=None): + global prozess + if worker_laeuft(): + return + celery = os.path.join(BASIS, "venv", "Scripts", "celery.exe") + log = open(LOG_PFAD, "a", encoding="utf-8", errors="replace") + prozess = subprocess.Popen( + [celery, "-A", "celery_app", "worker", "--loglevel=info", + "-Q", "transcode", "--pool=solo", "-n", f"{WORKER_NAME}@%h"], + cwd=BASIS, stdout=log, stderr=subprocess.STDOUT, + creationflags=getattr(subprocess, "CREATE_NO_WINDOW", 0), + ) + if icon: + icon.icon = disc_bild(True) + icon.update_menu() + + +def worker_stoppen(icon=None, item=None): + global prozess + if worker_laeuft(): + prozess.terminate() + try: + prozess.wait(15) + except subprocess.TimeoutExpired: + prozess.kill() + prozess = None + if icon: + icon.icon = disc_bild(False) + icon.update_menu() + + +def umschalten(icon, item): + if worker_laeuft(): + worker_stoppen(icon) + else: + worker_starten(icon) + + +def rippy_oeffnen(icon, item): + if RIPPY_HOST: + webbrowser.open(f"http://{RIPPY_HOST}") + + +def log_anzeigen(icon, item): + if os.path.exists(LOG_PFAD): + os.startfile(LOG_PFAD) # noqa: S606 — bewusst: Standard-Editor des Nutzers + + +def beenden(icon, item): + worker_stoppen() + icon.stop() + + +menue = pystray.Menu( + pystray.MenuItem( + lambda item: f'Rippy-Worker "{WORKER_NAME}": {"läuft" if worker_laeuft() else "gestoppt"}', + None, enabled=False, + ), + pystray.Menu.SEPARATOR, + pystray.MenuItem( + lambda item: "Worker stoppen" if worker_laeuft() else "Worker starten", + umschalten, + ), + pystray.MenuItem("Rippy öffnen", rippy_oeffnen), + pystray.MenuItem("Log anzeigen", log_anzeigen), + pystray.Menu.SEPARATOR, + pystray.MenuItem("Beenden (stoppt den Worker)", beenden), +) + + +if __name__ == "__main__": + tray = pystray.Icon("rippy-worker", disc_bild(False), "Rippy Worker", menue) + worker_starten() + tray.icon = disc_bild(worker_laeuft()) + tray.run()