WAS (je Fund der Durchsicht vom 12.09.2026): - F15 Tray „Beenden" fragt nach, wenn ein Rip oder eine Kompression läuft (app.quit reißt makemkvcon/HandBrake über die Leine mit). - F17 „Roh-Dateien löschen" und „Ordner löschen" fragen im Fenster nach (Größe und Pfad stehen dabei) — ein Klick war vorher 30 GB. - F18 Eingabefelder speichern auch bei Enter und beim Verschwinden des Feldes (Bereichswechsel) — Tipparbeit ging vorher verloren. - F19 Dialoge: Escape schließt, role=dialog/aria-modal, Fokus im Dialog. - F20 Toast „Disc erkannt" einmal je Disc und Titel, nicht dreimal. - F21 Was mit dem Poster passiert, steht in den Hinweisen (R4). - F22 Werkzeug-Funde gelten 60 s; ein neuer Pfad und „werkzeuge-laden" suchen frisch — vorher bei fehlendem MakeMKV je Aufruf dreimal reg query /s (bis 45 s, synchron). - F23 movie.nfo/tvshow.nfo tragen <uniqueid type=tmdb|imdb>. - F24 Doppelter Zielordner heißt „Akira (1988) (2)" statt „[bluray-2]". - F25 Umlaute in drei Knopftexten. - F26 Phase „rettet" zählt im Haupt als laufender Vorgang (Update-Sperre, Taskleiste). - F27 „Neu komprimieren" nicht mehr für verschobene, unkomprimierte Vorgänge. - F28 Die Automatik sagt bei Audio-CD und unbekannter Disc, warum sie nichts tut. - F30 Audio-CD-Spuren werden asynchron gelesen (alle acht Blöcke Luft), „Abbrechen" wirkt mitten in der Spur. - A5 Der tote Schlüssel transcodePreset ist aus presetFuer raus. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
396 lines
17 KiB
TypeScript
396 lines
17 KiB
TypeScript
// Der Kern — läuft als Electron utilityProcess (KONZEPT-WINDOWS.md § 4.1).
|
|
// Hier passiert die ganze Arbeit; das Fenster zeigt nur an.
|
|
//
|
|
// Seit 5.5.0 ist diese Datei nur noch der EINSTIEG (Separation of Concerns,
|
|
// Commander-Wunsch): Datenbank öffnen, Protokoll anlegen, die Dienste
|
|
// verdrahten und Nachrichten verteilen. Die Arbeit liegt in kern/dienste/:
|
|
// einstellungen — die EINE Quelle (§ 6.7), Ablage/Arbeitsordner
|
|
// laufwerke — Wache, Auswurf, Gesundheit, nachgebautes Laufwerk
|
|
// erkennung — Disc-Merkmale, TMDb/OMDb, Titel-Lauf, Serien-Merker
|
|
// vorgaenge — Datensätze, Warteschlange, Roh-Aufräumen, Statistik
|
|
// werkzeuge — makemkvcon/HandBrake/flac, Schlüssel, MakeMKV-Update
|
|
// speicher — freier Platz
|
|
// benachrichtigung— Discord/Telegram
|
|
// automatik — Zero-Click mit Countdown
|
|
// und die Kette selbst in kern/ablauf/ (Pipeline → Warteschlange → Ablage).
|
|
//
|
|
// 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 { dirname, join } from 'node:path'
|
|
import { istFensterNachricht, istHauptNachricht, type DbStatus, type KernNachricht, type RipAuftragOptionen, type RipAuftragTitel } from '../gemeinsam/nachrichten'
|
|
import { Protokoll } from '../gemeinsam/protokoll'
|
|
import { Pipeline } from './ablauf/pipeline'
|
|
import { AUFBEWAHRUNG_SCHLUESSEL } from './ablage/rohaufraeumen'
|
|
import { AutomatikDienst } from './dienste/automatik'
|
|
import { BenachrichtigungsDienst } from './dienste/benachrichtigung'
|
|
import { EinstellungsDienst } from './dienste/einstellungen'
|
|
import { ErkennungDienst } from './dienste/erkennung'
|
|
import { LaufwerkDienst } from './dienste/laufwerke'
|
|
import { SpeicherDienst, platzUrteil } from './dienste/speicher'
|
|
import { VorgangDienst } from './dienste/vorgaenge'
|
|
import { WerkzeugDienst } from './dienste/werkzeuge'
|
|
import { datenbankPfadAusArgv, type KernKontext } from './kontext'
|
|
import { Datenbank } from './speicher/db'
|
|
|
|
// ── Datenbank und Protokoll ─────────────────────────────────────────────
|
|
let db: Datenbank | null = null
|
|
let dbStatus: DbStatus
|
|
const pfad = datenbankPfadAusArgv(process.argv)
|
|
const protokoll = new Protokoll(join(pfad !== null ? dirname(pfad) : join(process.env['TEMP'] ?? '.', 'rippy'), 'logs'), 'kern')
|
|
protokoll.aufraeumen()
|
|
protokoll.schreiben(`Kern startet (PID ${process.pid}, Node ${process.versions.node}) — Datenbank ${pfad ?? '?'}`)
|
|
|
|
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) }
|
|
}
|
|
}
|
|
protokoll.schreiben(dbStatus.ok ? `Datenbank geöffnet: ${dbStatus.pfad} (Schema ${db?.schemaVersion() ?? '?'})` : `DATENBANK-FEHLER: ${dbStatus.fehler}`)
|
|
|
|
const smokeKern = process.argv.includes('--smoke-kern')
|
|
let fensterPort: MessagePortMain | null = null
|
|
|
|
const kontext: KernKontext = {
|
|
db,
|
|
dbStatus,
|
|
smoke: smokeKern,
|
|
protokoll,
|
|
anFenster: (nachricht) => fensterPort?.postMessage(nachricht),
|
|
anHauptUndFenster: (nachricht) => {
|
|
process.parentPort.postMessage(nachricht)
|
|
fensterPort?.postMessage(nachricht)
|
|
},
|
|
hinweis: (text) => {
|
|
protokoll.schreiben(text)
|
|
fensterPort?.postMessage({ art: 'kern-hinweis', text })
|
|
},
|
|
fehler: (text) => {
|
|
protokoll.schreiben(`FEHLER: ${text}`)
|
|
console.error(`[kern] ${text}`)
|
|
fensterPort?.postMessage({ art: 'kern-fehler', text })
|
|
},
|
|
}
|
|
|
|
// 5.7.0 (Durchsicht 12.09.2026, F2): Ein Fehler, den niemand fängt, darf
|
|
// nicht still verschwinden. Gemessen in Electron 44: Ein utilityProcess
|
|
// STIRBT NICHT an einer unbehandelten Promise-Ablehnung — er schweigt nur,
|
|
// und die Konsole eines Tray-Programms sieht niemand. Deshalb landet jede
|
|
// solche Ablehnung im Protokoll und im Fenster (R4). Eine echte Ausnahme
|
|
// wird protokolliert und beendet den Kern kontrolliert — der Haupt startet
|
|
// ihn neu und verdrahtet das Fenster wieder (kernstart.ts).
|
|
process.on('unhandledRejection', (grund: unknown) => {
|
|
kontext.fehler(`Unbehandelte Ablehnung im Kern: ${String(grund instanceof Error ? (grund.stack ?? grund.message) : grund).slice(0, 600)}`)
|
|
})
|
|
process.on('uncaughtException', (fehler: Error) => {
|
|
kontext.fehler(`Absturz im Kern: ${String(fehler.stack ?? fehler.message).slice(0, 600)} — der Kern beendet sich, der Haupt startet ihn neu.`)
|
|
setTimeout(() => process.exit(1), 200)
|
|
})
|
|
|
|
// ── Dienste verdrahten ──────────────────────────────────────────────────
|
|
const einstellungen = new EinstellungsDienst(kontext)
|
|
const werkzeuge = new WerkzeugDienst(kontext, einstellungen)
|
|
const speicher = new SpeicherDienst(kontext, einstellungen)
|
|
const benachrichtigung = new BenachrichtigungsDienst(kontext, einstellungen)
|
|
|
|
// Die Pipeline und die Laufwerke brauchen einander: Auswurf nur, wenn kein
|
|
// Rip läuft; Rip nur mit Gerätestand. Deshalb erst die Funktionen, dann
|
|
// beide Objekte.
|
|
let ripVerwaltung: Pipeline
|
|
const laufwerke = new LaufwerkDienst(
|
|
kontext,
|
|
{
|
|
discEingelegt: (geraet) => {
|
|
void erkennung
|
|
.discErmitteln(geraet)
|
|
.then((zuordnung) => {
|
|
automatik.zuordnungMerken(geraet.id, zuordnung)
|
|
return automatik.pruefen(geraet, zuordnung, erkennung.schonGerippt(geraet.id))
|
|
})
|
|
.catch((fehler: unknown) => kontext.fehler(`Nach dem Einlegen in ${geraet.id}: ${String(fehler)}`))
|
|
},
|
|
discEntnommen: (geraet) => {
|
|
erkennung.discVergessen(geraet.id)
|
|
automatik.discEntnommen(geraet.id)
|
|
ripVerwaltung.discEntnommen(geraet.id)
|
|
},
|
|
},
|
|
(id) => ripVerwaltung.laeuft(id),
|
|
)
|
|
|
|
const erkennung = new ErkennungDienst(kontext, {
|
|
ripLaeuft: (id) => ripVerwaltung.laeuft(id),
|
|
geraetInfoVon: (id) => laufwerke.geraetInfoVon(id),
|
|
makemkvcon: () => werkzeuge.makemkvcon(),
|
|
})
|
|
|
|
const vorgaenge = new VorgangDienst(kontext, einstellungen, werkzeuge, erkennung, benachrichtigung, () => speicher.senden())
|
|
|
|
ripVerwaltung = new Pipeline({
|
|
makemkvcon: () => werkzeuge.makemkvcon(),
|
|
handbrake: () => werkzeuge.handbrake(),
|
|
flac: () => werkzeuge.flac(),
|
|
audioToc: (id) => laufwerke.audioToc(id),
|
|
audioSpurLesen: (id, start, sektoren, schreiben, fortschritt, abbruch) => laufwerke.audioSpurLesen(id, start, sektoren, schreiben, fortschritt, abbruch),
|
|
geraet: (id) => laufwerke.geraetInfoVon(id),
|
|
ablage: () => einstellungen.ablageOrdner(),
|
|
rohWurzel: () => einstellungen.rohWurzel(),
|
|
einstellungen: () => einstellungen.fuerPipeline(),
|
|
melden: (status) => {
|
|
kontext.anHauptUndFenster({ art: 'rip-status', status })
|
|
if (status.phase === 'fertig' || status.phase === 'fehler' || status.phase === 'abgebrochen') {
|
|
protokoll.schreiben(`Rip ${status.id} ${status.phase}: ${status.text.slice(0, 300)}`)
|
|
}
|
|
// Nach einem gescheiterten Rip (oder Lesefehlern) gleich das Windows-
|
|
// Protokoll fragen: Disc oder Hardware? (5.4.0, Punkt 8)
|
|
if (status.phase === 'fehler' || (status.phase === 'fertig' && status.lesefehler)) void laufwerke.pruefen(status.id)
|
|
if (status.phase === 'fehler') void benachrichtigung.senden('Rippy: Rip fehlgeschlagen', `Laufwerk ${status.id}: ${status.text.slice(0, 300)}`)
|
|
if (status.phase === 'fertig') void benachrichtigung.senden('Rippy: Rip fertig', `Laufwerk ${status.id}: ${status.text.slice(0, 300)}`)
|
|
},
|
|
zuordnung: (id) => erkennung.zuordnung(id),
|
|
discAngaben: (id) => erkennung.discAngaben(id),
|
|
fingerprint: (id) => erkennung.fingerprint(id),
|
|
zuordnungFuer: (vorschlag) => erkennung.zuordnungAus(vorschlag),
|
|
bibliothekEintragen: (eintrag) => vorgaenge.bibliothekEintragen(eintrag),
|
|
vorgangBeginnt: (v) => vorgaenge.vorgangBeginnt(v),
|
|
vorgangEinreihen: (auftrag, titel) => vorgaenge.vorgangEinreihen(auftrag, titel),
|
|
vorgangEndet: (id, phase, text) => vorgaenge.vorgangEndet(id, phase, text),
|
|
nachRipAuswerfen: (id) => {
|
|
// Vorgabe: auswerfen — die Warteschlange will ein freies Laufwerk.
|
|
if (einstellungen.lesen('autoAuswurf') === 'false') return
|
|
void laufwerke.auswerfen(id)
|
|
},
|
|
})
|
|
|
|
const automatik = new AutomatikDienst(kontext, {
|
|
an: () => einstellungen.automatikAn(),
|
|
sekunden: () => einstellungen.automatikSekunden(),
|
|
ripLaeuft: (id) => ripVerwaltung.laeuft(id),
|
|
titelLesen: async (id) => {
|
|
const stand = await erkennung.titelLesen(id)
|
|
automatik.titelMerken(id, stand)
|
|
return stand
|
|
},
|
|
ripStarten: (id, auftrag, optionen) => ripMitPlatz(id, auftrag, optionen),
|
|
discAngaben: (id) => erkennung.discAngaben(id),
|
|
serienMerker: (zuordnung) => {
|
|
const m = zuordnung.meta.typ === 'tv' ? erkennung.serienMerkerFuer(zuordnung.meta.id) : null
|
|
return m === null ? null : { staffel: m.staffel, naechsteFolge: m.naechsteFolge }
|
|
},
|
|
benachrichtigen: (titel, text) => void benachrichtigung.senden(titel, text),
|
|
})
|
|
|
|
/** Der Rip-Start mit Platz-Prüfung (5.5.0): Der Dialog zeigt das Urteil
|
|
* vorher, der Kern urteilt beim Start — auch für die Automatik. Zu wenig
|
|
* für den verlustfreien Rip: kein Start, der Grund steht in den
|
|
* Ereignissen. Knapp: Start mit Hinweis. Nicht messbar: Start ohne Urteil
|
|
* (die Messung ist Komfort, nicht Pflicht). */
|
|
function ripMitPlatz(id: string, auftrag: RipAuftragTitel[], optionen: RipAuftragOptionen): void {
|
|
const rohBytes = auftrag.reduce((s, t) => s + (t.groesseBytes ?? 0), 0)
|
|
const arbeit = speicher.stand().arbeit
|
|
if (arbeit.ok && rohBytes > 0) {
|
|
const urteil = platzUrteil(arbeit.freiBytes, rohBytes, einstellungen.lesen('transcodeEnabled') !== 'false')
|
|
if (urteil.hart) {
|
|
kontext.fehler(`Rip auf ${id} nicht gestartet — ${urteil.text}`)
|
|
return
|
|
}
|
|
if (urteil.text.length > 0) kontext.hinweis(`Platz im Arbeitsordner: ${urteil.text}`)
|
|
}
|
|
// Der Rip beginnt — der Automatik-Kasten in der Kachel hat ausgedient.
|
|
automatik.leeren(id)
|
|
void laufwerke.bereit.then(() => ripVerwaltung.starten(id, auftrag, optionen))
|
|
}
|
|
|
|
// ── Nachrichten vom Fenster ─────────────────────────────────────────────
|
|
function fensterNachricht(frage: unknown): void {
|
|
if (!istFensterNachricht(frage)) {
|
|
// R4: Eine verworfene Nachricht verschwindet NICHT still. Genau das
|
|
// kostete am 01.09.2026 eine Runde — 'titel-lesen' fehlte im Prüfer,
|
|
// der Kern warf sie weg, und im Fenster stand ewig „Rippy liest die
|
|
// Titel …", ohne dass irgendwo etwas stand.
|
|
const art = typeof frage === 'object' && frage !== null ? String((frage as Record<string, unknown>).art) : typeof frage
|
|
kontext.fehler(`Nachricht „${art}" wurde verworfen — sie ist dem Kern unbekannt.`)
|
|
return
|
|
}
|
|
switch (frage.art) {
|
|
case 'ping':
|
|
kontext.anFenster({ art: 'pong', zeit: frage.zeit })
|
|
return
|
|
case 'auswerfen':
|
|
void laufwerke.auswerfen(frage.id)
|
|
return
|
|
case 'titel-lesen':
|
|
// Der Nutzer will selbst wählen — ein laufender Countdown endet.
|
|
automatik.stopp(frage.id, '')
|
|
void erkennung.titelLesen(frage.id)
|
|
return
|
|
case 'rip-start':
|
|
automatik.stopp(frage.id, '')
|
|
ripMitPlatz(frage.id, frage.auftrag ?? [], frage.optionen ?? {})
|
|
return
|
|
case 'rip-abbruch':
|
|
ripVerwaltung.abbrechen(frage.id)
|
|
return
|
|
case 'rip-ueberspringen':
|
|
ripVerwaltung.ueberspringen(frage.id)
|
|
return
|
|
case 'rettung-start':
|
|
automatik.leeren(frage.id)
|
|
void laufwerke.bereit.then(() => ripVerwaltung.rettungStarten(frage.id))
|
|
return
|
|
case 'metadaten-suchen':
|
|
void erkennung.metadatenSuchen(frage.id, frage.text)
|
|
return
|
|
case 'zuordnung-setzen':
|
|
automatik.stopp(frage.id, '')
|
|
void erkennung.zuordnungSetzen(frage.id, frage.vorschlag)
|
|
return
|
|
case 'einstellung-setzen':
|
|
if (einstellungen.setzen(frage.schluessel, frage.wert)) {
|
|
// Neue Aufbewahrungs-Regel, neuer Arbeitsordner: sofort sichtbar.
|
|
if (frage.schluessel === AUFBEWAHRUNG_SCHLUESSEL) vorgaenge.senden()
|
|
if (frage.schluessel === 'ablage' || frage.schluessel === 'arbeitsordner') {
|
|
vorgaenge.senden()
|
|
speicher.senden()
|
|
}
|
|
// 5.7.0 (Durchsicht F13): Ein neuer Werkzeug-Pfad wird sofort neu
|
|
// gesucht und gemessen — vorher stand „NICHT GEFUNDEN" so lange, bis
|
|
// das Fenster neu geladen wurde (in der Erst-Einrichtung besonders
|
|
// irreführend). Ein neuer eigener Schlüssel wird sofort geprüft.
|
|
if (frage.schluessel.startsWith('werkzeug.')) void werkzeuge.senden()
|
|
if (frage.schluessel === 'makemkvAppKey') void werkzeuge.schluesselLauf().then(() => werkzeuge.senden())
|
|
}
|
|
return
|
|
case 'einstellungen-laden':
|
|
einstellungen.senden()
|
|
return
|
|
case 'werkzeuge-laden':
|
|
void werkzeuge.senden()
|
|
return
|
|
case 'makemkv-update':
|
|
void werkzeuge.makemkvUpdateLauf()
|
|
return
|
|
case 'schluessel-pruefen':
|
|
void werkzeuge.schluesselLauf().then(() => werkzeuge.senden())
|
|
return
|
|
case 'vorgaenge-laden':
|
|
vorgaenge.neuMessen()
|
|
return
|
|
case 'vorgang-roh-loeschen':
|
|
vorgaenge.rohLoeschen(frage.vorgangId)
|
|
return
|
|
case 'vorgang-neu-komprimieren':
|
|
vorgaenge.neuKomprimieren(frage.vorgangId, frage.preset ?? '')
|
|
return
|
|
case 'kompression-abbrechen':
|
|
if (!vorgaenge.warteschlange.abbrechen(frage.vorgangId)) {
|
|
kontext.fehler(`Vorgang ${frage.vorgangId} ist nicht in der Warteschlange.`)
|
|
}
|
|
return
|
|
case 'kompression-pause':
|
|
vorgaenge.pause(frage.an)
|
|
return
|
|
case 'roh-aufraeumen':
|
|
vorgaenge.rohAufraeumenLauf()
|
|
return
|
|
case 'fremden-ordner-loeschen':
|
|
vorgaenge.fremdenOrdnerLoeschen(frage.ordner)
|
|
return
|
|
case 'laufwerk-pruefen':
|
|
void laufwerke.pruefen(frage.id)
|
|
return
|
|
case 'automatik-stopp':
|
|
automatik.stopp(frage.id, 'Automatik gestoppt — du entscheidest.')
|
|
return
|
|
case 'automatik-jetzt':
|
|
automatik.jetzt(frage.id)
|
|
return
|
|
case 'benachrichtigung-testen':
|
|
void benachrichtigung.testen()
|
|
return
|
|
case 'poster-laden':
|
|
void erkennung.posterLaden()
|
|
return
|
|
case 'speicher-laden':
|
|
speicher.senden()
|
|
return
|
|
}
|
|
}
|
|
|
|
process.parentPort.on('message', (ereignis) => {
|
|
const nachricht: unknown = ereignis.data
|
|
if (istHauptNachricht(nachricht) && nachricht.art === 'installer-ergebnis') {
|
|
werkzeuge.installerErgebnis(nachricht.fehler)
|
|
return
|
|
}
|
|
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) => fensterNachricht(portEreignis.data))
|
|
fensterPort.start()
|
|
// Damit das Fenster nach dem Verbinden sofort den Stand kennt:
|
|
kontext.anFenster(bereitNachricht())
|
|
void laufwerke.bereit.then(() => {
|
|
if (laufwerke.fehler !== null) kontext.fehler(`Laufwerke: ${laufwerke.fehler}`)
|
|
kontext.anFenster({ art: 'laufwerke', liste: laufwerke.aktuellerStand(), ereignisse: [] })
|
|
if (werkzeuge.schluesselStand !== null) {
|
|
kontext.anFenster({ art: 'schluessel-status', stand: werkzeuge.schluesselStand })
|
|
}
|
|
})
|
|
vorgaenge.senden()
|
|
speicher.senden()
|
|
}
|
|
})
|
|
|
|
function bereitNachricht(): KernNachricht {
|
|
return {
|
|
art: 'kern-bereit',
|
|
pid: process.pid,
|
|
nodeVersion: process.versions.node,
|
|
datenbank: dbStatus,
|
|
}
|
|
}
|
|
|
|
process.parentPort.postMessage(bereitNachricht())
|
|
// Das Haupt braucht die Einstellungen (Update-Adresse) auch ohne Fenster.
|
|
einstellungen.senden()
|
|
// Was nach dem letzten Beenden noch offen war (5.4.0).
|
|
vorgaenge.wiederaufnehmen()
|
|
// Nicht im Smoke: Der Schlüssel-Lauf spricht Forum + makemkvcon an —
|
|
// der Gerüst-Beweis soll ohne Netz und ohne MakeMKV grün sein.
|
|
if (!smokeKern && process.env['RIPPY_OHNE_SCHLUESSEL'] !== '1') {
|
|
void werkzeuge.schluesselLauf()
|
|
void werkzeuge.spurauswahlLauf()
|
|
// Werkzeuge einmal beim Start messen — die Preset-Empfehlung der
|
|
// Pipeline braucht Backends und Preset-Liste auch ohne offenes Fenster.
|
|
void werkzeuge.senden()
|
|
// Täglich neu prüfen (§ 9.1) — ohne den Prozess-Exit aufzuhalten.
|
|
setInterval(() => void werkzeuge.schluesselLauf(), 24 * 60 * 60 * 1000).unref()
|
|
}
|
|
// Roh-Aufräumen (5.4.0): eine Minute nach dem Start, dann täglich — und
|
|
// nach jeder gelungenen Kompression (siehe Warteschlange). Speicher (5.5.0):
|
|
// jede Minute.
|
|
if (!smokeKern) {
|
|
setTimeout(() => vorgaenge.rohAufraeumenLauf(), 60_000).unref()
|
|
setInterval(() => vorgaenge.rohAufraeumenLauf(), 24 * 60 * 60 * 1000).unref()
|
|
speicher.taktStarten()
|
|
}
|