Files
rippy/rippy-windows/src/kern/dienste/laufwerke.ts
T
HitonabiandClaude Fable 5.1 3dc6798c79 feat(v5): 5.5.0 — Kern und Oberfläche aufgeteilt, Übersicht und Einstellungen neu, Erst-Einrichtung mit Poster-Kulisse
Commander-Wünsche vom 02.09.2026 vor der Freigabe von 5.4.0 (die deshalb nie
veröffentlicht wurde):

Kern: kern/index.ts ist nur noch Bootstrap und Router; Dienste in
kern/dienste/ (Einstellungen, Werkzeuge, Laufwerke, Erkennung, Vorgänge,
Speicher, Benachrichtigung, Automatik), Kontext in kern/kontext.ts,
Protokoll-Datei in gemeinsam/protokoll.ts, Platz-Rechnung in gemeinsam/platz.ts.

Oberfläche: fenster/App.tsx ist nur noch der Rahmen; bausteine.tsx, dialoge/,
uebersicht/ (LaufwerkKachel, KompressionsKarte, SpeicherKarte, VerlaufKarte),
einstellungen/ (Karten je Funktion, SprachAuswahl), einrichtung/
(ErstEinrichtung, PosterHintergrund).

Neu: Bibliothek weg, dafür Verlauf; Speicher-und-Bilanz-Karte (statfs);
LED-Fußleiste; MakeMKV-Kopf in die Einstellungen; Sprach-Chips mit Dropdown
statt Allgemeinem Preset; Karten Automatik/Benachrichtigung/Allgemein/Update;
Durchsuchen bei Werkzeugen; Arbeitsordner lokal (Encode dort, dann in die
Ablage); Platz-Prüfung vor dem Rip (Dialog und Kern); Automatik mit Countdown;
Serien-Merker über Discs; Rettungs-Abbild mit Nullen nur nach gesehenen
Lesefehlern (kern/rip/rettung.ts, Phase rettet, iso:-Quelle); Warteschlange
pausieren; Discord/Telegram-Benachrichtigung; Protokoll-Datei je Tag;
Fensterlage 80 % mittig, gemerkt; Erst-Einrichtung in acht Schritten vor dem
Poster-Grid.

Beweise: 413 Tests (neu: speicher, benachrichtigung, automatik, rettung,
fensterlage, protokoll, kleinkram-550), drei Typprüfungen, Klick-Beweis mit
29 Schritten inklusive Erst-Einrichtung (zweiter Start mit
RIPPY_START_BEREICH=einrichtung), Bilder beweise/klick/01…09.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-02 17:51:15 +02:00

203 lines
7.7 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
}
const bericht = beurteilen(ergebnis.ereignisse, GESUNDHEIT_STUNDEN)
this.kontext.protokoll.schreiben(`Laufwerk ${id} geprüft: ${bericht.urteil}${bericht.text}`)
this.kontext.anFenster({ art: 'laufwerk-gesundheit', id, bericht })
}
}