// Startet und überwacht den Kern (utilityProcess) — KONZEPT § 4.1. // utilityProcess statt child_process.fork ist Electrons eigene Empfehlung; // nur er kann einen MessagePort DIREKT zwischen Kern und Fenster aufspannen // (Electron-Doku »utilityProcess« / »MessagePorts in Electron«). import { app, MessageChannelMain, utilityProcess, type UtilityProcess, type WebContents } from 'electron' import { join } from 'node:path' import { istKernNachricht, type HauptNachricht, type KernNachricht } from '../gemeinsam/nachrichten' export interface KernZustand { laeuft: boolean pid?: number neustarts: number } // Stirbt der Kern öfter als so, stimmt etwas Grundsätzliches — dann wird // nicht weiter neu gestartet, sondern der Zustand im Fenster gezeigt. const MAX_NEUSTARTS_JE_MINUTE = 3 export class KernVerwaltung { private kern: UtilityProcess | null = null private beendenGewollt = false private neustarts = 0 private neustartZeiten: number[] = [] constructor( private readonly datenbankPfad: string, private readonly beiNachricht: (nachricht: KernNachricht) => void, private readonly beiZustand: (zustand: KernZustand) => void, private readonly smokeModus = false, ) {} starten(): void { const kernDatei = join(__dirname, 'kern.js') const args = [`--datenbank=${this.datenbankPfad}`] // Der Smoke-Beweis läuft ohne Netz und ohne MakeMKV — der Kern lässt // die Schlüsselkette dann aus. if (this.smokeModus) args.push('--smoke-kern') // Im installierten Rippy liegen die mitgelieferten Werkzeuge unter // resources\vendor — der Kern findet sie über RIPPY_VENDOR (§ 9). const umgebung: NodeJS.ProcessEnv = { ...process.env } if (app.isPackaged) { umgebung['RIPPY_VENDOR'] = join(process.resourcesPath, 'vendor') } const kern = utilityProcess.fork(kernDatei, args, { serviceName: 'Rippy-Kern', stdio: 'inherit', env: umgebung, }) this.kern = kern kern.on('message', (nachricht: unknown) => { if (istKernNachricht(nachricht)) this.beiNachricht(nachricht) }) kern.on('spawn', () => { this.beiZustand(this.zustand()) }) kern.on('exit', (code) => { this.kern = null if (this.beendenGewollt) return // R4: Ein gestorbener Kern ist ein lautes Ereignis, kein stilles. console.error(`[haupt] Kern beendet mit Code ${code} — Neustart wird geprüft`) const jetzt = Date.now() this.neustartZeiten = this.neustartZeiten.filter((t) => jetzt - t < 60_000) if (this.neustartZeiten.length >= MAX_NEUSTARTS_JE_MINUTE) { console.error('[haupt] Kern stirbt wiederholt — kein weiterer Neustart') this.beiZustand(this.zustand()) return } this.neustartZeiten.push(jetzt) this.neustarts += 1 this.starten() }) } /** * Spannt einen frischen MessagePort direkt zwischen Kern und Fenster auf. * Der Fortschritt eines Rips geht damit später ohne Umweg über das Haupt * ins Fenster (§ 4.1). */ fensterVerbinden(webContents: WebContents): void { if (this.kern === null) return const { port1, port2 } = new MessageChannelMain() const nachricht: HauptNachricht = { art: 'fenster-port' } this.kern.postMessage(nachricht, [port1]) webContents.postMessage('kern-port', null, [port2]) } /** Eine Nachricht an den Kern (z. B. die Antwort auf 'installer-starten'). */ senden(nachricht: HauptNachricht): void { this.kern?.postMessage(nachricht) } zustand(): KernZustand { return { laeuft: this.kern !== null, pid: this.kern?.pid, neustarts: this.neustarts, } } beenden(): void { this.beendenGewollt = true this.kern?.kill() this.kern = null } }