fix(auswurf): CDROMEJECT meldete Erfolg und tat nichts - erst entriegeln
Ampel / ampel (push) Successful in 29s
Ampel / ampel (push) Successful in 29s
Commander-Meldung: "Den Button gibt es in den Settings, aber es passiert nicht,
das Laufwerk geht nicht auf." Am laufenden System nachgestellt, mit der Disc, die
gerade drin lag:
wirf_disc_aus("/dev/sr0") -> True
CDROM_DRIVE_STATUS danach -> 4 (Disc drin)
Das ioctl wird also ANGENOMMEN und tut nichts. Ursache: MakeMKV verriegelt
waehrend des Rips die Laufwerkstuer (CDROM_LOCKDOOR 1) und entriegelt sie nicht
wieder. Ein verriegeltes Laufwerk quittiert den Auswurf trotzdem mit Erfolg.
Gegenprobe an derselben Disc:
CDROM_LOCKDOOR 0 + CDROMEJECT -> Status 2 (SCHUBLADE OFFEN)
Genau das macht das Werkzeug `eject` immer: erst entriegeln, dann auswerfen.
Zweite Haelfte des Fixes, und die wichtigere: Das Ergebnis wird GEPRUEFT statt
geglaubt. Bisher gab wirf_disc_aus True zurueck, sobald das ioctl nicht geworfen
hatte - und ins Log kam "Disc ausgeworfen", waehrend die Schublade zu blieb.
Deshalb ist der Fehler in v3.14 durchgerutscht: Dort wurde richtig festgestellt,
dass die Einstellung von niemandem gelesen wurde, und danach WURDE sie gelesen -
ausgeworfen wurde weiterhin nicht. Jetzt wird das Laufwerk gefragt (bis zu 5 s,
die Schublade braucht ein bis zwei), und "kein Datentraeger" zaehlt mit, weil ein
Slot-Laufwerk keine Schublade hat.
Dieselbe Luecke steckte im Auswurf-Knopf der API (devices.eject) - dort mit
Klartext-Fehler, wenn die Disc drin bleibt.
Der Auswurf sitzt uebrigens schon an der richtigen Stelle: nach dem Rip, VOR dem
Einreihen der Kompression. Und das Laufwerk ist waehrend `transcoding` frei -
has_active_job blockiert nur bei pending/running, der Worker laeuft mit 4 Slots.
Die zweite Disc parallel war also nur am nicht aufgehenden Laufwerk gescheitert.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
+35
-1
@@ -20,13 +20,47 @@ from detection import (
|
|||||||
|
|
||||||
# include/uapi/linux/cdrom.h
|
# include/uapi/linux/cdrom.h
|
||||||
CDROMEJECT = 0x5309
|
CDROMEJECT = 0x5309
|
||||||
|
CDROM_LOCKDOOR = 0x5329 # 1 = Tür verriegeln, 0 = entriegeln
|
||||||
|
CDROM_DRIVE_STATUS = 0x5326
|
||||||
|
CDS_NO_DISC = 1
|
||||||
|
CDS_TRAY_OPEN = 2
|
||||||
|
|
||||||
|
AUSWURF_WARTEN_SEKUNDEN = 5
|
||||||
|
|
||||||
|
|
||||||
def eject(device_path: str) -> None:
|
def eject(device_path: str) -> None:
|
||||||
"""Wirft die Disc aus (CDROMEJECT-ioctl). Wirft OSError bei Fehlern."""
|
"""Wirft die Disc aus und prüft es nach. Wirft OSError, wenn sie drin bleibt.
|
||||||
|
|
||||||
|
⚠️ ERST ENTRIEGELN (Befund 26.07.2026, am laufenden System gemessen): Ein
|
||||||
|
nacktes CDROMEJECT wird von einem verriegelten Laufwerk mit ERFOLG quittiert
|
||||||
|
und tut nichts. MakeMKV verriegelt die Tür während des Rips
|
||||||
|
(`CDROM_LOCKDOOR 1`) und entriegelt sie nicht wieder — danach blieb die
|
||||||
|
Schublade zu, während Rippy „Disc ausgeworfen" ins Log schrieb. Gegenprobe
|
||||||
|
an derselben Disc: mit `CDROM_LOCKDOOR 0` davor geht sie auf (Status 2).
|
||||||
|
Deshalb macht das Werkzeug `eject` immer beides.
|
||||||
|
|
||||||
|
Gleichlautend in worker/ripping.wirf_disc_aus — es gibt kein geteiltes Paket
|
||||||
|
zwischen den Containern; dort steht die ausführliche Herleitung.
|
||||||
|
"""
|
||||||
|
import time
|
||||||
|
|
||||||
fd = os.open(device_path, os.O_RDONLY | os.O_NONBLOCK)
|
fd = os.open(device_path, os.O_RDONLY | os.O_NONBLOCK)
|
||||||
try:
|
try:
|
||||||
|
try:
|
||||||
|
ioctl(fd, CDROM_LOCKDOOR, 0)
|
||||||
|
except OSError:
|
||||||
|
pass # nicht verriegelt oder ioctl unbekannt — Auswurf trotzdem versuchen
|
||||||
ioctl(fd, CDROMEJECT, 0)
|
ioctl(fd, CDROMEJECT, 0)
|
||||||
|
# Nachsehen statt hoffen. „Kein Datenträger" zählt mit: ein
|
||||||
|
# Slot-Laufwerk hat keine Schublade.
|
||||||
|
for _ in range(AUSWURF_WARTEN_SEKUNDEN):
|
||||||
|
if ioctl(fd, CDROM_DRIVE_STATUS, 0) in (CDS_TRAY_OPEN, CDS_NO_DISC):
|
||||||
|
return
|
||||||
|
time.sleep(1)
|
||||||
|
raise OSError(
|
||||||
|
"Das Laufwerk hat den Auswurf angenommen, die Disc ist aber noch "
|
||||||
|
"drin. Blockiert etwas die Schublade, oder läuft noch ein Zugriff?"
|
||||||
|
)
|
||||||
finally:
|
finally:
|
||||||
os.close(fd)
|
os.close(fd)
|
||||||
|
|
||||||
|
|||||||
+86
-11
@@ -25,34 +25,109 @@ class RipAbbruch(Exception):
|
|||||||
Nutzer den Job abgebrochen hat (Status 'canceling' in der DB)."""
|
Nutzer den Job abgebrochen hat (Status 'canceling' in der DB)."""
|
||||||
|
|
||||||
|
|
||||||
# include/uapi/linux/cdrom.h — dasselbe ioctl wie in api/devices.py
|
# include/uapi/linux/cdrom.h — dieselben ioctls wie in api/devices.py
|
||||||
CDROMEJECT = 0x5309
|
CDROMEJECT = 0x5309
|
||||||
|
CDROM_LOCKDOOR = 0x5329 # 1 = Tür verriegeln, 0 = entriegeln
|
||||||
|
CDROM_DRIVE_STATUS = 0x5326
|
||||||
|
CDROMCLOSETRAY = 0x5319
|
||||||
|
|
||||||
|
# Antworten von CDROM_DRIVE_STATUS (cdrom.h)
|
||||||
|
CDS_NO_DISC = 1
|
||||||
|
CDS_TRAY_OPEN = 2
|
||||||
|
CDS_DRIVE_NOT_READY = 3
|
||||||
|
CDS_DISC_OK = 4
|
||||||
|
|
||||||
|
# Wie lange auf die Schublade gewartet wird. Ein Laufwerk braucht dafür ein
|
||||||
|
# bis zwei Sekunden; fünf sind reichlich und blockieren nichts Wichtiges.
|
||||||
|
AUSWURF_WARTEN_SEKUNDEN = 5
|
||||||
|
|
||||||
|
|
||||||
def wirf_disc_aus(device_path: str) -> bool:
|
def _auswurf_geglueckt(status: int) -> bool:
|
||||||
"""Wirft die Disc aus; gibt zurück, ob es geklappt hat. Wirft NIE.
|
"""Ist die Disc nach dem Auswurf wirklich draußen? (pure Funktion)
|
||||||
|
|
||||||
Befund 25.07.2026: Die Einstellung „Automatischer Auswurf" (Standard: ein,
|
Sowohl „Schublade offen" als auch „kein Datenträger" zählen: Ein
|
||||||
Beschreibung „Disc nach erfolgreichem Ripping automatisch auswerfen") wurde
|
Slot-Laufwerk hat keine Schublade und meldet nach dem Auswerfen CDS_NO_DISC.
|
||||||
von niemandem gelesen — bei DVD/Blu-ray warf Rippy nie aus.
|
"""
|
||||||
|
return status in (CDS_TRAY_OPEN, CDS_NO_DISC)
|
||||||
|
|
||||||
|
|
||||||
|
def wirf_disc_aus(device_path: str, ioctl_fn=None, oeffnen=None,
|
||||||
|
schliessen=None, warten=None) -> bool:
|
||||||
|
"""Wirft die Disc aus und PRÜFT, ob sie draußen ist. Wirft NIE.
|
||||||
|
|
||||||
|
## Warum `CDROMEJECT` allein nicht genügt (Befund 26.07.2026, gemessen)
|
||||||
|
|
||||||
|
Der Commander meldete: „Der Button gibt es in den Settings, aber es passiert
|
||||||
|
nicht, das Laufwerk geht nicht auf." Am laufenden System nachgestellt:
|
||||||
|
|
||||||
|
wirf_disc_aus("/dev/sr0") → True
|
||||||
|
CDROM_DRIVE_STATUS danach → 4 (Disc drin)
|
||||||
|
|
||||||
|
Das ioctl wird also **angenommen und tut nichts**. Ursache: MakeMKV
|
||||||
|
verriegelt während des Rips die Laufwerkstür (`CDROM_LOCKDOOR 1`) und
|
||||||
|
entriegelt sie nicht wieder. Ein verriegeltes Laufwerk quittiert den Auswurf
|
||||||
|
trotzdem mit Erfolg. Deshalb macht das Werkzeug `eject` immer beides:
|
||||||
|
erst entriegeln, dann auswerfen. Gegenprobe an derselben Disc:
|
||||||
|
|
||||||
|
CDROM_LOCKDOOR 0 + CDROMEJECT → Status 2 (SCHUBLADE OFFEN)
|
||||||
|
|
||||||
|
## Und deshalb wird das Ergebnis geprüft, nicht geglaubt
|
||||||
|
|
||||||
|
Genau diese Sorte Fehler ist zweimal durchgerutscht: In v3.14 stand hier
|
||||||
|
„Auswurf tat nichts, jetzt entscheidet die Einstellung" — die Einstellung
|
||||||
|
wurde danach wirklich gelesen, nur ausgeworfen wurde weiterhin nicht, und im
|
||||||
|
Log stand „Disc ausgeworfen". Ein Rückgabewert eines ioctls beweist nichts;
|
||||||
|
gefragt wird jetzt das Laufwerk.
|
||||||
|
|
||||||
Bewusst hier und nicht in detection.py: das Modul ist ein byteweiser
|
Bewusst hier und nicht in detection.py: das Modul ist ein byteweiser
|
||||||
Zwilling der API-Kopie. `fcntl` gibt es nur unter Linux — der native
|
Zwilling der API-Kopie. `fcntl` gibt es nur unter Linux — der native
|
||||||
Windows-Worker lädt ripping.py ebenfalls, rippt dort aber nie.
|
Windows-Worker lädt ripping.py ebenfalls, rippt dort aber nie.
|
||||||
|
|
||||||
|
Die drei Parameter sind nur zum Testen einspritzbar (kein echtes Laufwerk).
|
||||||
"""
|
"""
|
||||||
|
if ioctl_fn is None:
|
||||||
try:
|
try:
|
||||||
from fcntl import ioctl
|
from fcntl import ioctl
|
||||||
except ImportError: # Windows
|
except ImportError: # Windows — dieser Worker rippt nie
|
||||||
|
return False
|
||||||
|
ioctl_fn = ioctl
|
||||||
|
if warten is None:
|
||||||
|
import time
|
||||||
|
warten = time.sleep
|
||||||
|
oeffnen = oeffnen or (lambda p: os.open(p, os.O_RDONLY | os.O_NONBLOCK))
|
||||||
|
schliessen = schliessen or os.close
|
||||||
|
|
||||||
|
try:
|
||||||
|
fd = oeffnen(device_path)
|
||||||
|
except OSError:
|
||||||
return False
|
return False
|
||||||
try:
|
try:
|
||||||
fd = os.open(device_path, os.O_RDONLY | os.O_NONBLOCK)
|
# Entriegeln ist der entscheidende Schritt. Scheitert er, wird der
|
||||||
|
# Auswurf trotzdem versucht — bei einem nicht verriegelten Laufwerk
|
||||||
|
# (oder einem, das das ioctl nicht kennt) klappt er ohnehin.
|
||||||
try:
|
try:
|
||||||
ioctl(fd, CDROMEJECT, 0)
|
ioctl_fn(fd, CDROM_LOCKDOOR, 0)
|
||||||
finally:
|
except OSError:
|
||||||
os.close(fd)
|
pass
|
||||||
|
try:
|
||||||
|
ioctl_fn(fd, CDROMEJECT, 0)
|
||||||
|
except OSError:
|
||||||
|
return False
|
||||||
|
|
||||||
|
# Nachsehen statt hoffen: Die Schublade braucht ein bis zwei Sekunden.
|
||||||
|
for _ in range(AUSWURF_WARTEN_SEKUNDEN):
|
||||||
|
try:
|
||||||
|
if _auswurf_geglueckt(ioctl_fn(fd, CDROM_DRIVE_STATUS, 0)):
|
||||||
return True
|
return True
|
||||||
except OSError:
|
except OSError:
|
||||||
return False
|
return False
|
||||||
|
warten(1)
|
||||||
|
return False
|
||||||
|
finally:
|
||||||
|
try:
|
||||||
|
schliessen(fd)
|
||||||
|
except OSError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
def check_makemkv_installed() -> bool:
|
def check_makemkv_installed() -> bool:
|
||||||
|
|||||||
@@ -380,3 +380,87 @@ def test_fehler_ohne_preset_problem_bleibt_der_alte():
|
|||||||
|
|
||||||
ergebnis = ripping._handbrake_schleife(FakeProcess(), "/gibt-es-nicht.mkv")
|
ergebnis = ripping._handbrake_schleife(FakeProcess(), "/gibt-es-nicht.mkv")
|
||||||
assert ergebnis["error"] == "HandBrake endete mit Code 1"
|
assert ergebnis["error"] == "HandBrake endete mit Code 1"
|
||||||
|
|
||||||
|
|
||||||
|
# --- Auswurf: das ioctl meldet Erfolg und tut nichts (Befund 26.07.2026) -----
|
||||||
|
|
||||||
|
|
||||||
|
def _laufwerk(verriegelt=True, kennt_lockdoor=True):
|
||||||
|
"""Ein nachgebautes Laufwerk, das sich wie das echte verhaelt.
|
||||||
|
|
||||||
|
Gemessen am BU40N der Rippy-VM: Nach einem MakeMKV-Rip ist die Tuer
|
||||||
|
verriegelt. CDROMEJECT wird dann ANGENOMMEN und tut nichts - der Status
|
||||||
|
bleibt auf 4 (Disc drin). Erst CDROM_LOCKDOOR 0 macht den Auswurf wirksam.
|
||||||
|
"""
|
||||||
|
import ripping
|
||||||
|
|
||||||
|
zustand = {"verriegelt": verriegelt, "status": ripping.CDS_DISC_OK,
|
||||||
|
"aufrufe": []}
|
||||||
|
|
||||||
|
def ioctl_fn(fd, befehl, arg=0):
|
||||||
|
zustand["aufrufe"].append(befehl)
|
||||||
|
if befehl == ripping.CDROM_LOCKDOOR:
|
||||||
|
if not kennt_lockdoor:
|
||||||
|
raise OSError("ioctl unbekannt")
|
||||||
|
zustand["verriegelt"] = bool(arg)
|
||||||
|
return 0
|
||||||
|
if befehl == ripping.CDROMEJECT:
|
||||||
|
if not zustand["verriegelt"]:
|
||||||
|
zustand["status"] = ripping.CDS_TRAY_OPEN
|
||||||
|
return 0 # <- auch verriegelt: ERFOLG, aber ohne Wirkung
|
||||||
|
if befehl == ripping.CDROM_DRIVE_STATUS:
|
||||||
|
return zustand["status"]
|
||||||
|
raise OSError("unerwartetes ioctl")
|
||||||
|
|
||||||
|
return zustand, ioctl_fn
|
||||||
|
|
||||||
|
|
||||||
|
def test_auswurf_entriegelt_zuerst_und_klappt_dann():
|
||||||
|
import ripping
|
||||||
|
|
||||||
|
zustand, ioctl_fn = _laufwerk(verriegelt=True)
|
||||||
|
ok = ripping.wirf_disc_aus(
|
||||||
|
"/dev/sr0", ioctl_fn=ioctl_fn, oeffnen=lambda p: 42,
|
||||||
|
schliessen=lambda fd: None, warten=lambda s: None)
|
||||||
|
assert ok is True
|
||||||
|
assert zustand["status"] == ripping.CDS_TRAY_OPEN
|
||||||
|
# Reihenfolge: entriegeln VOR auswerfen
|
||||||
|
assert zustand["aufrufe"][0] == ripping.CDROM_LOCKDOOR
|
||||||
|
assert zustand["aufrufe"][1] == ripping.CDROMEJECT
|
||||||
|
|
||||||
|
|
||||||
|
def test_auswurf_meldet_fehlschlag_wenn_die_disc_drin_bleibt():
|
||||||
|
"""Der eigentliche Fehler. Vorher gab wirf_disc_aus True zurueck, weil das
|
||||||
|
ioctl nicht geworfen hatte - und ins Log kam "Disc ausgeworfen", waehrend
|
||||||
|
die Schublade zu blieb. Ein ioctl-Rueckgabewert beweist nichts."""
|
||||||
|
import ripping
|
||||||
|
|
||||||
|
# Ein Laufwerk, das LOCKDOOR nicht kennt und verriegelt bleibt
|
||||||
|
zustand, ioctl_fn = _laufwerk(verriegelt=True, kennt_lockdoor=False)
|
||||||
|
ok = ripping.wirf_disc_aus(
|
||||||
|
"/dev/sr0", ioctl_fn=ioctl_fn, oeffnen=lambda p: 42,
|
||||||
|
schliessen=lambda fd: None, warten=lambda s: None)
|
||||||
|
assert ok is False
|
||||||
|
assert zustand["status"] == ripping.CDS_DISC_OK # nie aufgegangen
|
||||||
|
|
||||||
|
|
||||||
|
def test_auswurf_bei_slot_laufwerk_ohne_schublade():
|
||||||
|
"""Ein Slot-Laufwerk hat keine Schublade und meldet nach dem Auswerfen
|
||||||
|
CDS_NO_DISC. Das muss als Erfolg zaehlen."""
|
||||||
|
import ripping
|
||||||
|
|
||||||
|
assert ripping._auswurf_geglueckt(ripping.CDS_NO_DISC) is True
|
||||||
|
assert ripping._auswurf_geglueckt(ripping.CDS_TRAY_OPEN) is True
|
||||||
|
assert ripping._auswurf_geglueckt(ripping.CDS_DISC_OK) is False
|
||||||
|
assert ripping._auswurf_geglueckt(ripping.CDS_DRIVE_NOT_READY) is False
|
||||||
|
|
||||||
|
|
||||||
|
def test_auswurf_ohne_laufwerk_wirft_nicht():
|
||||||
|
import ripping
|
||||||
|
|
||||||
|
def oeffnen_kaputt(p):
|
||||||
|
raise OSError("kein Laufwerk")
|
||||||
|
|
||||||
|
assert ripping.wirf_disc_aus(
|
||||||
|
"/dev/sr9", ioctl_fn=lambda *a: 0, oeffnen=oeffnen_kaputt,
|
||||||
|
schliessen=lambda fd: None, warten=lambda s: None) is False
|
||||||
|
|||||||
Reference in New Issue
Block a user