fix(windows): Vollstaendiger Rundgang durch die Docker-Reste
Ampel / ampel (push) Successful in 1m20s

Commander: „Bro, du musst alles was rippy jetzt im code hat für Windows
Bauen! Jeden pfad, alles wo die tools drauf zugreifen. Diese Rippy version
MUSS 100% Windows Kompatibel sein. Prüfe bitte den kompletten Quellcode nach
Docker Resten."

Systematisch gesucht statt Fundstelle fuer Fundstelle: feste POSIX-Pfade,
Linux-Programme, POSIX-eigene Aufrufe, `shutil.which`, `posixpath` auf echten
Pfaden, Container-Texte. Sechs echte Fehler dabei.

## 1. `/dev/{name}` in drei Endpunkten — der schwerste

Das UI ruft `/devices/{id}/eject`, `/scan-tracks` und `/tracks` mit der
Kennung aus der Geraeteliste auf, unter Windows also `G`. Gebaut wurde daraus
`/dev/G` — steht in keiner Laufwerksliste. **Auswerfen und „Disc scannen"
antworteten unter Windows IMMER mit 404**, ohne dass irgendwo stand, warum.

Hin- und Rueckweg gehoeren zusammen: Beide Treiber haben jetzt `kennung()`
und `pfad_zu_kennung()`. Wer die Kennung vergibt, loest sie auch auf.

## 2. `os.path.isdir("/app")` — zum zweiten Mal

Nach `caps.py` (heute frueh) auch in `ablauf.py`: Der eigenstaendige
Windows-Rippy hielt sich fuer einen FREMDEN Worker und haette sich selbst
vorgeworfen, Container-Pfade nicht zu erreichen — auf einer Maschine ohne
Container. Die Entscheidung ist jetzt einspritzbar; vorher hing der Test
daran, ob es einen Ordner `/app` gibt.

## 3. `shutil.which` in `schluessel.py`

Ausgerechnet im Modul, das es NUR unter Windows gibt: Es suchte makemkvcon im
PATH, wo unter Windows nie ein Programm aus „Programme" steht. Die
Schluessel-Automatik fuer 4K-UHD lief damit nie an.

## 4. `posixpath.join` auf echten Pfaden

`rohdaten.py` baute `C:\Roh/datei.mkv` — gemischte Trenner, die im UI falsch
aussehen und jeden Vergleich brechen.

## 5. Container-Pfad in einer Nutzermeldung

„Roh-Datei bleibt in /app/temp erhalten" nennt jetzt den echten Ordner. Wer
die Datei retten will, sucht sonst am falschen Ort.

## 6. Container-Pfade als UI-Vorbelegung

Rip-Dialog und `useBetrieb` starteten mit `/app/media`, bis die Antwort da
war. Leer ist ehrlicher: Es behauptet nichts.

## Und HandBrakes „Code 0"

Code 0 heisst ERFOLG. Rippy meldete trotzdem „fehlgeschlagen", weil die Datei
nicht am erwarteten Ort lag: **HandBrake bestimmt den Container aus dem
PRESET, nicht aus der Endung** — ein MP4-Preset schreibt `.mp4` neben das
verlangte `.mkv`. Jetzt erzwingt `--format` den Container passend zur Endung
(an HandBrake 1.11.2 gegengeprueft), und falls doch etwas daneben liegt, wird
es gefunden statt weggeworfen.

## Der Waechter

`test_keine_container_reste.py` prueft mechanisch, dass im Windows-Weg kein
Container-Pfad ohne Begruendung steht. Die Ausnahmen stehen namentlich mit
Grund da (Linux-Zweige, benannte Rueckfaelle) — und ein zweiter Test wirft
jede Ausnahme raus, die niemand mehr braucht.

Ueber den Tokenizer, nicht ueber „faengt mit Anfuehrungszeichen an": Der
erste Anlauf blieb prompt an seinem eigenen `r\"\"\"`-Docstring haengen.

887 Tests gruen, ruff sauber.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Hitonabi
2026-08-29 16:14:10 +02:00
co-authored by Claude Opus 5
parent 1a529f4e75
commit 1b84ec2a45
11 changed files with 384 additions and 23 deletions
+19 -1
View File
@@ -229,6 +229,24 @@ def read_sys_attr(device_name: str, attr: str) -> str:
return ""
def kennung(device_path: str) -> str:
"""Der kurze Name eines Laufwerks — `/dev/sr0` wird zu `sr0`.
Gegenstueck zu `pfad_zu_kennung`. Begruendung im Windows-Treiber: Hin-
und Rueckweg muessen zusammenpassen, sonst antworten die Endpunkte mit
404 (Befund 29.08.2026).
"""
return os.path.basename(device_path)
def pfad_zu_kennung(name: str, geraete=None) -> str:
"""Kennung -> Geraetepfad, oder "" wenn es dieses Laufwerk nicht gibt."""
for pfad in (geraete if geraete is not None else list_optical_devices()):
if pfad == name or kennung(pfad) == name:
return pfad
return ""
def device_info(device_path: str) -> dict:
"""Baut den Geräte-Eintrag fürs UI: Name aus /sys, Disc-Status per ioctl.
@@ -247,7 +265,7 @@ def device_info(device_path: str) -> dict:
drive_status,
)
name = os.path.basename(device_path)
name = kennung(device_path)
vendor = read_sys_attr(name, "vendor")
model = read_sys_attr(name, "model")
+29 -1
View File
@@ -436,6 +436,34 @@ def geraeteangaben(geraet: str, api=None) -> dict:
}
def kennung(geraet: str) -> str:
r"""Der kurze Name eines Laufwerks — `\.\G:` wird zu `G`.
## Warum das eine benannte Funktion ist (Befund 29.08.2026)
In `main.py` stand an DREI Stellen der Rueckweg als feste Zeile:
device_path = f"/dev/{name}"
Das UI ruft diese Endpunkte mit der Kennung aus der Geraeteliste auf —
unter Windows also `G`. Gebaut wurde daraus `/dev/G`, und das steht in
keiner Laufwerksliste. Ergebnis: **Auswerfen und „Disc scannen" antworten
unter Windows immer mit 404**, ohne dass irgendwo stuende, warum.
Hin und zurueck muessen zusammenpassen. Deshalb gehoert beides zum
Treiber: Er vergibt die Kennung, und er loest sie wieder auf.
"""
return geraet.rstrip(":").rsplit("\\", 1)[-1].rstrip(":")
def pfad_zu_kennung(name: str, geraete=None) -> str:
"""Kennung -> Geraetepfad, oder "" wenn es dieses Laufwerk nicht gibt."""
for pfad in (geraete if geraete is not None else list_optical_devices()):
if pfad == name or kennung(pfad) == name:
return pfad
return ""
def device_info(geraet: str, api=None) -> dict:
"""Der Geräte-Eintrag fürs UI — gleiche Felder wie beim Linux-Treiber.
@@ -459,7 +487,7 @@ def device_info(geraet: str, api=None) -> dict:
except OSError:
status = "unknown"
buchstabe = geraet.rstrip(":").rsplit("\\", 1)[-1].rstrip(":")
buchstabe = kennung(geraet)
angaben = _angaben_gemerkt(geraet, api)
modell = " ".join(t for t in (angaben.get("hersteller"),
angaben.get("modell")) if t)
+179
View File
@@ -0,0 +1,179 @@
"""Der Wächter gegen Container-Annahmen im Windows-Betrieb.
## Warum es diesen Test gibt (Commander, 29.08.2026)
> „Bro, du musst alles was rippy jetzt im code hat für Windows Bauen! Jeden
> pfad, alles wo die tools drauf zugreifen. Diese Rippy version MUSS 100%
> Windows Kompatibel sein. Prüfe bitte den kompletten Quellcode nach Docker
> Resten."
Der Rundgang dazu hat an einem Tag gefunden:
/app/media als Pfadgrenze -> Auswahl blieb leer, Browser tot
/app/temp/raw als Arbeitsordner -> 436 MB landeten in F:\\app\\temp
/root/.MakeMKV fuer den Beta-Key -> Key wurde nie gelesen
/dev/{name} in drei Endpunkten -> Auswerfen und Disc-Scan gaben 404
os.path.isdir("/app") zweimal -> Rippy hielt sich fuer fremd
shutil.which fuer makemkvcon -> Schluessel-Automatik lief nie an
posixpath auf echten Pfaden -> C:\\Roh/datei.mkv
Jeder einzelne Fall sah gleich aus: kein Absturz, keine Meldung, nur etwas,
das still nicht tat, was es sollte. Genau deshalb steht hier ein MECHANISCHER
Waechter und keine Merkregel.
## Die Regel
In allem, was der Windows-Betrieb laedt, darf ein Container- oder Linux-Pfad
nur stehen, wenn er in einem Zweig steht, der auf Windows nicht laeuft. Diese
Zweige stehen unten namentlich in `ERLAUBT` — mit Begruendung. Wer eine neue
Fundstelle hinzufuegt, muss sie dort begruenden; das ist der eigentliche
Zweck.
"""
import io
import os
import re
import pytest
WURZEL = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
#: Was der Windows-Betrieb wirklich laedt.
GEPRUEFT = ("src/rippy", "docker/api", "docker/worker")
#: Module, die es unter Windows GAR NICHT gibt — dort sind Linux-Pfade richtig.
NUR_LINUX = (
"src/rippy/drives/linux.py", # der Linux-Treiber
"docker/api/mounts.py", # Freigaben einhaengen macht der Wirt
)
#: Begruendete Ausnahmen: Datei -> warum der Pfad dort stehen DARF.
ERLAUBT = {
"src/rippy/betrieb.py":
"Container-Zweige von ablage_vorgabe/arbeits_vorgabe/platz_orte — "
"sie laufen nur mit container=True",
"src/rippy/config.py":
"Linux-Zweig von standard_datenbankpfad (Windows: %LOCALAPPDATA%)",
"src/rippy/daemon.py":
"Linux-Zweig von standard_konfigpfad (Windows: %PROGRAMDATA%)",
"src/rippy/rip/makemkv_daten.py":
"Linux-Zweig von daten_verzeichnis (Windows: %USERPROFILE%\\.MakeMKV)",
"docker/api/config.py":
"Pydantic-Vorgaben ohne Leser — kein Aufrufer im Quellcode",
"docker/api/main.py":
"MEDIA_ROOT als benannter Rueckfall; die Wurzel liefert medien_wurzel()",
"docker/api/rohdaten.py":
"RAW_STANDARD/MEDIA_ROOT als Rueckfall; die Wurzeln liefert wurzeln()",
"docker/api/prescan/prescan.py":
"Linux-Zweig von disc_wurzel (Windows haengt die Disc selbst ein) "
"und die Audio-CD-Werkzeuge, die es unter Windows nicht gibt",
"docker/worker/ablauf.py":
"RAW_DIR/MEDIA_ROOT als Rueckfall; die Wurzeln liefert _betriebs_wurzeln()",
"docker/worker/ripping.py":
"RIP_OUTPUT_DIR als Rueckfall und die Audio-CD-Werkzeuge (Linux-only)",
}
PFAD_MUSTER = re.compile(r'["\'](/(?:app|root|dev|mnt|srv|etc|var|usr)(?:/[^"\']*)?)["\']')
def _dateien():
for teil in GEPRUEFT:
for ordner, unter, namen in os.walk(os.path.join(WURZEL, teil)):
unter[:] = [u for u in unter if u != "__pycache__"]
for name in namen:
if not name.endswith(".py") or name.startswith("test_"):
continue
voll = os.path.join(ordner, name)
rel = os.path.relpath(voll, WURZEL).replace("\\", "/")
if rel in NUR_LINUX:
continue
yield rel, voll
def _code_zeilen(voll: str):
"""Zeilen OHNE Kommentare und Docstrings — nur was wirklich laeuft.
Ueber den TOKENIZER, nicht ueber „faengt mit Anfuehrungszeichen an": Ein
Docstring kann `r\"\"\"` oder `f\"\"\"` heissen, und der erste Anlauf dieses
Tests ist prompt an seinem eigenen `r\"\"\"`-Docstring haengengeblieben.
Python weiss selbst am besten, was Code ist und was Text.
"""
import tokenize
with tokenize.open(voll) as f:
try:
marken = list(tokenize.generate_tokens(f.readline))
except (tokenize.TokenError, IndentationError, SyntaxError):
return
# Zeilen, in denen echter Code steht (alles ausser Kommentar, Text,
# Einrueckung und Zeilenende).
egal = {tokenize.COMMENT, tokenize.STRING, tokenize.NL, tokenize.NEWLINE,
tokenize.INDENT, tokenize.DEDENT, tokenize.ENDMARKER}
zeilen = {}
for marke in marken:
if marke.type in egal:
continue
zeilen[marke.start[0]] = marke.line.rstrip("\n")
# Zeichenketten IM Code (kein Docstring) muessen mit — dort stehen die
# Pfade. Erkennbar daran, dass in derselben Zeile auch Code steht.
for marke in marken:
if marke.type == tokenize.STRING and marke.start[0] in zeilen:
zeilen[marke.start[0]] = marke.line.rstrip("\n")
for nr in sorted(zeilen):
yield nr, zeilen[nr]
def test_kein_unbegruendeter_container_pfad():
"""DER Waechter. Neue Fundstelle -> hier begruenden oder beheben."""
offen = []
for rel, voll in _dateien():
for nr, zeile in _code_zeilen(voll):
treffer = PFAD_MUSTER.search(zeile)
if treffer and rel not in ERLAUBT:
offen.append("%s:%d %s" % (rel, nr, treffer.group(1)))
assert not offen, (
"Container-Pfad im Windows-Betrieb ohne Begruendung:\n "
+ "\n ".join(offen)
+ "\n\nEntweder fuer Windows umschreiben (betrieb.medien_wurzel, "
"rippy.pfade) oder in ERLAUBT begruenden.")
def test_die_ausnahmen_sind_noch_noetig():
"""Eine Ausnahme, die niemand mehr braucht, ist eine Unwahrheit im Test."""
ungenutzt = []
for rel in ERLAUBT:
voll = os.path.join(WURZEL, rel)
if not os.path.isfile(voll):
ungenutzt.append(rel + " (Datei gibt es nicht mehr)")
continue
if not any(PFAD_MUSTER.search(z) for _, z in _code_zeilen(voll)):
ungenutzt.append(rel + " (kein Container-Pfad mehr drin)")
assert not ungenutzt, "Ausnahme ueberfluessig:\n " + "\n ".join(ungenutzt)
@pytest.mark.parametrize("modul,name", [
("src/rippy/drives/windows.py", "kennung"),
("src/rippy/drives/windows.py", "pfad_zu_kennung"),
("src/rippy/drives/linux.py", "kennung"),
("src/rippy/drives/linux.py", "pfad_zu_kennung"),
])
def test_beide_treiber_koennen_die_kennung_in_beide_richtungen(modul, name):
"""Hin und zurueck muessen zusammenpassen.
`main.py` baute den Geraetepfad aus der Kennung selbst zusammen
(`/dev/{name}`) — unter Windows heisst das Laufwerk aber `\\\\.\\G:`.
Auswerfen und „Disc scannen" antworteten deshalb IMMER mit 404.
"""
text = io.open(os.path.join(WURZEL, modul), encoding="utf-8",
errors="replace").read()
assert "def %s(" % name in text
def test_niemand_baut_den_geraetepfad_mehr_selbst():
"""Der konkrete Fehler: `f"/dev/{name}"` in drei Endpunkten."""
voll = os.path.join(WURZEL, "docker/api/main.py")
gebaut = [nr for nr, z in _code_zeilen(voll) if 'f"/dev/{' in z]
assert not gebaut, (
"main.py:%s baut den Geraetepfad selbst — device_discovery."
"pfad_zu_kennung() benutzen" % gebaut)