feat(drives): V2-4 (Teil 1) — Windows-Laufwerkstreiber, ohne Hardware geprueft
Ampel / ampel (push) Successful in 45s
Ampel / ampel (push) Successful in 45s
WAS: rippy/drives/windows.py mit denselben zwei Vertraegen wie die Linux-Fassung (auswerfen_versuchen gibt False zurueck, eject wirft). Dazu win_ioctl.py: die Win32-Steuercodes, HERGELEITET statt abgeschrieben. WARUM ctypes und nicht pywin32: 20 MB zusaetzliche Abhaengigkeit, die in jedes PyInstaller-Paket muss und bei jedem Python-Wechsel neu passen will. Gebraucht werden fuenf Funktionen aus kernel32 — die spricht ctypes direkt an. DIE STEUERCODES WERDEN AUSGERECHNET, NICHT ABGETIPPT: win_ioctl.py enthaelt das CTL_CODE-Makro aus winioctl.h als Funktion; jede Konstante wird daraus gebildet. test_win_ioctl.py rechnet das Ergebnis gegen die in der Microsoft-Dokumentation stehenden Zahlen. Grund: Ein falscher IOCTL-Code meldet kein "unbekannter Befehl", sondern ERROR_INVALID_FUNCTION — und das liest sich wie "dieses Laufwerk kann das nicht". Man sucht dann am Geraet statt an einer Zahl. AGENTS Regel D, ernst genommen. DIE ZWEI PORT-REGELN GELTEN AUCH HIER: - Auswerfen heisst entriegeln, auswerfen, NACHSEHEN. Unter Linux wurde am 26.07.2026 gemessen, dass ein verriegeltes Laufwerk den Auswurf mit ERFOLG quittiert und nichts tut. IOCTL_STORAGE_EJECT_MEDIA meldet ebenfalls nur die Annahme des Befehls — also wird auch hier nachgesehen (CHECK_VERIFY2 muss ERROR_NOT_READY liefern). - Ein unzugaengliches Laufwerk ist UNBEKANNT, nicht leer. device_info kennt deshalb drei Zustaende, nicht zwei. EIN FEHLER, DEN DIE TESTS SOFORT GEFANGEN HABEN: Win32Fehler setzte self.winerror VOR super().__init__(). Nachgemessen: Ein zweiargumentiges OSError.__init__(code, text) setzt winerror wieder auf None. Damit erkannte _medium_da ERROR_NOT_READY nicht mehr, hielt jedes leere Laufwerk fuer einen echten Fehler und meldete "unbekannt" statt "leer". Vier Tests rot, Ursache in einer Minute gemessen statt geraten. ⚠️ WAS HIER NICHT BEWIESEN IST: dass echte Hardware sich so verhaelt. Das Laufwerk ist derzeit nirgends angeschlossen. Der Ablauf, die Fehlerunterscheidung und die Typ-Zuordnung sind gegen ein nachgebautes Laufwerk geprueft (51 Tests, laufen auf jeder Plattform) — die Hardware- Schicht gilt bis zu einer Messung als GEBAUT, nicht als BEWIESEN. GEMESSEN: ruff sauber, 400 Tests gruen + 4 uebersprungen (vorher 378). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
4337253797
commit
b723fee3a2
@@ -0,0 +1,84 @@
|
||||
"""Die Win32-Steuercodes gegen die dokumentierten Zahlen rechnen.
|
||||
|
||||
## Warum dieser Test wichtig ist
|
||||
|
||||
Ein falscher IOCTL-Code liefert kein „unbekannter Befehl". Windows antwortet
|
||||
mit `ERROR_INVALID_FUNCTION` — und das liest sich wie „dieses Laufwerk kann
|
||||
das nicht". Man sucht dann tagelang am Gerät statt an einer Zahl.
|
||||
|
||||
Die Werte rechts sind die in `winioctl.h` / `ntddcdrm.h` und auf Microsoft
|
||||
Learn dokumentierten Konstanten. Links steht, was `ctl_code()` aus den
|
||||
Bestandteilen bildet. Stimmen beide überein, ist die Herleitung richtig.
|
||||
|
||||
Läuft auf JEDER Plattform: Hier wird nur gerechnet, nichts aufgerufen. Der
|
||||
Zahlendreher fällt damit schon auf dem Entwicklungsrechner auf und nicht erst
|
||||
an einem echten Laufwerk.
|
||||
"""
|
||||
|
||||
import pytest
|
||||
|
||||
from rippy.drives import win_ioctl as w
|
||||
|
||||
|
||||
def test_ctl_code_bildet_das_makro_aus_winioctl_h_ab():
|
||||
"""CTL_CODE(DeviceType, Function, Method, Access) =
|
||||
(DeviceType << 16) | (Access << 14) | (Function << 2) | Method"""
|
||||
assert w.ctl_code(0, 0, 0, 0) == 0
|
||||
assert w.ctl_code(1, 0, 0, 0) == 1 << 16
|
||||
assert w.ctl_code(0, 1, 0, 0) == 1 << 2
|
||||
assert w.ctl_code(0, 0, 3, 0) == 3
|
||||
assert w.ctl_code(0, 0, 0, 1) == 1 << 14
|
||||
|
||||
|
||||
@pytest.mark.parametrize("name,erwartet", [
|
||||
# Die Zahlen aus der Microsoft-Dokumentation bzw. den SDK-Headern.
|
||||
("IOCTL_STORAGE_CHECK_VERIFY2", 0x002D0800),
|
||||
("IOCTL_STORAGE_MEDIA_REMOVAL", 0x002D4804),
|
||||
("IOCTL_STORAGE_EJECT_MEDIA", 0x002D4808),
|
||||
("IOCTL_STORAGE_LOAD_MEDIA", 0x002D480C),
|
||||
("IOCTL_CDROM_DISK_TYPE", 0x00020040),
|
||||
("IOCTL_DISK_GET_LENGTH_INFO", 0x0007405C),
|
||||
])
|
||||
def test_steuercode_stimmt_mit_der_dokumentation_ueberein(name, erwartet):
|
||||
assert getattr(w, name) == erwartet, (
|
||||
f"{name} ist {getattr(w, name):#010x}, dokumentiert ist {erwartet:#010x}. "
|
||||
"Ein falscher Steuercode meldet ERROR_INVALID_FUNCTION — das sieht aus "
|
||||
"wie 'das Laufwerk kann das nicht' und schickt die Fehlersuche in die "
|
||||
"falsche Richtung."
|
||||
)
|
||||
|
||||
|
||||
# ── Gerätepfad ──────────────────────────────────────────────────────────
|
||||
@pytest.mark.parametrize("eingabe", ["D", "D:", "d:", "D:\\", "d:/", " D: "])
|
||||
def test_geraetepfad_ist_immer_dieselbe_form(eingabe):
|
||||
assert w.geraetepfad(eingabe) == r"\\.\D:"
|
||||
|
||||
|
||||
def test_geraetepfad_haengt_keinen_backslash_an():
|
||||
"""Der entscheidende Punkt: MIT abschliessendem Backslash oeffnet
|
||||
CreateFileW das WURZELVERZEICHNIS des Dateisystems statt des Geraets.
|
||||
Die IOCTLs scheitern dann mit ERROR_INVALID_FUNCTION."""
|
||||
assert not w.geraetepfad("E:\\").endswith("\\")
|
||||
assert w.geraetepfad("E:\\") == r"\\.\E:"
|
||||
|
||||
|
||||
def test_geraetepfad_ohne_angabe_wirft():
|
||||
for leer in ("", " ", ":", "\\"):
|
||||
with pytest.raises(ValueError):
|
||||
w.geraetepfad(leer)
|
||||
|
||||
|
||||
def test_drive_cdrom_ist_fuenf():
|
||||
"""GetDriveTypeW meldet optische Laufwerke als 5. Ein Zahlendreher hier
|
||||
hiesse: Rippy findet entweder gar kein Laufwerk oder haelt die
|
||||
Systemplatte fuer eines."""
|
||||
assert w.DRIVE_CDROM == 5
|
||||
assert w.DRIVE_FIXED == 3
|
||||
assert w.DRIVE_REMOVABLE == 2
|
||||
|
||||
|
||||
def test_error_not_ready_ist_einundzwanzig():
|
||||
"""Das ist die Antwort von CHECK_VERIFY2 bei leerem Laufwerk — und damit
|
||||
die Windows-Entsprechung von CDS_NO_DISC. Sie ENTSCHEIDET, ob ein
|
||||
Auswurf als geglueckt gilt."""
|
||||
assert w.ERROR_NOT_READY == 21
|
||||
@@ -0,0 +1,232 @@
|
||||
"""Der Windows-Treiber gegen ein nachgebautes Laufwerk.
|
||||
|
||||
## Was diese Tests beweisen — und was nicht
|
||||
|
||||
**Sie beweisen den ABLAUF:** dass erst entriegelt und dann ausgeworfen wird,
|
||||
dass danach nachgesehen statt geglaubt wird, dass ein verriegeltes Laufwerk als
|
||||
Fehlschlag gilt, dass ein unzugängliches Laufwerk „unbekannt" ergibt und nicht
|
||||
„leer", und dass beide Verträge (werfend / nicht werfend) stimmen.
|
||||
|
||||
**Sie beweisen NICHT, dass echte Hardware sich so verhält.** Dafür braucht es
|
||||
ein angestecktes Laufwerk und eine Disc. Bis das gemessen ist, gilt der Treiber
|
||||
als gebaut, nicht als bewiesen.
|
||||
|
||||
Das nachgebaute Laufwerk unten bildet genau das Verhalten ab, das am
|
||||
26.07.2026 am echten BU40N unter Linux GEMESSEN wurde: Der Auswurf-Befehl wird
|
||||
auch bei verriegelter Tür mit Erfolg quittiert und tut nichts. Es gibt keinen
|
||||
Anlass anzunehmen, dass Windows das anders macht — beide Wege enden im selben
|
||||
SCSI-Befehl an dasselbe Gerät.
|
||||
"""
|
||||
|
||||
import pytest
|
||||
|
||||
from rippy.drives import win_ioctl as w
|
||||
from rippy.drives import windows
|
||||
|
||||
|
||||
class FakeLaufwerk:
|
||||
"""Ein Laufwerk aus Papier — verhält sich wie das gemessene echte.
|
||||
|
||||
`verriegelt=True` bildet den Zustand nach einem MakeMKV-Rip ab: Die Tür ist
|
||||
zu, und der Auswurf-Befehl wird trotzdem mit Erfolg quittiert.
|
||||
"""
|
||||
|
||||
def __init__(self, medium=True, verriegelt=False, kennt_lockdoor=True,
|
||||
buchstaben=("C", "D"), typen=None, disk_flags=None,
|
||||
groesse=None, oeffnen_fehler=None):
|
||||
self.medium = medium
|
||||
self.verriegelt = verriegelt
|
||||
self.kennt_lockdoor = kennt_lockdoor
|
||||
self._buchstaben = list(buchstaben)
|
||||
self._typen = typen or {"C": w.DRIVE_FIXED, "D": w.DRIVE_CDROM}
|
||||
self.disk_flags = disk_flags
|
||||
self.groesse = groesse
|
||||
self.oeffnen_fehler = oeffnen_fehler
|
||||
self.aufrufe = [] # Reihenfolge der Steuercodes
|
||||
self.offen = 0
|
||||
|
||||
# -- Laufwerks-Auflistung
|
||||
def laufwerksbuchstaben(self):
|
||||
return list(self._buchstaben)
|
||||
|
||||
def laufwerkstyp(self, buchstabe):
|
||||
return self._typen.get(buchstabe, w.DRIVE_UNKNOWN)
|
||||
|
||||
# -- Gerät
|
||||
def oeffnen(self, pfad):
|
||||
if self.oeffnen_fehler is not None:
|
||||
raise self.oeffnen_fehler
|
||||
self.offen += 1
|
||||
return 4242
|
||||
|
||||
def schliessen(self, handle):
|
||||
self.offen -= 1
|
||||
|
||||
def steuern(self, handle, code, eingabe=b"", ausgabe_bytes=0):
|
||||
self.aufrufe.append(code)
|
||||
|
||||
if code == w.IOCTL_STORAGE_MEDIA_REMOVAL:
|
||||
if not self.kennt_lockdoor:
|
||||
raise windows.Win32Fehler("MEDIA_REMOVAL", w.ERROR_INVALID_FUNCTION)
|
||||
self.verriegelt = eingabe[:1] == b"\x01"
|
||||
return b""
|
||||
|
||||
if code == w.IOCTL_STORAGE_EJECT_MEDIA:
|
||||
# ⚠️ Der gemessene Punkt: Erfolg wird IMMER gemeldet. Gewirkt hat
|
||||
# er nur, wenn die Tuer nicht verriegelt ist.
|
||||
if not self.verriegelt:
|
||||
self.medium = False
|
||||
return b""
|
||||
|
||||
if code == w.IOCTL_STORAGE_CHECK_VERIFY2:
|
||||
if not self.medium:
|
||||
raise windows.Win32Fehler("CHECK_VERIFY2", w.ERROR_NOT_READY)
|
||||
return b""
|
||||
|
||||
if code == w.IOCTL_CDROM_DISK_TYPE:
|
||||
if self.disk_flags is None:
|
||||
raise windows.Win32Fehler("DISK_TYPE", w.ERROR_INVALID_FUNCTION)
|
||||
return int(self.disk_flags).to_bytes(4, "little")
|
||||
|
||||
if code == w.IOCTL_DISK_GET_LENGTH_INFO:
|
||||
if self.groesse is None:
|
||||
raise windows.Win32Fehler("GET_LENGTH_INFO", w.ERROR_INVALID_FUNCTION)
|
||||
return int(self.groesse).to_bytes(8, "little")
|
||||
|
||||
raise windows.Win32Fehler(f"unerwarteter Code {code:#x}", w.ERROR_INVALID_FUNCTION)
|
||||
|
||||
|
||||
# ── Laufwerke finden ────────────────────────────────────────────────────
|
||||
def test_nur_optische_laufwerke_werden_gelistet():
|
||||
"""Waeren es alle, stuende die Systemplatte als „Laufwerk" im UI."""
|
||||
api = FakeLaufwerk(buchstaben=("C", "D", "E", "Z"), typen={
|
||||
"C": w.DRIVE_FIXED, "D": w.DRIVE_CDROM,
|
||||
"E": w.DRIVE_CDROM, "Z": w.DRIVE_REMOTE,
|
||||
})
|
||||
assert windows.list_optical_devices(api) == [r"\\.\D:", r"\\.\E:"]
|
||||
|
||||
|
||||
def test_ohne_optisches_laufwerk_ist_die_liste_leer():
|
||||
api = FakeLaufwerk(buchstaben=("C",), typen={"C": w.DRIVE_FIXED})
|
||||
assert windows.list_optical_devices(api) == []
|
||||
|
||||
|
||||
# ── Auswurf: die Reihenfolge ist der Punkt ──────────────────────────────
|
||||
def test_auswurf_entriegelt_zuerst_und_klappt_dann():
|
||||
api = FakeLaufwerk(medium=True, verriegelt=True)
|
||||
assert windows.auswerfen_versuchen(r"\\.\D:", api=api, warten=lambda s: None) is True
|
||||
assert api.medium is False
|
||||
# Entriegeln VOR Auswerfen — sonst quittiert das Laufwerk Erfolg und tut nichts.
|
||||
assert api.aufrufe[0] == w.IOCTL_STORAGE_MEDIA_REMOVAL
|
||||
assert api.aufrufe[1] == w.IOCTL_STORAGE_EJECT_MEDIA
|
||||
|
||||
|
||||
def test_auswurf_meldet_fehlschlag_wenn_die_disc_drin_bleibt():
|
||||
"""Der teure Fehler aus v1, hier vorweggenommen: Ein Laufwerk, das das
|
||||
Entriegeln nicht kennt und verriegelt bleibt, quittiert den Auswurf
|
||||
trotzdem mit Erfolg. Wer dem Rueckgabewert glaubt, schreibt „Disc
|
||||
ausgeworfen" ins Log, waehrend die Schublade zu bleibt."""
|
||||
api = FakeLaufwerk(medium=True, verriegelt=True, kennt_lockdoor=False)
|
||||
assert windows.auswerfen_versuchen(r"\\.\D:", api=api, warten=lambda s: None) is False
|
||||
assert api.medium is True # nie aufgegangen
|
||||
|
||||
|
||||
def test_auswurf_ohne_laufwerk_wirft_nicht():
|
||||
api = FakeLaufwerk(oeffnen_fehler=windows.Win32Fehler("CreateFileW", w.ERROR_FILE_NOT_FOUND))
|
||||
assert windows.auswerfen_versuchen(r"\\.\Z:", api=api, warten=lambda s: None) is False
|
||||
|
||||
|
||||
def test_geraet_wird_immer_wieder_geschlossen():
|
||||
"""Ein offenes Handle auf einem optischen Laufwerk blockiert den Auswurf
|
||||
fuer JEDEN anderen — auch fuer den Explorer."""
|
||||
api = FakeLaufwerk(medium=True)
|
||||
windows.auswerfen_versuchen(r"\\.\D:", api=api, warten=lambda s: None)
|
||||
assert api.offen == 0
|
||||
|
||||
|
||||
# ── Der andere Vertrag: eject() muss werfen ─────────────────────────────
|
||||
def test_eject_ist_still_wenn_die_disc_rauskommt():
|
||||
api = FakeLaufwerk(medium=True, verriegelt=True)
|
||||
assert windows.eject(r"\\.\D:", api=api) is None
|
||||
|
||||
|
||||
def test_eject_wirft_wenn_die_disc_drin_bleibt():
|
||||
api = FakeLaufwerk(medium=True, verriegelt=True, kennt_lockdoor=False)
|
||||
with pytest.raises(OSError, match="noch"):
|
||||
windows.eject(r"\\.\D:", api=api)
|
||||
|
||||
|
||||
def test_eject_reicht_den_urspruenglichen_fehler_weiter():
|
||||
"""„Zugriff verweigert" und „Gerät nicht gefunden" sind zwei ganz
|
||||
verschiedene Probleme. Wer sie hinter einer eigenen Meldung versteckt,
|
||||
nimmt dem Nutzer die einzige brauchbare Auskunft."""
|
||||
original = windows.Win32Fehler("CreateFileW", w.ERROR_ACCESS_DENIED)
|
||||
api = FakeLaufwerk(oeffnen_fehler=original)
|
||||
with pytest.raises(OSError) as fehler:
|
||||
windows.eject(r"\\.\D:", api=api)
|
||||
assert fehler.value is original
|
||||
# `code` statt `winerror`: letzteres gibt es nur unter Windows, und der
|
||||
# Treiber wird auch anderswo getestet.
|
||||
assert fehler.value.code == w.ERROR_ACCESS_DENIED
|
||||
|
||||
|
||||
# ── Zustand und Typ ─────────────────────────────────────────────────────
|
||||
def test_leeres_laufwerk_ist_leer_und_nicht_unbekannt():
|
||||
api = FakeLaufwerk(medium=False)
|
||||
assert windows.detect_disc_type(r"\\.\D:", api=api) == "no_disc"
|
||||
assert windows.device_info(r"\\.\D:", api=api)["status"] == "empty"
|
||||
|
||||
|
||||
def test_unzugaengliches_laufwerk_ist_UNBEKANNT_und_nicht_leer():
|
||||
"""DIE Regel des Ports. Ein Laufwerk, das gerade belegt ist, hat nicht
|
||||
„keine Disc" — man weiss es nur nicht. In v1 hat genau diese Vermischung
|
||||
Listen im UI geleert."""
|
||||
api = FakeLaufwerk(oeffnen_fehler=windows.Win32Fehler("CreateFileW", w.ERROR_ACCESS_DENIED))
|
||||
assert windows.detect_disc_type(r"\\.\D:", api=api) == "unknown"
|
||||
assert windows.device_info(r"\\.\D:", api=api)["status"] == "unknown"
|
||||
|
||||
|
||||
@pytest.mark.parametrize("flags,groesse,erwartet", [
|
||||
(w.CDROM_DISK_AUDIO_TRACK, 700 * 1024**2, "cd"),
|
||||
(w.CDROM_DISK_DATA_TRACK, 8 * 1024**3, "dvd"),
|
||||
(w.CDROM_DISK_DATA_TRACK, 25 * 1024**3, "bluray"),
|
||||
(w.CDROM_DISK_DATA_TRACK, 50 * 1000**3, "bluray"), # BD-50
|
||||
(w.CDROM_DISK_DATA_TRACK, 66 * 1000**3, "uhd"), # BD-66
|
||||
(w.CDROM_DISK_DATA_TRACK, 100 * 1000**3, "uhd"),
|
||||
])
|
||||
def test_disc_typ_kommt_aus_derselben_zuordnung_wie_unter_linux(flags, groesse, erwartet):
|
||||
"""Die Einordnung liegt in cdrom.classify() — EINE Funktion fuer beide
|
||||
Plattformen, und die ist eigenstaendig getestet. Hier wird nur geprueft,
|
||||
dass Windows die beiden Eingangswerte richtig beschafft."""
|
||||
api = FakeLaufwerk(medium=True, disk_flags=flags, groesse=groesse)
|
||||
assert windows.detect_disc_type(r"\\.\D:", api=api) == erwartet
|
||||
|
||||
|
||||
def test_verriegeln_schickt_das_richtige_byte():
|
||||
api = FakeLaufwerk(medium=True)
|
||||
windows.verriegeln(r"\\.\D:", True, api=api)
|
||||
assert api.verriegelt is True
|
||||
windows.verriegeln(r"\\.\D:", False, api=api)
|
||||
assert api.verriegelt is False
|
||||
|
||||
|
||||
def test_device_info_hat_dieselben_felder_wie_unter_linux():
|
||||
"""Das UI unterscheidet nicht nach Plattform — fehlt ein Feld, bleibt die
|
||||
Karte im Dashboard leer."""
|
||||
api = FakeLaufwerk(medium=True, disk_flags=w.CDROM_DISK_DATA_TRACK, groesse=25 * 1024**3)
|
||||
eintrag = windows.device_info(r"\\.\D:", api=api)
|
||||
assert set(eintrag) == {"id", "name", "type", "path", "status", "model", "serial"}
|
||||
assert eintrag["id"] == "D"
|
||||
assert eintrag["status"] == "ready"
|
||||
assert eintrag["type"] == "bluray"
|
||||
|
||||
|
||||
def test_treiber_verweigert_sich_ausserhalb_von_windows():
|
||||
"""Sonst waere der erste Fehler ein kryptischer ctypes-Absturz statt
|
||||
einer Ansage."""
|
||||
import sys as _sys
|
||||
|
||||
if _sys.platform.startswith("win"):
|
||||
pytest.skip("laeuft nur, wenn NICHT unter Windows getestet wird")
|
||||
with pytest.raises(RuntimeError, match="nur unter Windows"):
|
||||
windows.Win32()
|
||||
@@ -0,0 +1,128 @@
|
||||
"""Die Win32-Steuercodes für optische Laufwerke — hergeleitet, nicht abgeschrieben.
|
||||
|
||||
## Warum sie hier ausgerechnet werden
|
||||
|
||||
`AGENTS.md` Regel D: *„Externe Schnittstellen NIE aus dem Kopf. Vor Nutzung
|
||||
fremder CLI-Flags oder Bibliotheks-APIs: --help/Doku prüfen und die Fundstelle
|
||||
im Commit nennen."*
|
||||
|
||||
Zahlen wie `0x2D4808` sind genau der Fall, in dem sich ein Zahlendreher
|
||||
lautlos versteckt: Ein falscher Code liefert kein „unbekannter Befehl", sondern
|
||||
`ERROR_INVALID_FUNCTION` — und das sieht aus wie „das Laufwerk kann das nicht".
|
||||
Man sucht dann am Gerät statt an der Zahl.
|
||||
|
||||
Deshalb steht hier das Makro aus `winioctl.h` als Funktion, und jede Konstante
|
||||
wird daraus gebildet. Wer nachrechnen will, sieht die Bestandteile.
|
||||
|
||||
#define CTL_CODE(DeviceType, Function, Method, Access) ( \\
|
||||
((DeviceType) << 16) | ((Access) << 14) | \\
|
||||
((Function) << 2) | (Method) \\
|
||||
)
|
||||
|
||||
Fundstellen (Windows SDK / Microsoft Learn):
|
||||
* CTL_CODE, FILE_DEVICE_*, METHOD_*, FILE_*_ACCESS — winioctl.h
|
||||
* IOCTL_STORAGE_CHECK_VERIFY2 — learn.microsoft.com/windows/win32/api/
|
||||
winioctl/ni-winioctl-ioctl_storage_check_verify2
|
||||
* IOCTL_STORAGE_MEDIA_REMOVAL / _EJECT_MEDIA / _LOAD_MEDIA — ebenda
|
||||
* IOCTL_CDROM_DISK_TYPE, CDROM_DISK_AUDIO_TRACK/DATA_TRACK — ntddcdrm.h
|
||||
* IOCTL_DISK_GET_LENGTH_INFO — winioctl.h (GET_LENGTH_INFORMATION)
|
||||
|
||||
## Gegenprobe
|
||||
|
||||
`test_win_ioctl.py` rechnet die hier gebildeten Werte gegen die Zahlen, die in
|
||||
der Microsoft-Dokumentation stehen. Stimmt eine nicht, faellt das beim Testen
|
||||
auf — auf JEDER Plattform, auch ohne Windows und ohne Laufwerk.
|
||||
"""
|
||||
|
||||
# ── Bausteine aus winioctl.h ────────────────────────────────────────────
|
||||
FILE_DEVICE_CD_ROM = 0x00000002
|
||||
FILE_DEVICE_DISK = 0x00000007
|
||||
FILE_DEVICE_MASS_STORAGE = 0x0000002D # = IOCTL_STORAGE_BASE
|
||||
|
||||
METHOD_BUFFERED = 0
|
||||
METHOD_IN_DIRECT = 1
|
||||
METHOD_OUT_DIRECT = 2
|
||||
METHOD_NEITHER = 3
|
||||
|
||||
FILE_ANY_ACCESS = 0
|
||||
FILE_READ_ACCESS = 0x0001
|
||||
FILE_WRITE_ACCESS = 0x0002
|
||||
|
||||
|
||||
def ctl_code(geraetetyp: int, funktion: int, methode: int, zugriff: int) -> int:
|
||||
"""Das CTL_CODE-Makro aus winioctl.h, eins zu eins."""
|
||||
return ((geraetetyp << 16) | (zugriff << 14) | (funktion << 2) | methode)
|
||||
|
||||
|
||||
# ── Speicher (IOCTL_STORAGE_*) ──────────────────────────────────────────
|
||||
# „Ist ein Medium eingelegt?" — die Windows-Entsprechung von
|
||||
# CDROM_DRIVE_STATUS. Antwortet mit ERROR_NOT_READY (21), wenn nichts drin ist.
|
||||
IOCTL_STORAGE_CHECK_VERIFY2 = ctl_code(
|
||||
FILE_DEVICE_MASS_STORAGE, 0x0200, METHOD_BUFFERED, FILE_ANY_ACCESS)
|
||||
|
||||
# Tür verriegeln/entriegeln — die Entsprechung von CDROM_LOCKDOOR.
|
||||
# Erwartet eine PREVENT_MEDIA_REMOVAL-Struktur (ein BOOLEAN).
|
||||
IOCTL_STORAGE_MEDIA_REMOVAL = ctl_code(
|
||||
FILE_DEVICE_MASS_STORAGE, 0x0201, METHOD_BUFFERED, FILE_READ_ACCESS)
|
||||
|
||||
# Auswerfen — die Entsprechung von CDROMEJECT.
|
||||
IOCTL_STORAGE_EJECT_MEDIA = ctl_code(
|
||||
FILE_DEVICE_MASS_STORAGE, 0x0202, METHOD_BUFFERED, FILE_READ_ACCESS)
|
||||
|
||||
# Schublade einziehen — die Entsprechung von CDROMCLOSETRAY.
|
||||
IOCTL_STORAGE_LOAD_MEDIA = ctl_code(
|
||||
FILE_DEVICE_MASS_STORAGE, 0x0203, METHOD_BUFFERED, FILE_READ_ACCESS)
|
||||
|
||||
# ── CD-ROM (IOCTL_CDROM_*) ──────────────────────────────────────────────
|
||||
# Audio- oder Datenspur? Grob, aber genau das, was CDROM_DISC_STATUS auch
|
||||
# liefert — mehr braucht die Einordnung nicht (die Feinunterscheidung
|
||||
# DVD/BD/UHD kommt über die Größe).
|
||||
IOCTL_CDROM_DISK_TYPE = ctl_code(
|
||||
FILE_DEVICE_CD_ROM, 0x0010, METHOD_BUFFERED, FILE_ANY_ACCESS)
|
||||
|
||||
CDROM_DISK_AUDIO_TRACK = 0x00000001
|
||||
CDROM_DISK_DATA_TRACK = 0x00000002
|
||||
|
||||
# ── Größe des Mediums ───────────────────────────────────────────────────
|
||||
# Die Entsprechung von BLKGETSIZE64. Liefert GET_LENGTH_INFORMATION
|
||||
# (ein LARGE_INTEGER, also 8 Byte).
|
||||
IOCTL_DISK_GET_LENGTH_INFO = ctl_code(
|
||||
FILE_DEVICE_DISK, 0x0017, METHOD_BUFFERED, FILE_READ_ACCESS)
|
||||
|
||||
# ── Rückmeldungen von GetLastError() ────────────────────────────────────
|
||||
ERROR_NOT_READY = 21 # kein Medium eingelegt
|
||||
ERROR_ACCESS_DENIED = 5
|
||||
ERROR_FILE_NOT_FOUND = 2
|
||||
ERROR_INVALID_FUNCTION = 1 # Gerät kennt diesen Steuercode nicht
|
||||
ERROR_MEDIA_CHANGED = 1110
|
||||
ERROR_NO_MEDIA_IN_DRIVE = 1112
|
||||
|
||||
# ── GetDriveTypeW ───────────────────────────────────────────────────────
|
||||
DRIVE_UNKNOWN = 0
|
||||
DRIVE_NO_ROOT_DIR = 1
|
||||
DRIVE_REMOVABLE = 2
|
||||
DRIVE_FIXED = 3
|
||||
DRIVE_REMOTE = 4
|
||||
DRIVE_CDROM = 5
|
||||
DRIVE_RAMDISK = 6
|
||||
|
||||
# ── CreateFileW ─────────────────────────────────────────────────────────
|
||||
GENERIC_READ = 0x80000000
|
||||
FILE_SHARE_READ = 0x00000001
|
||||
FILE_SHARE_WRITE = 0x00000002
|
||||
OPEN_EXISTING = 3
|
||||
INVALID_HANDLE_VALUE = -1
|
||||
|
||||
|
||||
def geraetepfad(buchstabe: str) -> str:
|
||||
r"""„D:" oder „D" oder „D:\" -> r"\\.\D:"
|
||||
|
||||
Der Win32-Gerätepfad hat KEINEN abschließenden Backslash. Mit einem
|
||||
öffnet `CreateFileW` das Wurzelverzeichnis des Dateisystems statt des
|
||||
Geräts — die IOCTLs scheitern dann mit ERROR_INVALID_FUNCTION, was wie
|
||||
„das Laufwerk kann das nicht" aussieht.
|
||||
"""
|
||||
kern = (buchstabe or "").strip().rstrip("\\/").rstrip(":")
|
||||
if not kern:
|
||||
raise ValueError("Kein Laufwerksbuchstabe angegeben")
|
||||
return "\\\\.\\" + kern[0].upper() + ":"
|
||||
@@ -0,0 +1,361 @@
|
||||
"""Laufwerks-Treiber für Windows. Erfüllt `rippy.ports.Drives`.
|
||||
|
||||
Dieselben Verträge wie `linux.py`, nur mit Win32 statt ioctl:
|
||||
|
||||
auswerfen_versuchen(geraet) -> bool gibt False zurück, wirft nie
|
||||
eject(geraet) -> None wirft OSError
|
||||
|
||||
## Warum ctypes und nicht pywin32
|
||||
|
||||
pywin32 wäre eine zusätzliche Abhängigkeit von rund 20 MB, die in ein
|
||||
PyInstaller-Paket mit hinein muss und bei jedem Python-Wechsel neu passen
|
||||
will. Alles, was hier gebraucht wird, sind fünf Funktionen aus `kernel32.dll` —
|
||||
die spricht `ctypes` direkt an, ohne irgendetwas mitzuliefern.
|
||||
|
||||
## Die zwei Regeln des Ports gelten hier genauso
|
||||
|
||||
**1. Auswerfen heißt: entriegeln, auswerfen, NACHSEHEN.** Unter Linux wurde am
|
||||
26.07.2026 gemessen, dass ein verriegeltes Laufwerk den Auswurf mit ERFOLG
|
||||
quittiert und nichts tut — MakeMKV verriegelt die Tür während des Rips und
|
||||
entriegelt sie nicht wieder. Es gibt keinen Grund anzunehmen, dass Windows das
|
||||
anders handhabt: `IOCTL_STORAGE_EJECT_MEDIA` meldet ebenfalls nur, dass der
|
||||
Befehl angenommen wurde. Deshalb wird auch hier nachgesehen — der Beweis ist
|
||||
`IOCTL_STORAGE_CHECK_VERIFY2`, das bei leerem Laufwerk `ERROR_NOT_READY`
|
||||
liefert.
|
||||
|
||||
**2. Ein unbekannter Zustand heißt UNBEKANNT, nicht leer.** Wenn sich das Gerät
|
||||
nicht öffnen lässt oder ein Steuercode scheitert, ist die Antwort „ich weiß es
|
||||
nicht" — nicht „da ist keine Disc". Genau diese Vermischung hat in v1 die
|
||||
Listen im UI geleert.
|
||||
|
||||
## Was hier geprüft ist und was nicht
|
||||
|
||||
**Geprüft, ohne Laufwerk:** die Steuercodes (`test_win_ioctl.py` rechnet sie
|
||||
gegen die Dokumentation), der Ablauf des Auswurfs, die Fehlerunterscheidung,
|
||||
die Typ-Zuordnung — alles über eine eingespritzte Win32-Nachbildung
|
||||
(`test_windows.py`). Diese Tests laufen auf jeder Plattform.
|
||||
|
||||
**NICHT geprüft:** dass echte Hardware sich so verhält. Dafür braucht es ein
|
||||
angestecktes Laufwerk und eine Disc. Solange das nicht gemessen ist, gilt
|
||||
dieser Treiber als GEBAUT, nicht als BEWIESEN — die Unterscheidung steht so
|
||||
in AGENTS.md („Beweisen statt behaupten").
|
||||
"""
|
||||
|
||||
import sys
|
||||
|
||||
from rippy.drives import win_ioctl as w
|
||||
from rippy.drives.cdrom import (
|
||||
CDS_AUDIO,
|
||||
CDS_DATA_1,
|
||||
CDS_DISC_OK,
|
||||
CDS_NO_DISC,
|
||||
classify,
|
||||
)
|
||||
|
||||
# Wie lange nach dem Auswurf-Befehl auf die Schublade gewartet wird.
|
||||
# Gleicher Wert wie unter Linux — ein Laufwerk braucht ein bis zwei Sekunden.
|
||||
AUSWURF_WARTEN_SEKUNDEN = 5
|
||||
|
||||
# Ergebnisse von auswerfen_mit_grund — dieselben wie im Linux-Treiber, damit
|
||||
# aufrufender Code nicht wissen muss, auf welcher Plattform er läuft.
|
||||
AUSWURF_OK = "ok"
|
||||
AUSWURF_KEIN_ZUGRIFF = "kein-zugriff"
|
||||
AUSWURF_ABGELEHNT = "abgelehnt"
|
||||
AUSWURF_BLEIBT_DRIN = "bleibt-drin"
|
||||
|
||||
|
||||
class Win32Fehler(OSError):
|
||||
"""Ein fehlgeschlagener Win32-Aufruf, mit dem Code von GetLastError().
|
||||
|
||||
⚠️ Die Zuweisung MUSS nach `super().__init__()` stehen. Nachgemessen:
|
||||
Ein zweiargumentiges `OSError.__init__(code, text)` setzt `winerror`
|
||||
wieder auf `None` — ein davor gesetzter Wert ist danach fort. Genau
|
||||
daran sind beim Bau dieser Datei vier Tests gescheitert: `_medium_da`
|
||||
erkannte `ERROR_NOT_READY` nicht mehr, hielt jedes leere Laufwerk für
|
||||
einen echten Fehler und meldete „unbekannt" statt „leer".
|
||||
|
||||
`code` steht zusätzlich als eigener Name bereit — `winerror` gibt es nur
|
||||
unter Windows, und dieser Treiber wird auch anderswo getestet.
|
||||
"""
|
||||
|
||||
def __init__(self, was: str, code: int):
|
||||
super().__init__(code, f"{was} scheiterte (Win32-Fehler {code})")
|
||||
self.code = code
|
||||
self.winerror = code
|
||||
|
||||
|
||||
class Win32:
|
||||
"""Die echten kernel32-Aufrufe.
|
||||
|
||||
In Tests wird stattdessen eine Nachbildung eingesetzt — deshalb steckt
|
||||
hier NUR die Anbindung und keine Entscheidung. Was der Treiber tut, steht
|
||||
in den Funktionen darunter und ist damit ohne Windows prüfbar.
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
if not sys.platform.startswith("win"):
|
||||
raise RuntimeError(
|
||||
"Der Windows-Treiber laeuft nur unter Windows. Auf Linux ist "
|
||||
"rippy.drives.linux zustaendig."
|
||||
)
|
||||
import ctypes
|
||||
from ctypes import wintypes
|
||||
|
||||
self._ctypes = ctypes
|
||||
self._wintypes = wintypes
|
||||
self._k32 = ctypes.WinDLL("kernel32", use_last_error=True)
|
||||
|
||||
# ── Laufwerke finden ────────────────────────────────────────────────
|
||||
def laufwerksbuchstaben(self) -> list:
|
||||
maske = self._k32.GetLogicalDrives()
|
||||
return [chr(ord("A") + i) for i in range(26) if maske & (1 << i)]
|
||||
|
||||
def laufwerkstyp(self, buchstabe: str) -> int:
|
||||
return self._k32.GetDriveTypeW(f"{buchstabe}:\\")
|
||||
|
||||
# ── Gerät öffnen ────────────────────────────────────────────────────
|
||||
def oeffnen(self, geraetepfad: str):
|
||||
handle = self._k32.CreateFileW(
|
||||
geraetepfad,
|
||||
w.GENERIC_READ,
|
||||
w.FILE_SHARE_READ | w.FILE_SHARE_WRITE,
|
||||
None,
|
||||
w.OPEN_EXISTING,
|
||||
0,
|
||||
None,
|
||||
)
|
||||
if handle == w.INVALID_HANDLE_VALUE or handle in (0, None):
|
||||
raise Win32Fehler("CreateFileW", self._ctypes.get_last_error())
|
||||
return handle
|
||||
|
||||
def schliessen(self, handle) -> None:
|
||||
self._k32.CloseHandle(handle)
|
||||
|
||||
# ── Steuercode senden ───────────────────────────────────────────────
|
||||
def steuern(self, handle, code: int, eingabe: bytes = b"",
|
||||
ausgabe_bytes: int = 0) -> bytes:
|
||||
"""DeviceIoControl. Wirft Win32Fehler, gibt sonst die Antwort zurück."""
|
||||
ctypes = self._ctypes
|
||||
ein_puffer = ctypes.create_string_buffer(eingabe) if eingabe else None
|
||||
aus_puffer = ctypes.create_string_buffer(ausgabe_bytes) if ausgabe_bytes else None
|
||||
zurueck = self._wintypes.DWORD(0)
|
||||
erfolg = self._k32.DeviceIoControl(
|
||||
handle, code,
|
||||
ein_puffer, len(eingabe),
|
||||
aus_puffer, ausgabe_bytes,
|
||||
ctypes.byref(zurueck), None,
|
||||
)
|
||||
if not erfolg:
|
||||
raise Win32Fehler(f"DeviceIoControl({code:#x})", ctypes.get_last_error())
|
||||
return aus_puffer.raw[:zurueck.value] if aus_puffer else b""
|
||||
|
||||
|
||||
def _api(api):
|
||||
return api if api is not None else Win32()
|
||||
|
||||
|
||||
# ── Laufwerke finden ────────────────────────────────────────────────────
|
||||
def list_optical_devices(api=None) -> list:
|
||||
"""Alle optischen Laufwerke — als Gerätepfade wie r"\\\\.\\D:".
|
||||
|
||||
Kein Scan über alle 26 Buchstaben mit Öffnen-Versuch: `GetDriveTypeW`
|
||||
beantwortet die Frage ohne jeden Zugriff auf das Gerät. Ein Öffnen-Versuch
|
||||
auf einem leeren Laufwerk kann sonst mehrere Sekunden dauern (das Laufwerk
|
||||
fährt hoch), und das mal 26.
|
||||
"""
|
||||
api = _api(api)
|
||||
return sorted(
|
||||
w.geraetepfad(b) for b in api.laufwerksbuchstaben()
|
||||
if api.laufwerkstyp(b) == w.DRIVE_CDROM
|
||||
)
|
||||
|
||||
|
||||
# ── Zustand ─────────────────────────────────────────────────────────────
|
||||
def _medium_da(api, handle) -> bool:
|
||||
"""Liegt eine Disc ein? Wirft Win32Fehler bei allem außer NOT_READY."""
|
||||
try:
|
||||
api.steuern(handle, w.IOCTL_STORAGE_CHECK_VERIFY2)
|
||||
return True
|
||||
except Win32Fehler as e:
|
||||
if e.code in (w.ERROR_NOT_READY, w.ERROR_NO_MEDIA_IN_DRIVE):
|
||||
return False
|
||||
raise
|
||||
|
||||
|
||||
def drive_status(geraet: str, api=None) -> int:
|
||||
"""Wie `linux.detection.drive_status`: CDS_DISC_OK oder CDS_NO_DISC.
|
||||
|
||||
Wirft, wenn der Zustand NICHT feststellbar ist. Der Aufrufer muss
|
||||
entscheiden, was er damit macht — ein stilles „leer" wäre eine falsche
|
||||
Aussage über die Welt.
|
||||
"""
|
||||
api = _api(api)
|
||||
handle = api.oeffnen(geraet)
|
||||
try:
|
||||
return CDS_DISC_OK if _medium_da(api, handle) else CDS_NO_DISC
|
||||
finally:
|
||||
api.schliessen(handle)
|
||||
|
||||
|
||||
def disc_status(geraet: str, api=None) -> int:
|
||||
"""Audio- oder Datenträger — übersetzt in die CDS_*-Werte von cdrom.py.
|
||||
|
||||
Damit passt das Ergebnis in dieselbe `classify()`, die der Linux-Treiber
|
||||
benutzt: EINE Zuordnungslogik für beide Plattformen, und die ist getestet.
|
||||
"""
|
||||
api = _api(api)
|
||||
handle = api.oeffnen(geraet)
|
||||
try:
|
||||
antwort = api.steuern(handle, w.IOCTL_CDROM_DISK_TYPE, ausgabe_bytes=4)
|
||||
flags = int.from_bytes(antwort[:4], "little") if antwort else 0
|
||||
finally:
|
||||
api.schliessen(handle)
|
||||
if flags & w.CDROM_DISK_AUDIO_TRACK:
|
||||
return CDS_AUDIO
|
||||
if flags & w.CDROM_DISK_DATA_TRACK:
|
||||
return CDS_DATA_1
|
||||
return 0 # unbekannt -> classify() sagt "unknown"
|
||||
|
||||
|
||||
def disc_size_bytes(geraet: str, api=None) -> int:
|
||||
"""Größe des eingelegten Mediums — die Entsprechung von BLKGETSIZE64.
|
||||
|
||||
Sie entscheidet zwischen DVD, Blu-ray und 4K-UHD (Schwellen in cdrom.py).
|
||||
"""
|
||||
api = _api(api)
|
||||
handle = api.oeffnen(geraet)
|
||||
try:
|
||||
antwort = api.steuern(handle, w.IOCTL_DISK_GET_LENGTH_INFO, ausgabe_bytes=8)
|
||||
return int.from_bytes(antwort[:8], "little") if antwort else 0
|
||||
finally:
|
||||
api.schliessen(handle)
|
||||
|
||||
|
||||
def detect_disc_type(geraet: str, api=None) -> str:
|
||||
"""cd | dvd | bluray | uhd | no_disc | unknown."""
|
||||
api = _api(api)
|
||||
try:
|
||||
if drive_status(geraet, api) != CDS_DISC_OK:
|
||||
return "no_disc"
|
||||
return classify(disc_status(geraet, api), disc_size_bytes(geraet, api))
|
||||
except OSError:
|
||||
# „konnte nicht nachsehen" — ausdruecklich NICHT "no_disc".
|
||||
return "unknown"
|
||||
|
||||
|
||||
# ── Verriegeln ──────────────────────────────────────────────────────────
|
||||
def verriegeln(geraet: str, an: bool, api=None) -> None:
|
||||
"""Tür verriegeln (True) oder entriegeln (False).
|
||||
|
||||
`PREVENT_MEDIA_REMOVAL` ist eine Struktur mit genau einem BOOLEAN.
|
||||
"""
|
||||
api = _api(api)
|
||||
handle = api.oeffnen(geraet)
|
||||
try:
|
||||
api.steuern(handle, w.IOCTL_STORAGE_MEDIA_REMOVAL,
|
||||
eingabe=b"\x01" if an else b"\x00")
|
||||
finally:
|
||||
api.schliessen(handle)
|
||||
|
||||
|
||||
# ── Auswerfen ───────────────────────────────────────────────────────────
|
||||
def auswerfen_mit_grund(geraet: str, api=None, warten=None):
|
||||
"""Die gemeinsame Mechanik. Wirft NIE.
|
||||
|
||||
Rückgabe `(grund, fehler)` wie im Linux-Treiber. Reihenfolge ist
|
||||
dieselbe und aus demselben Grund: **entriegeln, auswerfen, nachsehen.**
|
||||
"""
|
||||
api = _api(api)
|
||||
if warten is None:
|
||||
import time
|
||||
warten = time.sleep
|
||||
|
||||
try:
|
||||
handle = api.oeffnen(geraet)
|
||||
except OSError as e:
|
||||
return AUSWURF_KEIN_ZUGRIFF, e
|
||||
|
||||
try:
|
||||
# Entriegeln zuerst. Scheitert es, wird der Auswurf trotzdem versucht —
|
||||
# bei einem nicht verriegelten Laufwerk klappt er ohnehin.
|
||||
try:
|
||||
api.steuern(handle, w.IOCTL_STORAGE_MEDIA_REMOVAL, eingabe=b"\x00")
|
||||
except OSError:
|
||||
pass
|
||||
|
||||
try:
|
||||
api.steuern(handle, w.IOCTL_STORAGE_EJECT_MEDIA)
|
||||
except OSError as e:
|
||||
return AUSWURF_ABGELEHNT, e
|
||||
|
||||
# NACHSEHEN. Der Befehl oben meldet nur, dass er angenommen wurde.
|
||||
for _ in range(AUSWURF_WARTEN_SEKUNDEN):
|
||||
try:
|
||||
if not _medium_da(api, handle):
|
||||
return AUSWURF_OK, None
|
||||
except OSError as e:
|
||||
return AUSWURF_ABGELEHNT, e
|
||||
warten(1)
|
||||
return AUSWURF_BLEIBT_DRIN, None
|
||||
finally:
|
||||
try:
|
||||
api.schliessen(handle)
|
||||
except OSError:
|
||||
pass
|
||||
|
||||
|
||||
def auswerfen_versuchen(geraet: str, api=None, warten=None) -> bool:
|
||||
"""Wirft die Disc aus und prüft nach. Wirft NIE — Vertrag des Workers."""
|
||||
grund, _ = auswerfen_mit_grund(geraet, api=api, warten=warten)
|
||||
return grund == AUSWURF_OK
|
||||
|
||||
|
||||
def eject(geraet: str, api=None) -> None:
|
||||
"""Wirft die Disc aus. Wirft OSError, wenn sie drin bleibt — Vertrag der API."""
|
||||
grund, fehler = auswerfen_mit_grund(geraet, api=api)
|
||||
if grund == AUSWURF_OK:
|
||||
return
|
||||
if fehler is not None:
|
||||
raise fehler
|
||||
if grund == AUSWURF_KEIN_ZUGRIFF:
|
||||
raise OSError(f"Das Laufwerk {geraet} ließ sich nicht ansprechen.")
|
||||
raise OSError(
|
||||
"Das Laufwerk hat den Auswurf angenommen, die Disc ist aber noch "
|
||||
"drin. Blockiert etwas die Schublade, oder läuft noch ein Zugriff?"
|
||||
)
|
||||
|
||||
|
||||
# ── Für das UI ──────────────────────────────────────────────────────────
|
||||
def device_info(geraet: str, api=None) -> dict:
|
||||
"""Der Geräte-Eintrag fürs UI — gleiche Felder wie beim Linux-Treiber.
|
||||
|
||||
`status` kennt hier drei Werte, nicht zwei: `ready`, `empty` und
|
||||
**`unknown`**. Der dritte ist Absicht — ein Laufwerk, das sich nicht
|
||||
ansprechen lässt (belegt, gerade angesteckt, defekt), ist nicht leer.
|
||||
"""
|
||||
api = _api(api)
|
||||
status = "unknown"
|
||||
disc_typ = "unknown"
|
||||
try:
|
||||
if drive_status(geraet, api) == CDS_DISC_OK:
|
||||
status = "ready"
|
||||
try:
|
||||
disc_typ = classify(disc_status(geraet, api),
|
||||
disc_size_bytes(geraet, api))
|
||||
except OSError:
|
||||
disc_typ = "unknown"
|
||||
else:
|
||||
status = "empty"
|
||||
except OSError:
|
||||
status = "unknown"
|
||||
|
||||
buchstabe = geraet.rstrip(":").rsplit("\\", 1)[-1].rstrip(":")
|
||||
return {
|
||||
"id": buchstabe,
|
||||
"name": f"Laufwerk {buchstabe}:",
|
||||
"type": disc_typ,
|
||||
"path": geraet,
|
||||
"status": status,
|
||||
"model": "",
|
||||
"serial": "",
|
||||
}
|
||||
Reference in New Issue
Block a user