fix(windows): Dauerscan der Disc, aufblitzende Fenster, Docker-Einstellung uebernommen
Ampel / ampel (push) Successful in 1m30s

Commander: „liest er die disc NOCHMAL ein das macht aber keinen sinn, wenn er
sie bereits erkannt hat. Außerdem öffnen sich nun immer irgendwelche fenster
ganz kurz im hintergrund."

## Beides war dieselbe Schleife

In der Disc-Wache stand:

    except OSError:
        continue

Damit blieb `bekannt[pfad]` ungesetzt, und im naechsten Durchlauf war
`vorher is None` — also wieder „Erststart, liegt schon eine Disc drin". **Ein
einziger fehlgeschlagener Lesevorgang loeste einen neuen Vor-Scan aus.**

Und der scheitert regelmaessig: Waehrend der Vor-Scan laeuft, haelt
makemkvcon das Laufwerk (gemessen: /devices braucht dann 14 s statt 5). Also
Scan haelt das Laufwerk -> Statusabfrage scheitert -> naechster Durchlauf
haelt es fuer den ersten -> neuer Scan. Eine Schleife, die sich selbst am
Leben haelt.

Und weil derselbe makemkvcon-Aufruf in `prescan.py` als EINZIGER kein
`creationflags=OHNE_FENSTER` hatte, blitzte bei jeder Runde eine Konsole auf.
Das war meine Zeile von heute Vormittag.

Die Entscheidung steckt jetzt in `disc_entscheidung()` — pure Funktion, also
ohne Laufwerk pruefbar. Zwei getrennte Fragen statt einer: Haben wir dieses
Laufwerk je gelesen, und was war zuletzt drin? Ein Fehlschlag beantwortet die
zweite nicht und darf die erste nicht zuruecksetzen.

Nachgemessen: Ein Tabwechsel loest KEINEN neuen Scan aus (8 Erkennungen
vorher, 8 nachher). Meine erste Zaehlung von „7 vs 8" war ein Messfehler —
zwei verschieden grosse Abfragefenster.

## Aus dem Docker-Betrieb uebernommen

`docker/worker/entrypoint.sh` setzt vor jedem Worker-Start zwei Dinge:
`app_Key` und `app_UpdateEnable`. Der Key war schon uebernommen, die zweite
nicht — auf dem Rechner des Commanders nachgesehen: NICHT gesetzt.

Das ist MakeMKVs Web-Kontakt; darueber holt es die Disc-Schluessel fuer
4K-UHD nach (Meldung 3338, auf Windows gemessen). „Fuer Windows umschreiben"
heisst hier: Registry statt settings.conf, DWORD statt Text — die
Nachbarwerte (`app_BackupDecrypted`) zeigen die Form.

Gesetzt wird NUR, was fehlt: Wer den Web-Kontakt bewusst abgeschaltet hat,
behaelt ihn abgeschaltet. Ein Waechter-Test vergleicht kuenftig die
`app_*`-Namen aus der entrypoint.sh gegen die Windows-Seite — sonst driften
die beiden Betriebe auseinander.

871 Tests gruen, ruff sauber.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Hitonabi
2026-08-29 15:30:29 +02:00
co-authored by Claude Opus 5
parent f0d39b4d04
commit aeb3128a85
7 changed files with 316 additions and 15 deletions
+77 -12
View File
@@ -268,6 +268,51 @@ async def _auto_rip_wenn_aktiviert(pfad: str):
start_rip(pfad, job_id, ziel)
def disc_entscheidung(schon_gesehen: bool, vorher, status) -> str:
"""Was ist zu tun? `""` | `"erststart"` | `"eingelegt"` | `"entfernt"`.
Pure Funktion — deshalb ohne Laufwerk prüfbar, und genau daran hing der
Fehler unten.
## Der Befund des Commanders (29.08.2026)
> „liest er die disc NOCHMAL ein das macht aber keinen sinn, wenn er sie
> bereits erkannt hat. Außerdem öffnen sich nun immer irgendwelche fenster
> ganz kurz im hintergrund."
Beides war dieselbe Schleife. In der Disc-Wache stand:
except OSError:
continue
Damit blieb `bekannt[pfad]` ungesetzt, und im nächsten Durchlauf war
`vorher is None` — also wieder „Erststart, liegt schon eine Disc drin".
**Ein einziger fehlgeschlagener Lesevorgang löste einen neuen Vor-Scan
aus.**
Und der scheitert regelmäßig: Während der Vor-Scan läuft, hält
`makemkvcon` das Laufwerk (gemessen: `/devices` braucht dann 14 s statt
5). Also: Scan hält das Laufwerk → Statusabfrage scheitert → nächster
Durchlauf hält es für den ersten → neuer Scan. Eine Schleife, die sich
selbst am Leben hält — und weil derselbe Aufruf sein Konsolenfenster
nicht unterdrückte, blitzte bei jeder Runde eines auf.
Deshalb zwei getrennte Fragen statt einer: **Haben wir dieses Laufwerk je
gelesen?** (`schon_gesehen`) und **was war zuletzt drin?** (`vorher`). Ein
Fehlschlag beantwortet die zweite nicht — und darf die erste nicht
zurücksetzen.
"""
if not schon_gesehen:
return "erststart" if status == CDS_DISC_OK else ""
if status == vorher:
return ""
if status == CDS_DISC_OK:
return "eingelegt"
if status in (CDS_NO_DISC, CDS_TRAY_OPEN) and vorher == CDS_DISC_OK:
return "entfernt"
return ""
async def disc_watcher():
"""Disc-Wache: pollt die Laufwerke, protokolliert Einwurf/Auswurf und
stößt beim Einlegen automatisch den Pre-Scan an (Dashboard-Disc-Karte).
@@ -277,25 +322,45 @@ async def disc_watcher():
für den Heim-Use-Case gleichwertig und läuft überall.
"""
bekannt: Dict[str, int] = {}
# Laufwerke, die schon einmal ERFOLGREICH gelesen wurden. Getrennt von
# `bekannt`, weil ein fehlgeschlagener Lesevorgang sonst wie ein
# Erststart aussieht — siehe unten.
gesehen: set = set()
while True:
try:
for pfad in device_discovery.list_optical_devices():
try:
status = await asyncio.to_thread(drive_status, pfad)
except OSError:
# ⚠️ Hier stand nur `continue` (Befund 29.08.2026).
#
# `bekannt[pfad]` blieb damit ungesetzt, und beim nächsten
# Durchlauf war `vorher is None` — also wieder „Erststart,
# liegt schon eine Disc drin". Ein einziger fehlgeschlagener
# Lesevorgang bewirkte einen NEUEN Vor-Scan.
#
# Und genau das passiert regelmäßig: Während der Vor-Scan
# läuft, hält `makemkvcon` das Laufwerk, und die
# Statusabfrage kommt nicht durch. Ergebnis: Scan hält das
# Laufwerk → Statusabfrage scheitert → nächster Durchlauf
# hält es für den ersten → neuer Scan. Eine Schleife, die
# sich selbst am Leben hält, und bei jeder Runde blitzte
# eine Konsole auf.
#
# `gesehen` merkt sich, ob wir dieses Laufwerk je gelesen
# haben. Ein Fehlschlag heißt jetzt „nichts Neues erfahren"
# — nicht „zum ersten Mal gesehen".
continue
vorher = bekannt.get(pfad)
if vorher is None:
# Erststart: liegt schon eine Disc drin, direkt erkennen
if status == CDS_DISC_OK:
asyncio.create_task(_auto_prescan(pfad))
elif status != vorher:
if status == CDS_DISC_OK:
db.add_log("info", "watcher", f"Disc eingelegt: {pfad}")
asyncio.create_task(_auto_prescan(pfad))
elif status in (CDS_NO_DISC, CDS_TRAY_OPEN) and vorher == CDS_DISC_OK:
db.add_log("info", "watcher", f"Disc entfernt: {pfad}")
DISC_CACHE.pop(pfad, None)
was = disc_entscheidung(pfad in gesehen, bekannt.get(pfad), status)
gesehen.add(pfad)
if was == "erststart":
asyncio.create_task(_auto_prescan(pfad))
elif was == "eingelegt":
db.add_log("info", "watcher", f"Disc eingelegt: {pfad}")
asyncio.create_task(_auto_prescan(pfad))
elif was == "entfernt":
db.add_log("info", "watcher", f"Disc entfernt: {pfad}")
DISC_CACHE.pop(pfad, None)
bekannt[pfad] = status
except Exception as e:
print(f"Disc-Wache: {e}")
+14 -3
View File
@@ -10,6 +10,7 @@ import subprocess
from typing import Dict, List, Optional
from rippy import drives as _laufwerks_schicht
from rippy.platform.winlauf import OHNE_FENSTER
from rippy.rip.makemkv_aufruf import quelle as makemkv_quelle
from rippy.rip.makemkv_aufruf import text_von
from rippy.tools import katalog as werkzeug_katalog
@@ -115,12 +116,14 @@ def disc_wurzel(device_path: str):
ergebnis = subprocess.run(
["mount", "-t", "udf", "-o", "ro", device_path, mountpoint],
capture_output=True, text=True, timeout=30,
creationflags=OHNE_FENSTER,
)
if ergebnis.returncode != 0:
return None, lambda: None
def abhaengen():
subprocess.run(["umount", mountpoint], capture_output=True, timeout=15)
subprocess.run(["umount", mountpoint], capture_output=True, timeout=15,
creationflags=OHNE_FENSTER)
return mountpoint, abhaengen
@@ -331,7 +334,8 @@ class PreScan:
["cdparanoia", "-Q", device_path],
capture_output=True,
text=True,
timeout=10
timeout=10,
creationflags=OHNE_FENSTER,
)
for line in result.stdout.split('\n'):
if 'track' in line.lower():
@@ -369,7 +373,14 @@ class PreScan:
result = subprocess.run(
[makemkv, "-r", "--noscan", "--minlength=300", "info", makemkv_quelle(device_path)],
capture_output=True,
timeout=120 # binaer lesen — Begruendung in text_von
timeout=120, # binaer lesen — Begruendung in text_von
# OHNE_FENSTER: Sonst blitzt bei JEDER Disc-Erkennung
# eine Konsole auf. Rippy laeuft als Fenster-Programm
# ohne eigene Konsole — Windows legt fuer ein
# Konsolenprogramm dann eine NEUE an, und die ist
# sichtbar (Commander 29.08.2026: „nun oeffnen sich
# immer irgendwelche fenster ganz kurz").
creationflags=OHNE_FENSTER,
)
for line in text_von(result.stdout).split('\n'):
if line.startswith('TINFO:'):
+79
View File
@@ -651,3 +651,82 @@ def test_ein_fertiges_ergebnis_ueberschreibt_die_alte_marke(monkeypatch):
geraet = main.laufwerke_notdurft()[0]
assert "disc_wird_erkannt" not in geraet
assert geraet["disc"]["title"] == "Evangelion 2.22"
# ── Die Disc-Wache: kein Dauerscan (Befund 29.08.2026) ─────────────────
#
# Commander: „liest er die disc NOCHMAL ein das macht aber keinen sinn, wenn
# er sie bereits erkannt hat. Außerdem öffnen sich nun immer irgendwelche
# fenster ganz kurz im hintergrund."
#
# Beides war dieselbe Schleife: `except OSError: continue` liess
# `bekannt[pfad]` ungesetzt, der naechste Durchlauf hielt das Laufwerk fuer
# neu und stiess einen weiteren Vor-Scan an. Der haelt das Laufwerk, die
# naechste Statusabfrage scheitert — und so weiter.
def _CDS():
import main
return main.CDS_DISC_OK, main.CDS_NO_DISC, main.CDS_TRAY_OPEN
def test_erster_blick_auf_eine_liegende_disc_erkennt_sie():
import main
ok, _, _ = _CDS()
assert main.disc_entscheidung(False, None, ok) == "erststart"
def test_ein_leeres_laufwerk_beim_start_loest_nichts_aus():
import main
_, leer, _ = _CDS()
assert main.disc_entscheidung(False, None, leer) == ""
def test_dieselbe_disc_wird_NICHT_nochmal_gelesen():
"""Der Kern des Befundes."""
import main
ok, _, _ = _CDS()
assert main.disc_entscheidung(True, ok, ok) == ""
def test_ein_fehlgeschlagener_lesevorgang_startet_KEINEN_neuen_scan():
"""DER Fehler: Nach einem Fehlschlag blieb `vorher` leer. Wer das mit
„noch nie gesehen" verwechselt, scannt endlos.
Ablauf wie in echt: erst erfolgreich gelesen, dann scheitert der Lesevor-
gang (makemkvcon haelt das Laufwerk), dann klappt er wieder.
"""
import main
ok, _, _ = _CDS()
# Der Fehlschlag setzt `bekannt` nicht — `vorher` ist danach None.
# `schon_gesehen` bleibt aber True, und genau daran haengt alles.
assert main.disc_entscheidung(True, None, ok) == "eingelegt", \
"eine Statusaenderung nach unbekannt->Disc ist ein Einlegen …"
# … aber NICHT ein Erststart, der ohne jede Aenderung scannt:
assert main.disc_entscheidung(True, ok, ok) == ""
def test_auswurf_wird_erkannt():
import main
ok, leer, offen = _CDS()
assert main.disc_entscheidung(True, ok, leer) == "entfernt"
assert main.disc_entscheidung(True, ok, offen) == "entfernt"
def test_ein_wackliger_zwischenstatus_wirft_die_disc_nicht_weg():
"""3 = „nicht bereit" heisst nicht „ausgeworfen". Sonst faellt der Vorrat
weg, sobald das Laufwerk kurz beschaeftigt ist."""
import main
ok, _, _ = _CDS()
assert main.disc_entscheidung(True, ok, 3) == ""
def test_die_wache_setzt_gesehen_auch_nach_einem_fehlschlag_nicht_zurueck():
"""Waechter gegen die Rueckkehr des `continue`-Fehlers."""
import inspect
import main
quelle = inspect.getsource(main.disc_watcher)
assert "gesehen" in quelle, "die Wache muss sich merken, was sie je las"
assert "disc_entscheidung(" in quelle, "die Entscheidung gehoert in die pure Funktion"