fix(windows): Vollstaendiger Rundgang durch die Docker-Reste
Ampel / ampel (push) Successful in 1m20s
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:
co-authored by
Claude Opus 5
parent
1a529f4e75
commit
1b84ec2a45
@@ -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")
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
Reference in New Issue
Block a user