// Der Kern — läuft als Electron utilityProcess (KONZEPT-WINDOWS.md § 4.1). // Hier passiert die ganze Arbeit; das Fenster zeigt nur an. Stand W-1: // Datenbank (node:sqlite, § 3.4), Disc-Wache im 3-Sekunden-Takt (§ 5), // Auswerfen mit Nachsehen (R3), Ping/Pong über den direkten MessagePort. // // parentPort/MessagePortMain: Schnittstelle laut Electron-Doku »utilityProcess« // und »MessagePorts in Electron« — dieselbe Verdrahtung wie im gemessenen // Beweis beweise/sqlite-main.js. import type { MessagePortMain } from 'electron' import { istFensterNachricht, istHauptNachricht, type DbStatus, type KernNachricht, } from '../gemeinsam/nachrichten' import type { LaufwerkApi } from './laufwerk/codes' import { auswerfenMitGrund, geraeteInfo, kennung, listOptischeLaufwerke } from './laufwerk/disc' import { DiscWache } from './laufwerk/wache' import { Datenbank } from './speicher/db' // --datenbank= kommt vom Haupt (der Kern kennt Electrons app.getPath // nicht — er ist ein reiner Node-Prozess). function datenbankPfadAusArgv(argv: readonly string[]): string | null { for (const arg of argv) { if (arg.startsWith('--datenbank=')) { const pfad = arg.slice('--datenbank='.length) if (pfad.length > 0) return pfad } } return null } let db: Datenbank | null = null let dbStatus: DbStatus const pfad = datenbankPfadAusArgv(process.argv) if (pfad === null) { dbStatus = { ok: false, fehler: 'Kein --datenbank= übergeben' } } else { try { db = new Datenbank(pfad) // Selbsttest: Schreiben und Zurücklesen — »geöffnet« allein ist kein Beweis. db.einstellungSchreiben('letzter_start', new Date().toISOString()) if (db.einstellungLesen('letzter_start') === null) { dbStatus = { ok: false, fehler: 'Selbsttest: Geschriebenes kam nicht zurück' } } else { dbStatus = { ok: true, pfad } } } catch (fehler) { // R4: Der Fehler wird GEMELDET (im Fenster sichtbar), nicht verschluckt. // Der Kern läuft ohne Datenbank weiter — »ich weiß es nicht« darf nie zu // »es geht nicht« werden (§ 6.2, sinngemäß). dbStatus = { ok: false, fehler: String(fehler) } } } let fensterPort: MessagePortMain | null = null function anFenster(nachricht: KernNachricht): void { fensterPort?.postMessage(nachricht) } function anHauptUndFenster(nachricht: KernNachricht): void { process.parentPort.postMessage(nachricht) anFenster(nachricht) } // ── Laufwerke (W-1) ───────────────────────────────────────────────────── // 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). let laufwerkApi: LaufwerkApi | null = null let laufwerkFehler: string | null = null let wache: DiscWache | null = null const laufwerkeBereit: Promise = (async () => { try { if (process.platform !== 'win32') { throw new Error('Die Laufwerks-Schicht läuft nur unter Windows (KONZEPT § 14).') } const { win32Api } = await import('./laufwerk/win32') laufwerkApi = win32Api() wache = new DiscWache( () => listOptischeLaufwerke(laufwerkApi!).map((g) => geraeteInfo(laufwerkApi!, g)), (meldung) => { anHauptUndFenster({ art: 'laufwerke', liste: meldung.laufwerke, ereignisse: meldung.ereignisse }) }, (fehler) => { // R4: Eine gescheiterte Runde ist eine laute Zeile — der letzte // Stand im Fenster bleibt stehen (R2), die Wache läuft weiter. console.error(`[kern] Disc-Wache: Runde fehlgeschlagen: ${String(fehler)}`) anFenster({ art: 'kern-fehler', text: `Disc-Wache: ${String(fehler)}` }) }, ) wache.starten() } catch (fehler) { laufwerkFehler = String(fehler) console.error(`[kern] Laufwerke: ${laufwerkFehler}`) } })() async function auswerfen(id: string): Promise { await laufwerkeBereit if (laufwerkApi === null) { anFenster({ art: 'auswurf-ergebnis', id, grund: 'kein-zugriff', fehler: laufwerkFehler }) return } const geraete = listOptischeLaufwerke(laufwerkApi) const geraet = geraete.find((g) => kennung(g) === id) if (geraet === undefined) { anFenster({ art: 'auswurf-ergebnis', id, grund: 'kein-zugriff', fehler: `Laufwerk ${id}: gibt es nicht` }) return } const { grund, fehler } = await auswerfenMitGrund(laufwerkApi, geraet) 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. wache?.runde() } process.parentPort.on('message', (ereignis) => { const nachricht: unknown = ereignis.data if (istHauptNachricht(nachricht) && nachricht.art === 'fenster-port') { // Das Haupt reicht einen frischen Port zum Fenster durch. Ein neues // Fenster bringt einen neuen Port — der alte wird geschlossen. fensterPort?.close() fensterPort = ereignis.ports[0] ?? null if (fensterPort === null) return fensterPort.on('message', (portEreignis) => { const frage: unknown = portEreignis.data if (!istFensterNachricht(frage)) return if (frage.art === 'ping') { anFenster({ art: 'pong', zeit: frage.zeit }) } else if (frage.art === 'auswerfen') { void auswerfen(frage.id) } }) fensterPort.start() // Damit das Fenster nach dem Verbinden sofort den Stand kennt: anFenster(bereitNachricht()) void laufwerkeBereit.then(() => { if (laufwerkFehler !== null) { anFenster({ art: 'kern-fehler', text: `Laufwerke: ${laufwerkFehler}` }) } anFenster({ art: 'laufwerke', liste: wache?.aktuellerStand() ?? [], ereignisse: [] }) }) } }) function bereitNachricht(): KernNachricht { return { art: 'kern-bereit', pid: process.pid, nodeVersion: process.versions.node, datenbank: dbStatus, } } process.parentPort.postMessage(bereitNachricht())