WAS: win32.ts kennt QueryDosDeviceW (am 12.09.2026 an diesem PC gemessen: C: → \Device\HarddiskVolume7, X: → \Device\LanmanRedirector\…\192.168.178.62\rippy); LaufwerkApi.geraeteName liefert das Kernel-Gerät hinter dem Buchstaben. beurteilen() filtert damit auf DIESES Gerät; ohne Gerätename zählt nur noch der cdrom-Treiber (gehoertZuLaufwerk). Vorher nahm der XPath cdrom UND disk für alle Geräte — eine hustende Festplatte oder ein zweites Laufwerk erzeugte „VERDACHT: DIE HARDWARE" in der Kachel des Blu-ray- Laufwerks. WARUM: Durchsicht 12.09.2026, Fund F11 — mit Tests (Festplatten- Ereignisse, zweites Laufwerk, ruhig mit Gerätename). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
212 lines
8.1 KiB
TypeScript
212 lines
8.1 KiB
TypeScript
// Die Laufwerke (W-1, § 5 Schritt 1): Disc-Wache im 3-Sekunden-Takt,
|
|
// Auswerfen mit Nachsehen (R3), Audio-CD-Zugriffe, die Laufwerks-Gesundheit
|
|
// aus dem Windows-Protokoll (5.4.0) — und das nachgebaute Laufwerk des
|
|
// Klick-Beweises.
|
|
//
|
|
// koffi wird erst HIER geladen, über einen echten dynamischen import():
|
|
// Rollup legt win32.ts damit als eigenen Chunk neben kern.js (ein
|
|
// wörtliches require überlebte das Bündeln nicht — im Smoke-Lauf am
|
|
// 30.08.2026 sichtbar geworden). Scheitert das Laden (falsche Plattform,
|
|
// kaputtes Paket), läuft der Kern weiter und der Grund steht im Fenster (R4).
|
|
import type { GeraeteInfo, WacheEreignis } from '../../gemeinsam/nachrichten'
|
|
import type { KernKontext } from '../kontext'
|
|
import type { LaufwerkApi } from '../laufwerk/codes'
|
|
import {
|
|
audioSpurLesen,
|
|
audioToc,
|
|
auswerfenMitGrund,
|
|
geraeteInfo,
|
|
kennung,
|
|
listOptischeLaufwerke,
|
|
} from '../laufwerk/disc'
|
|
import { beurteilen, ereignisseLesen } from '../laufwerk/ereignisse'
|
|
import { DiscWache } from '../laufwerk/wache'
|
|
import type { Spur } from '../rip/audiocd'
|
|
|
|
const GESUNDHEIT_STUNDEN = 24
|
|
|
|
export interface LaufwerkHorcher {
|
|
discEingelegt(geraet: GeraeteInfo): void
|
|
discEntnommen(geraet: GeraeteInfo): void
|
|
}
|
|
|
|
export class LaufwerkDienst {
|
|
private api: LaufwerkApi | null = null
|
|
private wache: DiscWache | null = null
|
|
fehler: string | null = null
|
|
/** Das nachgebaute Laufwerk des Klick-Beweises (bau/klick-smoke.mjs): Ein
|
|
* Buchstabe, den es nicht gibt (Q) — die Disc-Merkmale finden dann
|
|
* nichts, und makemkvcon/HandBrake sind Node-Skripte (werkzeuge/aufruf.ts).
|
|
* Leer im normalen Betrieb; die Win32-Schicht bleibt dann der einzige Weg. */
|
|
readonly fake: string
|
|
readonly bereit: Promise<void>
|
|
|
|
constructor(
|
|
private readonly kontext: KernKontext,
|
|
private readonly horcher: LaufwerkHorcher,
|
|
/** Läuft auf diesem Laufwerk gerade ein Rip? (Auswurf wird dann abgelehnt.) */
|
|
private readonly ripLaeuft: (id: string) => boolean,
|
|
umgebung: NodeJS.ProcessEnv = process.env,
|
|
) {
|
|
this.fake = (umgebung['RIPPY_FAKE_LAUFWERK'] ?? '').replace(/:$/, '').toUpperCase()
|
|
this.bereit = this.starten()
|
|
}
|
|
|
|
private fakeGeraet(): GeraeteInfo {
|
|
return {
|
|
id: this.fake,
|
|
name: `Laufwerk ${this.fake}:`,
|
|
typ: 'bluray',
|
|
pfad: `\\\\.\\${this.fake}:`,
|
|
status: 'ready',
|
|
modell: 'NACHGEBAUT (KLICK-BEWEIS)',
|
|
seriennummer: 'KLICK',
|
|
groesseBytes: 33_759_100_928,
|
|
grund: '',
|
|
}
|
|
}
|
|
|
|
private wacheMeldung(meldung: { laufwerke: GeraeteInfo[]; ereignisse: WacheEreignis[] }): void {
|
|
this.kontext.anHauptUndFenster({ art: 'laufwerke', liste: meldung.laufwerke, ereignisse: meldung.ereignisse })
|
|
for (const ereignis of meldung.ereignisse) {
|
|
if (ereignis.art === 'disc-eingelegt') {
|
|
this.kontext.protokoll.schreiben(`Disc eingelegt in ${ereignis.geraet.id}: ${ereignis.geraet.typ}, ${ereignis.geraet.groesseBytes} Bytes`)
|
|
this.horcher.discEingelegt(ereignis.geraet)
|
|
} else if (ereignis.art === 'disc-entnommen') {
|
|
this.kontext.protokoll.schreiben(`Disc entnommen aus ${ereignis.geraet.id}`)
|
|
this.horcher.discEntnommen(ereignis.geraet)
|
|
} else if (ereignis.art === 'laufwerk-weg') {
|
|
this.kontext.protokoll.schreiben(`Laufwerk ${ereignis.id} nicht mehr da`)
|
|
}
|
|
}
|
|
}
|
|
|
|
private async starten(): Promise<void> {
|
|
const wacheFehler = (fehler: unknown): void => {
|
|
// R4: Eine gescheiterte Runde ist eine laute Zeile — der letzte
|
|
// Stand im Fenster bleibt stehen (R2), die Wache läuft weiter.
|
|
this.kontext.fehler(`Disc-Wache: ${String(fehler)}`)
|
|
}
|
|
try {
|
|
if (this.fake.length > 0) {
|
|
this.wache = new DiscWache(() => [this.fakeGeraet()], (m) => this.wacheMeldung(m), wacheFehler)
|
|
this.wache.starten()
|
|
return
|
|
}
|
|
if (process.platform !== 'win32') {
|
|
throw new Error('Die Laufwerks-Schicht läuft nur unter Windows (KONZEPT § 14).')
|
|
}
|
|
const { win32Api } = await import('../laufwerk/win32')
|
|
this.api = win32Api()
|
|
this.wache = new DiscWache(
|
|
() => listOptischeLaufwerke(this.api!).map((g) => geraeteInfo(this.api!, g)),
|
|
(m) => this.wacheMeldung(m),
|
|
wacheFehler,
|
|
)
|
|
this.wache.starten()
|
|
} catch (fehler) {
|
|
this.fehler = String(fehler)
|
|
this.kontext.protokoll.schreiben(`Laufwerke: ${this.fehler}`)
|
|
console.error(`[kern] Laufwerke: ${this.fehler}`)
|
|
}
|
|
}
|
|
|
|
aktuellerStand(): GeraeteInfo[] {
|
|
return this.wache?.aktuellerStand() ?? []
|
|
}
|
|
|
|
/** Der Geräte-Name zu einer Kennung — oder null. */
|
|
geraetVon(id: string): string | null {
|
|
if (this.api === null) return null
|
|
return listOptischeLaufwerke(this.api).find((g) => kennung(g) === id) ?? null
|
|
}
|
|
|
|
/** Der Geräte-Stand zu einer Kennung — echt oder nachgebaut. */
|
|
geraetInfoVon(id: string): GeraeteInfo | null {
|
|
if (this.fake.length > 0) return id === this.fake ? this.fakeGeraet() : null
|
|
const geraet = this.geraetVon(id)
|
|
if (geraet === null || this.api === null) return null
|
|
return geraeteInfo(this.api, geraet)
|
|
}
|
|
|
|
audioToc(id: string): Spur[] {
|
|
const geraet = this.geraetVon(id)
|
|
if (this.api === null || geraet === null) throw new Error(`Laufwerk ${id}: gibt es nicht`)
|
|
return audioToc(this.api, geraet)
|
|
}
|
|
|
|
audioSpurLesen(
|
|
id: string,
|
|
start: number,
|
|
sektoren: number,
|
|
schreiben: (daten: Buffer) => void,
|
|
fortschritt?: (anteil: number) => void,
|
|
): number {
|
|
const geraet = this.geraetVon(id)
|
|
if (this.api === null || geraet === null) throw new Error(`Laufwerk ${id}: gibt es nicht`)
|
|
return audioSpurLesen(this.api, geraet, start, sektoren, schreiben, fortschritt)
|
|
}
|
|
|
|
async auswerfen(id: string): Promise<void> {
|
|
await this.bereit
|
|
if (this.ripLaeuft(id)) {
|
|
this.kontext.anFenster({
|
|
art: 'auswurf-ergebnis',
|
|
id,
|
|
grund: 'abgelehnt',
|
|
fehler: `Auf Laufwerk ${id} läuft ein Rip — erst abbrechen oder abwarten.`,
|
|
})
|
|
return
|
|
}
|
|
if (this.api === null) {
|
|
this.kontext.anFenster({ art: 'auswurf-ergebnis', id, grund: 'kein-zugriff', fehler: this.fehler })
|
|
return
|
|
}
|
|
const geraet = this.geraetVon(id)
|
|
if (geraet === null) {
|
|
this.kontext.anFenster({ art: 'auswurf-ergebnis', id, grund: 'kein-zugriff', fehler: `Laufwerk ${id}: gibt es nicht` })
|
|
return
|
|
}
|
|
const { grund, fehler } = await auswerfenMitGrund(this.api, geraet)
|
|
this.kontext.protokoll.schreiben(`Auswurf ${id}: ${grund}${fehler !== null ? ` (${fehler})` : ''}`)
|
|
this.kontext.anFenster({ art: 'auswurf-ergebnis', id, grund, fehler })
|
|
// Die Wache merkt den neuen Zustand spätestens in der nächsten Runde;
|
|
// eine sofortige Runde macht das Fenster ohne 3-Sekunden-Wartezeit frisch.
|
|
this.wache?.runde()
|
|
}
|
|
|
|
// ── Laufwerks-Gesundheit (5.4.0, Punkt 8) ─────────────────────────────
|
|
async pruefen(id: string): Promise<void> {
|
|
if (process.platform !== 'win32') return
|
|
const ergebnis = await ereignisseLesen(GESUNDHEIT_STUNDEN)
|
|
if ('fehler' in ergebnis) {
|
|
this.kontext.anFenster({
|
|
art: 'laufwerk-gesundheit',
|
|
id,
|
|
bericht: {
|
|
urteil: 'nicht-lesbar',
|
|
text: `Das Windows-Protokoll ließ sich nicht lesen: ${ergebnis.fehler}`,
|
|
fehlerhafteBloecke: 0,
|
|
controllerfehler: 0,
|
|
sonstige: 0,
|
|
letzte: [],
|
|
stunden: GESUNDHEIT_STUNDEN,
|
|
},
|
|
})
|
|
return
|
|
}
|
|
// 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 })
|
|
}
|
|
}
|