be04a9772e
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>
111 lines
3.6 KiB
Python
111 lines
3.6 KiB
Python
"""Laufwerks-Discovery über /sys und ioctls — ehrlich, ohne udev.
|
|
|
|
Warum kein udevadm mehr: Im Container läuft kein udevd, die udev-Datenbank ist
|
|
leer — `udevadm info` lieferte schlicht nichts (der alte Weg zeigte deshalb nie
|
|
ein Laufwerk an). /sys/class/block/<name>/device/{vendor,model} kommt dagegen
|
|
direkt vom Kernel und funktioniert überall.
|
|
"""
|
|
|
|
import glob
|
|
import os
|
|
from fcntl import ioctl
|
|
|
|
from detection import (
|
|
CDS_DISC_OK,
|
|
classify,
|
|
disc_size_bytes,
|
|
disc_status,
|
|
drive_status,
|
|
)
|
|
|
|
# include/uapi/linux/cdrom.h
|
|
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:
|
|
"""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)
|
|
try:
|
|
try:
|
|
ioctl(fd, CDROM_LOCKDOOR, 0)
|
|
except OSError:
|
|
pass # nicht verriegelt oder ioctl unbekannt — Auswurf trotzdem versuchen
|
|
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:
|
|
os.close(fd)
|
|
|
|
|
|
def list_optical_devices() -> list:
|
|
"""Alle optischen Laufwerke, die der Container sieht (devices: in compose)."""
|
|
return sorted(glob.glob("/dev/sr[0-9]*"))
|
|
|
|
|
|
def read_sys_attr(device_name: str, attr: str) -> str:
|
|
pfad = f"/sys/class/block/{device_name}/device/{attr}"
|
|
try:
|
|
with open(pfad, encoding="ascii", errors="replace") as f:
|
|
return f.read().strip()
|
|
except OSError:
|
|
return ""
|
|
|
|
|
|
def device_info(device_path: str) -> dict:
|
|
"""Baut den Geräte-Eintrag fürs UI: Name aus /sys, Disc-Status per ioctl."""
|
|
name = os.path.basename(device_path)
|
|
vendor = read_sys_attr(name, "vendor")
|
|
model = read_sys_attr(name, "model")
|
|
|
|
try:
|
|
status_code = drive_status(device_path)
|
|
except OSError:
|
|
status_code = -1
|
|
|
|
disc_type = "unknown"
|
|
status = "empty"
|
|
if status_code == CDS_DISC_OK:
|
|
status = "ready"
|
|
try:
|
|
disc_type = classify(disc_status(device_path), disc_size_bytes(device_path))
|
|
except OSError:
|
|
disc_type = "unknown"
|
|
|
|
return {
|
|
"id": name,
|
|
"name": " ".join(teil for teil in (vendor, model) if teil) or f"Laufwerk {name}",
|
|
"type": disc_type,
|
|
"path": device_path,
|
|
"status": status,
|
|
"model": model,
|
|
"serial": read_sys_attr(name, "wwid"),
|
|
}
|