From 6438beec057e9662a91949430d91a3cea8d76977 Mon Sep 17 00:00:00 2001 From: Hitonabi Date: Sun, 26 Jul 2026 16:05:54 +0200 Subject: [PATCH] fix(worker): kein CMD-Blitzen mehr - und die 7200-Sekunden-Meldung ist erklaert Zwei Meldungen, beide auf dieselbe Wurzel zurueckgefuehrt: Der Worker startet Konsolenprogramme, und das hat unter Windows Nebenwirkungen. 1. "Es geht immer alle paar Sekunden ne CMD auf." Kein Geist, sondern der HERZSCHLAG. Der Worker laeuft als pythonw.exe, also ohne eigene Konsole. Wer ohne Konsole ein Konsolenprogramm startet, bekommt von Windows eine NEUE - und die ist sichtbar. `capture_output=True` hilft nicht: es leitet die Datenstroeme um, unterdrueckt aber kein Fenster. Und caps.py fragt jede Minute `HandBrakeCLI --help`, `--preset-list` und `--version` ab, dazu makemkvcon aus der Schluessel-Automatik: vier bis fuenf Fenster pro Minute, in Schueben. Neues winlauf.py haelt CREATE_NO_WINDOW an EINER Stelle; alle elf Aufrufe in caps.py, ripping.py und schluessel.py benutzen es. Auf Linux ist der Wert 0 und creationflags=0 eine Nulloperation - im Worker-Container gegengeprueft, damit derselbe Code auf beiden Seiten laeuft. 2. Die 7200 Sekunden sind CELERYS EIGENE UMRECHNUNG, nicht die Uhren. Erst fiel auf, dass die Sekundenbruchteile nur 40 ms auseinanderliegen (.177269 vs .134799) - derselbe Augenblick, zweimal verschieden gerechnet. Die Quelle sagt warum (celery/utils/time.py): def utcoffset(): # Sekunden WEST von UTC, in Stunden return time.altzone // 3600 # CEST -> -2 ; UTC -> 0 def adjust_timestamp(ts, offset, here=utcoffset): return ts - (offset - here()) * 3600 Absender VM (offset 0), Empfaenger Windows-PC (here() = -2): ts - (0 - (-2)) * 3600 = ts - 7200. Celery verschiebt den empfangenen Stempel also selbst und vergleicht ihn dann mit der eigenen Uhr. Die Annahme dahinter - Ereignisse truegen Ortszeit - stimmt nicht, `Event()` stempelt Epoch. Abhilfe gemessen, nicht geraten: Mit TZ=UTC meldet Windows utcoffset 0 (timezone=0, isdst=0 - auf dem Commander-PC geprueft), damit wird die Umrechnung zur Nulloperation. Beide .bat-Dateien setzen es jetzt. Nebeneffekt ist erwuenscht: Der Worker rechnet dann in derselben Zone wie die VM und wie Rippys UI. Fuer Rippy war die Meldung ohnehin folgenlos - /capabilities fragt per Celery-Ping (Frage/Antwort, ohne Zeitstempel), nicht ueber die Gossip-Ereignisse. Aber eine Warnung, die bei jedem Blick ins Log steht und nichts bedeutet, kostet Vertrauen. Co-Authored-By: Claude Opus 5 --- deploy/worker-windows/RippyWorkerSetup.exe | Bin 75776 -> 75776 bytes deploy/worker-windows/install-gui.ps1 | 4 +-- deploy/worker-windows/install.ps1 | 2 ++ docker/worker/caps.py | 7 ++++- docker/worker/ripping.py | 14 +++++++-- docker/worker/schluessel.py | 5 +++- docker/worker/winlauf.py | 32 +++++++++++++++++++++ 7 files changed, 57 insertions(+), 7 deletions(-) create mode 100644 docker/worker/winlauf.py diff --git a/deploy/worker-windows/RippyWorkerSetup.exe b/deploy/worker-windows/RippyWorkerSetup.exe index 3d1d74cd489a1d4a65db21b23c2100835ddb6ca9..4b0706eb3dd054ec1d36b977afd0b2eb9bf32a78 100644 GIT binary patch delta 193 zcmZp;z|wGmWkLs&lgPv_IYzsQE5rFds4+5lGXQ}DBf|^niHgFL6&d3hT_!g&)(a}A zGlFCoJb;*qftBILWI-ljrsaLp9XT1bHU}_0&}Lk`*-)<~lB+niL?I-~HZ;U}^TzgB zj7a>+8{0KD=Xd@r6WGYGl6l%mXRo=E3(c-w?3=f>f-#1JR|0AW$R54v4>=g6nSs_$ b=jLSmyq%AW@s~7{gZXwrHOBXh6Ajn_y5%<| delta 178 zcmZp;z|wGmWkLtjHo=Kqa*TQtSBCRlQDbBXX8-~RMur#C6BUIgD>BA2noMqFtQQne zX9USGcmOdI11rOx$%0J6Ox=Cc134MBHU}_0&}MAkY^c`~x%qJWG{(t?+f_Dqcm6CB z2<|z_#$DCwGto#K0BwQ+x#@p77^Rtk765IWuEfdsX0ijLz;-?^ U#$VD*2IkuZ)fnG1PBdT#0GHn}z5oCK 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)