Ampel / ampel (push) Successful in 1m21s
kern/laufwerk in drei Schichten: codes.ts (Steuercodes HERGELEITET wie win_ioctl.py, gegen die Doku-Zahlen getestet), win32.ts (der einzige koffi-Ort im Kern, Muster aus beweise/laufwerk.js), disc.ts (Logik gegen die LaufwerkApi-Schnittstelle — classify mit den cdrom.py-Schwellen, Geraete-Info mit ready/empty/unknown + Klartext-Grund, Auswurf als entriegeln→auswerfen→NACHSEHEN mit injizierbarem Warten). Dazu wache.ts im 3-Sekunden-Takt (§ 5 Plan A): Uebergangslogik pur, gescheiterte Runde behaelt den Stand (R2) und meldet laut (R4). Fenster zeigt Laufwerke live (Modell, Typ, Groesse, Grund) mit Auswerfen-Knopf; Ereigniszeilen fuer Disc rein/raus und Fehler. Bündel-Falle gefunden und behoben: Ein woertliches require überlebt Rollup nicht — win32 wird per dynamischem import() als eigener Chunk gebaut; der Fehler war dank R4 im Fenster sichtbar statt still. Gemessen am echten BU40N (30.08.2026, RIPPY_MESSUNG=1): Laufwerk G: status=ready typ=bluray groesse=33759690752 modell=HL-DT-ST BD-RE BU40N 1.03 serial=0025114C0149 Der Auswurf-Beweis (messung.auswurf) folgt am Ende der Sitzung — der BU40N kann die Schublade nicht selbst einziehen. 52 Tests gruen (Steuercodes, classify, Auswurf-Ablauf, Geraete-Info, Wache, Smoke weiterhin gruen mit Laufwerks-Kachel im Bild). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
162 lines
6.2 KiB
TypeScript
162 lines
6.2 KiB
TypeScript
// 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=<pfad> 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=<pfad> ü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<void> = (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<void> {
|
|
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())
|