// Laufwerks-Gesundheit aus dem Windows-Ereignisprotokoll (5.4.0). // // Der Anlass (01.09.2026): Beim Spartacus-Defekt und beim 5042-Zustand // des BU40N war die entscheidende Frage „Disc oder Hardware?" — und die // Antwort stand im System-Protokoll, von Hand gelesen. Jetzt liest Rippy // sie selbst. // // GEMESSEN (01.09.2026, Regel D, an diesem PC): // wevtutil qe System "/q:" /f:xml /c:200 /rd:true // Provider 'cdrom', EventID 7 → „Fehlerhafter Block bei Gerät \Device\CdRom0." // EventID 11 → „Der Treiber hat einen Controllerfehler auf \Device\CdRom0 gefunden." // EventID 51 → Auslagerungs-/E/A-Fehler (bei 7 dabei) // In 30 Tagen: 338 × 7, 8 × 11, 46 × 51 — die 7er häuften sich exakt um // den Spartacus-Rip (20:32), die 11er beim USB-Aussetzer (20:17). // TimeCreated SystemTime ist UTC. Die XML-Ereignisse stehen OHNE // Zeilenumbruch hintereinander; ein Regex je reicht. // Die Argumente gehen ohne Shell an wevtutil — ein „/q:"-Argument mit // Leerzeichen bleibt so EIN Argument (über Git Bash zerlegt es der // Pfad-Konverter, über spawn nicht). import { spawn } from 'node:child_process' import type { LaufwerkBericht } from '../../gemeinsam/nachrichten' import { textVon } from '../rip/parser' export interface LaufwerkEreignis { /** ISO-Zeit (UTC). */ zeit: string provider: string id: number geraet: string } /** Die Ereignis-IDs des cdrom-/disk-Treibers, wie am PC gemessen. */ export const EREIGNIS_TEXTE: Record = { 7: 'fehlerhafter Block', 11: 'Controllerfehler', 15: 'Gerät nicht bereit', 51: 'E/A-Fehler beim Lesen', 153: 'E/A-Vorgang wiederholt', } export function xpathFuer(stunden: number): string { const ms = Math.max(1, Math.round(stunden)) * 60 * 60 * 1000 return `*[System[Provider[@Name='cdrom' or @Name='disk'] and TimeCreated[timediff(@SystemTime) <= ${ms}]]]` } export function buildWevtutilArgs(stunden: number, hoechstens = 200): string[] { return ['qe', 'System', `/q:${xpathFuer(stunden)}`, '/f:xml', `/c:${hoechstens}`, '/rd:true'] } /** Die XML-Ausgabe von wevtutil → Ereignisse (pur, testbar). */ export function ereignisseAusXml(xml: string): LaufwerkEreignis[] { const liste: LaufwerkEreignis[] = [] for (const treffer of xml.matchAll(//g)) { const block = treffer[0] const provider = /]*>(\d+)<\/EventID>/.exec(block)?.[1] ?? '') const zeit = /([^<]*)<\/Data>/.exec(block)?.[1] ?? '' if (!Number.isInteger(id) || provider.length === 0) continue liste.push({ zeit, provider, id, geraet }) } return liste } /** 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()) let fehlerhafteBloecke = 0 let controllerfehler = 0 let sonstige = 0 for (const e of relevant) { if (e.id === 7) fehlerhafteBloecke += 1 else if (e.id === 11 || e.id === 15) controllerfehler += 1 else sonstige += 1 } const letzte = relevant.slice(0, 8).map((e) => { const lokal = e.zeit.length > 0 ? new Date(e.zeit) : null const zeit = lokal !== null && !Number.isNaN(lokal.getTime()) ? `${lokal.getFullYear()}-${String(lokal.getMonth() + 1).padStart(2, '0')}-${String(lokal.getDate()).padStart(2, '0')} ${String(lokal.getHours()).padStart(2, '0')}:${String(lokal.getMinutes()).padStart(2, '0')}` : '?' return `${zeit} · ${e.provider} ${e.id} · ${EREIGNIS_TEXTE[e.id] ?? 'Ereignis'} (${e.geraet})` }) const zeitraum = seit === undefined ? `in den letzten ${stunden} Stunden` : `seit ${seit.toISOString().slice(11, 16)} UTC` if (relevant.length === 0) { return { urteil: 'ruhig', text: `Das Windows-Protokoll meldet ${zeitraum} nichts zum Laufwerk — weder Lesefehler noch Hardware-Fehler.`, fehlerhafteBloecke, controllerfehler, sonstige, letzte, stunden, } } if (controllerfehler > 0) { return { urteil: 'hardware', text: `${controllerfehler} Controllerfehler ${zeitraum}` + (fehlerhafteBloecke > 0 ? ` und ${fehlerhafteBloecke} fehlerhafte Blöcke` : '') + '. Ein Controllerfehler kommt vom Laufwerk oder seiner Verbindung, nicht von der Disc: ' + 'Bei einem USB-Laufwerk zuerst einen Port direkt am Mainboard probieren, Kabel und Netzteil prüfen.', fehlerhafteBloecke, controllerfehler, sonstige, letzte, stunden, } } if (fehlerhafteBloecke > 0) { return { urteil: 'disc', text: `${fehlerhafteBloecke} fehlerhafte Blöcke ${zeitraum}, keine Controllerfehler. ` + 'Das spricht für die DISC (Kratzer, Fertigungsfehler), nicht für das Laufwerk: ' + 'Disc reinigen, den betroffenen Titel überspringen oder eine andere Ausgabe versuchen.', fehlerhafteBloecke, controllerfehler, sonstige, letzte, stunden, } } return { urteil: 'unklar', text: `${sonstige} Laufwerks-Ereignisse ${zeitraum}, aber weder fehlerhafte Blöcke noch Controllerfehler — kein klares Bild.`, fehlerhafteBloecke, controllerfehler, sonstige, letzte, stunden, } } /** Liest das Protokoll — null heißt „konnte nicht lesen", mit Grund. */ export function ereignisseLesen( stunden: number, timeoutMs = 30_000, ): Promise<{ ereignisse: LaufwerkEreignis[] } | { fehler: string }> { return new Promise((fertig) => { let kind: ReturnType try { kind = spawn('wevtutil', buildWevtutilArgs(stunden), { windowsHide: true, stdio: ['ignore', 'pipe', 'pipe'] }) } catch (fehler) { fertig({ fehler: `wevtutil nicht startbar: ${String(fehler)}` }) return } const stuecke: Buffer[] = [] const fehlerStuecke: Buffer[] = [] kind.stdout?.on('data', (s: Buffer) => stuecke.push(s)) kind.stderr?.on('data', (s: Buffer) => fehlerStuecke.push(s)) const zeiger = setTimeout(() => kind.kill(), timeoutMs) kind.on('error', (fehler) => { clearTimeout(zeiger) fertig({ fehler: `wevtutil nicht startbar: ${String(fehler)}` }) }) kind.on('close', (code) => { clearTimeout(zeiger) const xml = textVon(Buffer.concat(stuecke)) // Ohne Treffer endet wevtutil mit 0 und leerer Ausgabe; ein Fehler // steht auf stderr mit Code ≠ 0. if (code !== 0 && xml.trim().length === 0) { fertig({ fehler: `wevtutil endete mit Code ${code}: ${textVon(Buffer.concat(fehlerStuecke)).trim().slice(0, 200)}` }) return } fertig({ ereignisse: ereignisseAusXml(xml) }) }) }) }