diff --git a/deploy/worker-windows/RippyWorkerSetup.exe b/deploy/worker-windows/RippyWorkerSetup.exe index 3d1d74c..4b0706e 100644 Binary files a/deploy/worker-windows/RippyWorkerSetup.exe and b/deploy/worker-windows/RippyWorkerSetup.exe differ diff --git a/deploy/worker-windows/install-gui.ps1 b/deploy/worker-windows/install-gui.ps1 index c8ab523..c842571 100644 --- a/deploy/worker-windows/install-gui.ps1 +++ b/deploy/worker-windows/install-gui.ps1 @@ -482,10 +482,10 @@ function Do-Install { $poolArg = if ($slots -le 1) { "--pool=solo" } else { "--pool=threads --concurrency=$slots" } Log "Gleichzeitige Auftraege: $slots" - $trayBat = "@echo off`r`ncd /d `"%~dp0`"`r`nset REDIS_URL=redis://${rHost}:6379/0`r`nset DATABASE_URL=postgresql://rippy:rippy@${rHost}:5432/rippy`r`nset API_URL=http://${rHost}:8000`r`nset WORKER_NAME=$wName`r`nset RIPPY_TRAY_HOST=$rHost`r`nset RIPPY_SLOTS=$slots`r`n${mapZeile}set PATH=%~dp0;%PATH%`r`nstart `"`" venv\Scripts\pythonw.exe tray.py" + $trayBat = "@echo off`r`ncd /d `"%~dp0`"`r`nset REDIS_URL=redis://${rHost}:6379/0`r`nset DATABASE_URL=postgresql://rippy:rippy@${rHost}:5432/rippy`r`nset API_URL=http://${rHost}:8000`r`nset WORKER_NAME=$wName`r`nset RIPPY_TRAY_HOST=$rHost`r`nset RIPPY_SLOTS=$slots`r`nset TZ=UTC`r`n${mapZeile}set PATH=%~dp0;%PATH%`r`nstart `"`" venv\Scripts\pythonw.exe tray.py" Set-Content -Path "start-tray.bat" -Value $trayBat -Encoding ASCII - $workBat = "@echo off`r`ncd /d `"%~dp0`"`r`nset REDIS_URL=redis://${rHost}:6379/0`r`nset DATABASE_URL=postgresql://rippy:rippy@${rHost}:5432/rippy`r`nset API_URL=http://${rHost}:8000`r`nset WORKER_NAME=$wName`r`nset RIPPY_SLOTS=$slots`r`n${mapZeile}set PATH=%~dp0;%PATH%`r`nvenv\Scripts\celery.exe -A celery_app worker --loglevel=info -Q transcode $poolArg -n ${wName}@%%h" + $workBat = "@echo off`r`ncd /d `"%~dp0`"`r`nset REDIS_URL=redis://${rHost}:6379/0`r`nset DATABASE_URL=postgresql://rippy:rippy@${rHost}:5432/rippy`r`nset API_URL=http://${rHost}:8000`r`nset WORKER_NAME=$wName`r`nset RIPPY_SLOTS=$slots`r`nset TZ=UTC`r`n${mapZeile}set PATH=%~dp0;%PATH%`r`nvenv\Scripts\celery.exe -A celery_app worker --loglevel=info -Q transcode $poolArg -n ${wName}@%%h" Set-Content -Path "start-worker.bat" -Value $workBat -Encoding ASCII # --- Deinstaller --------------------------------------------------- diff --git a/deploy/worker-windows/install.ps1 b/deploy/worker-windows/install.ps1 index c5ffa31..db39972 100644 --- a/deploy/worker-windows/install.ps1 +++ b/deploy/worker-windows/install.ps1 @@ -184,6 +184,7 @@ set DATABASE_URL=postgresql://rippy:rippy@${RippyHost}:5432/rippy set API_URL=http://${RippyHost}:8000 set WORKER_NAME=$WorkerName set RIPPY_SLOTS=$Slots +set TZ=UTC ${mapZeile}set PATH=%~dp0;%PATH% venv\Scripts\celery.exe -A celery_app worker --loglevel=info -Q transcode $poolArg -n ${WorkerName}@%%h "@ @@ -200,6 +201,7 @@ set API_URL=http://${RippyHost}:8000 set WORKER_NAME=$WorkerName set RIPPY_TRAY_HOST=$RippyHost set RIPPY_SLOTS=$Slots +set TZ=UTC ${mapZeile}set PATH=%~dp0;%PATH% start "" venv\Scripts\pythonw.exe tray.py "@ diff --git a/docker/worker/caps.py b/docker/worker/caps.py index 9a50095..a4d8e4e 100644 --- a/docker/worker/caps.py +++ b/docker/worker/caps.py @@ -11,6 +11,8 @@ import re import shutil import subprocess +from winlauf import OHNE_FENSTER + HB_ENCODER_KOPF = re.compile(r"^-e,\s*--encoder\b") @@ -152,6 +154,7 @@ def hole_handbrake_presets() -> list: aus = subprocess.run( ["HandBrakeCLI", "--preset-list"], capture_output=True, text=True, timeout=30, + creationflags=OHNE_FENSTER, ) return parse_preset_liste((aus.stdout or "") + (aus.stderr or "")) except (OSError, subprocess.TimeoutExpired): @@ -263,7 +266,8 @@ def hole_handbrake_hilfe() -> str: return "" try: aus = subprocess.run( - ["HandBrakeCLI", "--help"], capture_output=True, text=True, timeout=30 + ["HandBrakeCLI", "--help"], capture_output=True, text=True, timeout=30, + creationflags=OHNE_FENSTER ) return (aus.stdout or "") + (aus.stderr or "") except (OSError, subprocess.TimeoutExpired): @@ -352,6 +356,7 @@ def werkzeug_versionen() -> dict: aus = subprocess.run( ["HandBrakeCLI", "--version"], capture_output=True, text=True, timeout=15, + creationflags=OHNE_FENSTER, ) treffer = re.search(r"HandBrake\s+([\w.]+)", (aus.stdout or "") + (aus.stderr or "")) info["handbrake"] = treffer.group(1) if treffer else "installiert" diff --git a/docker/worker/ripping.py b/docker/worker/ripping.py index 2525449..f5c2372 100644 --- a/docker/worker/ripping.py +++ b/docker/worker/ripping.py @@ -17,6 +17,8 @@ import shutil import subprocess import tempfile +from winlauf import OHNE_FENSTER + RIP_OUTPUT_DIR = os.getenv("RIP_OUTPUT_DIR", "/app/media") @@ -318,6 +320,7 @@ def lies_titel_info(device_path: str, timeout: int = 300) -> tuple: ergebnis = subprocess.run( ["makemkvcon", "-r", "--noscan", "info", f"dev:{device_path}"], capture_output=True, text=True, timeout=timeout, + creationflags=OHNE_FENSTER, ) ausgabe = ergebnis.stdout or "" return parse_titel_info(ausgabe), parse_stream_info(ausgabe) @@ -358,6 +361,7 @@ def lies_titel_dauern(device_path: str, timeout: int = 300) -> dict: ergebnis = subprocess.run( ["makemkvcon", "-r", "--noscan", "info", f"dev:{device_path}"], capture_output=True, text=True, timeout=timeout, + creationflags=OHNE_FENSTER, ) except (OSError, subprocess.TimeoutExpired): return {} @@ -386,6 +390,7 @@ def lies_datei_dauer(pfad: str, timeout: int = 120) -> int: ergebnis = subprocess.run( ["HandBrakeCLI", "--scan", "-i", pfad], capture_output=True, text=True, timeout=timeout, + creationflags=OHNE_FENSTER, ) except (OSError, subprocess.TimeoutExpired): return 0 @@ -624,7 +629,8 @@ def run_handbrake(input_path: str, output_path: str, preset: str = DEFAULT_HB_PR stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True, - bufsize=1 + bufsize=1, + creationflags=OHNE_FENSTER, ) return _handbrake_schleife(process, output_path, abbruch_cb, progress_cb) except Exception as e: @@ -753,7 +759,8 @@ def run_makemkv(device_path: str, output_dir: str, progress_cb=None, titel: str stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True, - bufsize=1 + bufsize=1, + creationflags=OHNE_FENSTER, ) letzte_meldung = "" @@ -883,7 +890,8 @@ def rip_cd(device_path: str, disc_id: str, progress_cb=None, output_dir: str = N stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True, - bufsize=1 + bufsize=1, + creationflags=OHNE_FENSTER, ) try: diff --git a/docker/worker/schluessel.py b/docker/worker/schluessel.py index 8fde149..792624a 100644 --- a/docker/worker/schluessel.py +++ b/docker/worker/schluessel.py @@ -45,6 +45,8 @@ import re import subprocess import urllib.request +from winlauf import OHNE_FENSTER + # `DRV:,,,,"","",""` DRV_ZEILE = re.compile(r'^DRV:(\d+),(\d+),(\d+),(\d+),"([^"]*)","([^"]*)","([^"]*)"') @@ -163,7 +165,7 @@ def laufwerke_lesen(programm: str, laufen=None, timeout: int = 120) -> list: ergebnis = starten( [programm, "-r", "--cache=1", "info", "disc:9999"], capture_output=True, text=True, timeout=timeout, - errors="replace", + errors="replace", creationflags=OHNE_FENSTER, ) except (OSError, subprocess.TimeoutExpired): return [] @@ -184,6 +186,7 @@ def disc_oeffnen(programm: str, index: int, laufen=None, timeout: int = 600) -> starten( [programm, "-r", "--noscan", "info", f"disc:{index}"], capture_output=True, text=True, timeout=timeout, errors="replace", + creationflags=OHNE_FENSTER, ) return True except (OSError, subprocess.TimeoutExpired): diff --git a/docker/worker/winlauf.py b/docker/worker/winlauf.py new file mode 100644 index 0000000..762e273 --- /dev/null +++ b/docker/worker/winlauf.py @@ -0,0 +1,32 @@ +"""Kind-Prozesse starten, ohne dass ein Konsolenfenster aufblitzt. + +## Der Befund, der das nötig gemacht hat (26.07.2026) + +Commander: *„Es geht übrigens immer alle paar Sekunden ne CMD auf."* Und er hatte +recht — es war kein Geist, sondern der Herzschlag des Workers. + +Der Windows-Worker läuft als `pythonw.exe`, also ohne eigene Konsole (das Tray +startet ihn mit CREATE_NO_WINDOW). Startet ein Prozess OHNE Konsole ein +Konsolenprogramm, legt Windows dafür eine NEUE Konsole an — und die ist sichtbar. +`capture_output=True` hilft nicht: Es leitet die Datenströme um, unterdrückt aber +kein Fenster. + +Und der Worker startet solche Programme oft: `caps.py` fragt jede Minute +`HandBrakeCLI --help`, `--preset-list` und `--version` ab, dazu kommt die +Schlüssel-Automatik mit `makemkvcon`. Vier bis fünf Fenster pro Minute, in +Schüben — genau das „alle paar Sekunden". + +## Warum eine eigene Datei + +Damit es an EINER Stelle richtig ist. Die betroffenen Aufrufe stehen in caps.py, +ripping.py und schluessel.py; jeder hätte das Flag einzeln vergessen können, und +genau so ist es passiert. Auf Linux ist `CREATE_NO_WINDOW` nicht vorhanden und +`creationflags=0` eine Nulloperation — im Worker-Container gegengeprüft, damit +derselbe Code auf beiden Seiten läuft. +""" + +import subprocess + +# Auf Windows das Flag, auf Linux 0 (dort wird creationflags=0 akzeptiert und +# ignoriert — am 26.07.2026 im Worker-Container gemessen). +OHNE_FENSTER = getattr(subprocess, "CREATE_NO_WINDOW", 0)