diff --git a/rippy-windows/src/kern/dienste/laufwerke.ts b/rippy-windows/src/kern/dienste/laufwerke.ts index 1d68c52..7f79bf1 100644 --- a/rippy-windows/src/kern/dienste/laufwerke.ts +++ b/rippy-windows/src/kern/dienste/laufwerke.ts @@ -195,7 +195,16 @@ export class LaufwerkDienst { }) return } - const bericht = beurteilen(ergebnis.ereignisse, GESUNDHEIT_STUNDEN) + // 5.7.0 (F11): nur die Ereignisse DIESES Geräts — \Device\CdRomN + // hinter dem Buchstaben, nicht die einer Festplatte oder eines zweiten + // Laufwerks. Ohne Gerätename (Nachbau, Fehlschlag) zählt nur cdrom. + let geraet = '' + try { + geraet = this.api?.geraeteName(id) ?? '' + } catch (fehler) { + this.kontext.protokoll.schreiben(`Laufwerk ${id}: Gerätename nicht ermittelbar (${String(fehler)})`) + } + const bericht = beurteilen(ergebnis.ereignisse, GESUNDHEIT_STUNDEN, undefined, geraet) this.kontext.protokoll.schreiben(`Laufwerk ${id} geprüft: ${bericht.urteil} — ${bericht.text}`) this.kontext.anFenster({ art: 'laufwerk-gesundheit', id, bericht }) } diff --git a/rippy-windows/src/kern/laufwerk/codes.ts b/rippy-windows/src/kern/laufwerk/codes.ts index 537d549..da9e45f 100644 --- a/rippy-windows/src/kern/laufwerk/codes.ts +++ b/rippy-windows/src/kern/laufwerk/codes.ts @@ -84,6 +84,9 @@ export class Win32Fehler extends Error { export interface LaufwerkApi { laufwerksbuchstaben(): string[] laufwerkstyp(buchstabe: string): number + /** Kernel-Gerätename hinter dem Buchstaben (\Device\CdRom0) — '' wenn + * nicht zu ermitteln (5.7.0, F11). */ + geraeteName(buchstabe: string): string /** Öffnet das Gerät. `zugriff` 0 fragt das GERÄT, GENERIC_READ das MEDIUM — * bei gestörter Disc scheitert das zweite und das erste geht weiter * (rc11-Befund, in beweise/laufwerk.js nachgemessen). Wirft Win32Fehler. */ diff --git a/rippy-windows/src/kern/laufwerk/ereignisse.ts b/rippy-windows/src/kern/laufwerk/ereignisse.ts index 954e1a3..0a5e204 100644 --- a/rippy-windows/src/kern/laufwerk/ereignisse.ts +++ b/rippy-windows/src/kern/laufwerk/ereignisse.ts @@ -62,10 +62,24 @@ export function ereignisseAusXml(xml: string): LaufwerkEreignis[] { return liste } +/** Gehört dieses Ereignis zu DIESEM Laufwerk? (pur, 5.7.0 — Durchsicht F11) + * Mit bekanntem Kernel-Gerät (\Device\CdRom0) zählt nur, was dieses + * Gerät nennt; ohne Gerätename zählt nur der cdrom-Treiber — nie mehr die + * Festplatten (Provider disk), die vorher ein „VERDACHT: DIE HARDWARE" + * für das Blu-ray-Laufwerk auslösten. */ +export function gehoertZuLaufwerk(e: LaufwerkEreignis, geraet: string): boolean { + if (geraet.length > 0) return e.geraet.toLowerCase().includes(geraet.toLowerCase()) + return e.provider === 'cdrom' +} + /** Urteil aus den Ereignissen (pur). `seit` grenzt auf einen Zeitraum ein - * (z. B. den laufenden Rip); ohne `seit` zählt alles, was da ist. */ -export function beurteilen(ereignisse: readonly LaufwerkEreignis[], stunden: number, seit?: Date): LaufwerkBericht { - const relevant = seit === undefined ? [...ereignisse] : ereignisse.filter((e) => new Date(e.zeit).getTime() >= seit.getTime()) + * (z. B. den laufenden Rip); ohne `seit` zählt alles, was da ist. + * `geraet` ist der Kernel-Gerätename des Laufwerks (\Device\CdRom0), + * '' wenn unbekannt — dann zählen alle cdrom-Ereignisse. */ +export function beurteilen(ereignisse: readonly LaufwerkEreignis[], stunden: number, seit?: Date, geraet = ''): LaufwerkBericht { + const relevant = ereignisse.filter( + (e) => gehoertZuLaufwerk(e, geraet) && (seit === undefined || new Date(e.zeit).getTime() >= seit.getTime()), + ) let fehlerhafteBloecke = 0 let controllerfehler = 0 let sonstige = 0 @@ -87,7 +101,7 @@ export function beurteilen(ereignisse: readonly LaufwerkEreignis[], stunden: num if (relevant.length === 0) { return { urteil: 'ruhig', - text: `Das Windows-Protokoll meldet ${zeitraum} nichts zum Laufwerk — weder Lesefehler noch Hardware-Fehler.`, + text: `Das Windows-Protokoll meldet ${zeitraum} nichts zu${geraet.length > 0 ? ` ${geraet}` : 'm Laufwerk'} — weder Lesefehler noch Hardware-Fehler.`, fehlerhafteBloecke, controllerfehler, sonstige, diff --git a/rippy-windows/src/kern/laufwerk/win32.ts b/rippy-windows/src/kern/laufwerk/win32.ts index 393da73..1cd5b3c 100644 --- a/rippy-windows/src/kern/laufwerk/win32.ts +++ b/rippy-windows/src/kern/laufwerk/win32.ts @@ -24,6 +24,15 @@ export function win32Api(): LaufwerkApi { ) const CloseHandle = kernel32.func('bool __stdcall CloseHandle(void *hObject)') const GetLastError = kernel32.func('uint32_t __stdcall GetLastError()') + // 5.7.0 (Durchsicht F11): Welches Kernel-Gerät steckt hinter G:? — + // QueryDosDeviceW liefert \Device\CdRom0 (learn.microsoft.com, + // fileapi/nf-fileapi-querydosdevicew: Ziel ist eine Mehrfach-Zeichenkette, + // der erste Eintrag zählt; am 12.09.2026 an C: dieses PCs gemessen). + // Damit zählt die Laufwerks-Gesundheit nur noch die Ereignisse DIESES + // Laufwerks, nicht die einer hustenden Festplatte. + const QueryDosDeviceW = kernel32.func( + 'uint32_t __stdcall QueryDosDeviceW(str16 lpDeviceName, _Out_ char16_t *lpTargetPath, uint32_t ucchMax)', + ) const DeviceIoControl = kernel32.func( 'bool __stdcall DeviceIoControl(void *hDevice, uint32_t dwIoControlCode, void *lpInBuffer, uint32_t nInBufferSize, _Out_ void *lpOutBuffer, uint32_t nOutBufferSize, _Out_ uint32_t *lpBytesReturned, void *lpOverlapped)', ) @@ -65,6 +74,16 @@ export function win32Api(): LaufwerkApi { CloseHandle(handle) }, + geraeteName(buchstabe: string): string { + const ziel = Buffer.alloc(2 * 1024) + const laenge = QueryDosDeviceW(`${buchstabe.replace(/:$/, '')}:`, ziel, 1024) as number + if (laenge === 0) return '' + // UTF-16LE, die erste Zeichenkette bis zum ersten NUL. + const text = ziel.toString('utf16le', 0, laenge * 2) + const ende = text.indexOf('\0') + return (ende >= 0 ? text.slice(0, ende) : text).trim() + }, + steuern(handle: unknown, code: number, eingabe?: Buffer | null, ausgabeBytes = 0): Buffer { const aus = ausgabeBytes > 0 ? Buffer.alloc(ausgabeBytes) : null const zurueck = [0] diff --git a/rippy-windows/test/ereignisse-570.test.ts b/rippy-windows/test/ereignisse-570.test.ts new file mode 100644 index 0000000..9214785 --- /dev/null +++ b/rippy-windows/test/ereignisse-570.test.ts @@ -0,0 +1,53 @@ +// 5.7.0 (Durchsicht 12.09.2026, F11): Die Laufwerks-Gesundheit zählt nur die +// Ereignisse DIESES Laufwerks. Vorher nahm der XPath Provider cdrom UND +// disk für ALLE Geräte — eine hustende Festplatte (\Device\Harddisk1) +// erzeugte „VERDACHT: DIE HARDWARE" in der Kachel des Blu-ray-Laufwerks. +import { describe, expect, it } from 'vitest' +import { beurteilen, gehoertZuLaufwerk, type LaufwerkEreignis } from '../src/kern/laufwerk/ereignisse' + +const e = (provider: string, id: number, geraet: string): LaufwerkEreignis => ({ + zeit: '2026-09-02T18:32:00.000Z', + provider, + id, + geraet, +}) + +const PLATTE = e('disk', 11, '\\Device\\Harddisk1\\DR1') +const PLATTE_BLOCK = e('disk', 7, '\\Device\\Harddisk1\\DR1') +const CDROM0 = e('cdrom', 7, '\\Device\\CdRom0') +const CDROM1 = e('cdrom', 11, '\\Device\\CdRom1') + +describe('gehoertZuLaufwerk', () => { + it('mit Gerätename zählt nur dieses Gerät', () => { + expect(gehoertZuLaufwerk(CDROM0, '\\Device\\CdRom0')).toBe(true) + expect(gehoertZuLaufwerk(CDROM1, '\\Device\\CdRom0')).toBe(false) + expect(gehoertZuLaufwerk(PLATTE, '\\Device\\CdRom0')).toBe(false) + // Groß/Klein ist Windows egal. + expect(gehoertZuLaufwerk(CDROM0, '\\device\\cdrom0')).toBe(true) + }) + it('ohne Gerätename zählt nur der cdrom-Treiber — nie eine Festplatte', () => { + expect(gehoertZuLaufwerk(CDROM0, '')).toBe(true) + expect(gehoertZuLaufwerk(CDROM1, '')).toBe(true) + expect(gehoertZuLaufwerk(PLATTE, '')).toBe(false) + }) +}) + +describe('beurteilen je Gerät', () => { + it('Controllerfehler der Festplatte beschuldigen nicht das Laufwerk', () => { + const bericht = beurteilen([PLATTE, PLATTE_BLOCK, CDROM0], 24, undefined, '\\Device\\CdRom0') + expect(bericht.urteil).toBe('disc') + expect(bericht.controllerfehler).toBe(0) + expect(bericht.fehlerhafteBloecke).toBe(1) + expect(bericht.letzte).toHaveLength(1) + }) + it('ein zweites optisches Laufwerk zählt nicht mit', () => { + const bericht = beurteilen([CDROM1, CDROM0], 24, undefined, '\\Device\\CdRom0') + expect(bericht.urteil).toBe('disc') + expect(bericht.controllerfehler).toBe(0) + }) + it('nur Festplatten-Ereignisse → für das Laufwerk ist es ruhig, mit Gerätename im Text', () => { + const bericht = beurteilen([PLATTE, PLATTE_BLOCK], 24, undefined, '\\Device\\CdRom0') + expect(bericht.urteil).toBe('ruhig') + expect(bericht.text).toContain('\\Device\\CdRom0') + }) +}) diff --git a/rippy-windows/test/laufwerk.test.ts b/rippy-windows/test/laufwerk.test.ts index 6fdf7f9..2f188e6 100644 --- a/rippy-windows/test/laufwerk.test.ts +++ b/rippy-windows/test/laufwerk.test.ts @@ -119,6 +119,7 @@ function fakeApi(verhalten: FakeVerhalten): LaufwerkApi & { protokoll: string[] protokoll, laufwerksbuchstaben: () => verhalten.buchstaben ?? ['C', 'G'], laufwerkstyp: (b) => verhalten.typen?.[b] ?? (b === 'G' ? DRIVE_CDROM : 3), + geraeteName: (b) => (b === 'G' ? 'GERAET-CDROM0' : ''), oeffnen(pfad, zugriff) { protokoll.push(`oeffnen ${pfad} zugriff=${zugriff}`) if (verhalten.oeffnenFehler !== undefined) throw new Win32Fehler('CreateFileW', verhalten.oeffnenFehler)