WAS: - pipeline.starten fängt jede Ausnahme und meldet sie als Fehler des Laufwerks; vor dem Start eine Schreibprobe am Zielordner mit Klartext („nicht erreichbar … NAS verbunden?"). Vorher warf mkdirSync bei toter Ablage aus starten() heraus, der Aufrufer schluckte es: kein Fehler im Fenster, keine Protokollzeile, der Dialog ging zu (Sonde 10). - kern/index.ts: unbehandelte Promise-Ablehnungen landen im Protokoll und im Fenster (gemessen: ein Electron-utilityProcess stirbt daran nicht, er schweigt); echte Ausnahmen werden protokolliert, der Kern endet kontrolliert, der Haupt startet ihn neu. Die Einlege-Kette hat ein Fangnetz. - kernstart.ts spannt nach einem Kern-Neustart den Fenster-Port neu auf; das Fenster schließt den toten Port und meldet den Neustart als Zeile. Vorher blieb es taub, bis Rippy komplett neu gestartet wurde. - wache.runde() löscht den geplanten Timer, bevor es einen neuen setzt — jeder Auswurf legte sonst eine weitere 3-s-Kette an (Sonde 3). - beenden() räumt den leeren Roh-Ordner, verschont aber rettung.iso und den Bericht (RETTUNG_DATEIEN). - Typ „unbekannt" heißt nur ohne Titel-Auftrag ISO; mit Auftrag rippt makemkvcon, der Typ folgt der Größe (Blu-ray/DVD) für Preset und Ordner. WARUM: Durchsicht 12.09.2026, Funde F2, F4, F6, F8, F10 — jeder mit Test. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
120 lines
4.3 KiB
TypeScript
120 lines
4.3 KiB
TypeScript
// 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[] = []
|
|
/** Das Fenster, das zuletzt einen Port bekam — ein NEU gestarteter Kern
|
|
* bekommt es wieder (5.7.0, Durchsicht F4: vorher hielt das Fenster nach
|
|
* einem Kern-Neustart den toten Port und war taub, bis Rippy komplett neu
|
|
* gestartet wurde). */
|
|
private letztesFenster: WebContents | null = null
|
|
|
|
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())
|
|
// Ein Neustart: Das Fenster hängt noch am toten Port des alten Kerns —
|
|
// frischen Port aufspannen, sonst tut kein Knopf mehr etwas.
|
|
if (this.neustarts > 0 && this.letztesFenster !== null && !this.letztesFenster.isDestroyed()) {
|
|
this.fensterVerbinden(this.letztesFenster)
|
|
}
|
|
})
|
|
|
|
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 {
|
|
this.letztesFenster = webContents
|
|
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
|
|
}
|
|
}
|