WAS: makemkvInstallerHolen fragt vor der Archiv-Quelle über eine Freigabe-Funktion; ohne Freigabe wird das Archiv übersprungen und im Bericht genannt. Der Kern stellt die Frage ans Fenster (beschaffung-status mit `frage`), die MakeMKV-Karte zeigt sie mit „Archiv-Kopie laden" / „Lieber nicht" (beschaffung-antwort); ohne Antwort in zehn Minuten gilt Nein. Die geladene Datei wird mit Größe und SHA-256 gemeldet. WARUM: Durchsicht 12.09.2026, Fund F16 (Commander-Entscheid: Archiv nur nach Rückfrage) — eine unsignierte EXE von fremder Hand, die mit Adminrechten läuft, entscheidet der Nutzer, nicht Rippy. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
272 lines
12 KiB
TypeScript
272 lines
12 KiB
TypeScript
// Die Werkzeuge (§ 9): wo makemkvcon, HandBrakeCLI und flac liegen, die
|
|
// gemessenen HandBrake-Listen (Presets, Encoder), die MakeMKV-Schlüsselkette
|
|
// (§ 9.1) samt Auswahlregel (5.4.0) und das MakeMKV-Selbst-Update.
|
|
import type { KernNachricht } from '../../gemeinsam/nachrichten'
|
|
import type { KernKontext } from '../kontext'
|
|
import { finden } from '../werkzeuge/katalog'
|
|
import type { EinstellungsDienst } from './einstellungen'
|
|
|
|
export type SchluesselStand = Extract<KernNachricht, { art: 'schluessel-status' }>['stand']
|
|
|
|
export class WerkzeugDienst {
|
|
/** Die gemessene Auskunft bleibt liegen — die Pipeline braucht sie für die
|
|
* Preset-Empfehlung, auch wenn gerade kein Fenster offen ist. */
|
|
auskunft: { presets: string[]; backends: string[] } = { presets: [], backends: [] }
|
|
schluesselStand: SchluesselStand | null = null
|
|
private beschaffungLaeuft = false
|
|
private installerAntwort: ((fehler: string | null) => void) | null = null
|
|
/** 5.7.0 (F16): Die offene Ja/Nein-Frage der Beschaffung — null, wenn keine. */
|
|
private frageAntwort: ((ja: boolean) => void) | null = null
|
|
private letzterBeschaffungsText = ''
|
|
|
|
constructor(
|
|
private readonly kontext: KernKontext,
|
|
private readonly einstellungen: EinstellungsDienst,
|
|
) {}
|
|
|
|
// 5.7.0 (Durchsicht F22): Ein Fund gilt 60 s. Vorher lief bei fehlendem
|
|
// MakeMKV bei JEDEM Aufruf die Suchkette bis in die Registry (dreimal
|
|
// `reg query … /s`, synchron, bis 45 s) — und makemkvcon() wird oft
|
|
// gerufen: je Pipeline-Start, je Titel-Lauf, im Update-Lauf alle 15 s.
|
|
private gefunden = new Map<string, { pfad: string; eingestellt: string; wann: number }>()
|
|
private static readonly FUND_FRISCH_MS = 60_000
|
|
|
|
private suchen(name: string, schluessel: string): string {
|
|
const eingestellt = this.einstellungen.lesen(schluessel)
|
|
const alt = this.gefunden.get(name)
|
|
if (alt !== undefined && alt.eingestellt === eingestellt && Date.now() - alt.wann < WerkzeugDienst.FUND_FRISCH_MS) return alt.pfad
|
|
const pfad = finden(name, eingestellt)
|
|
this.gefunden.set(name, { pfad, eingestellt, wann: Date.now() })
|
|
return pfad
|
|
}
|
|
|
|
/** Nach einer Installation oder einem neuen Pfad: frisch suchen. */
|
|
neuSuchen(): void {
|
|
this.gefunden.clear()
|
|
}
|
|
|
|
makemkvcon(): string {
|
|
return this.suchen('makemkv', 'werkzeug.makemkv')
|
|
}
|
|
|
|
handbrake(): string {
|
|
return this.suchen('handbrake', 'werkzeug.handbrake')
|
|
}
|
|
|
|
flac(): string {
|
|
return this.suchen('flac', 'werkzeug.flac')
|
|
}
|
|
|
|
/** Werkzeuge suchen, HandBrake messen, ans Fenster. */
|
|
async senden(): Promise<void> {
|
|
this.neuSuchen()
|
|
const makemkvcon = this.makemkvcon()
|
|
const handbrake = this.handbrake()
|
|
const flac = this.flac()
|
|
const { encoderMessen } = await import('../komprimieren/encoder')
|
|
const gemessen = await encoderMessen(handbrake)
|
|
this.auskunft = { presets: gemessen.presets, backends: gemessen.backends }
|
|
this.kontext.anFenster({
|
|
art: 'werkzeuge',
|
|
auskunft: { makemkvcon, handbrake, flac, presets: gemessen.presets, backends: gemessen.backends },
|
|
})
|
|
this.kontext.protokoll.schreiben(
|
|
`Werkzeuge: makemkvcon=${makemkvcon || 'FEHLT'} handbrake=${handbrake || 'FEHLT'} flac=${flac || 'FEHLT'} · ${gemessen.presets.length} Presets · Encoder ${gemessen.backends.join(',') || '—'}`,
|
|
)
|
|
}
|
|
|
|
// ── MakeMKV-Schlüsselkette (§ 9.1): beim Start und täglich ──────────────
|
|
async schluesselLauf(): Promise<void> {
|
|
if (process.platform !== 'win32') return
|
|
try {
|
|
const { schluesselKette } = await import('../werkzeuge/schluessel')
|
|
this.schluesselStand = await schluesselKette(
|
|
this.makemkvcon(),
|
|
this.einstellungen.lesen('makemkvAppKey'),
|
|
new Date().toISOString(),
|
|
)
|
|
this.kontext.anHauptUndFenster({ art: 'schluessel-status', stand: this.schluesselStand })
|
|
this.kontext.protokoll.schreiben(`Schlüssel: ${this.schluesselStand.text}`)
|
|
} catch (fehler) {
|
|
// R4: Der Schlüssel-Lauf ist Komfort — sein Scheitern wird gesagt.
|
|
this.kontext.fehler(`Schlüsselkette: ${String(fehler)}`)
|
|
}
|
|
}
|
|
|
|
/** MakeMKVs Spur-Auswahlregel sicherstellen (5.4.0) — sonst fehlen
|
|
* Sprachen in der Roh-Datei, die der Dialog anbot. */
|
|
async spurauswahlLauf(): Promise<void> {
|
|
if (process.platform !== 'win32') return
|
|
try {
|
|
const { spurauswahlSicherstellen } = await import('../werkzeuge/spurauswahl')
|
|
const stand = spurauswahlSicherstellen()
|
|
if (stand.stand === 'eigen' || stand.stand === 'fehler') {
|
|
this.kontext.fehler(`MakeMKV-Auswahlregel: ${stand.text}`)
|
|
} else if (stand.stand === 'gesetzt') {
|
|
this.kontext.hinweis(stand.text)
|
|
} else {
|
|
this.kontext.protokoll.schreiben(stand.text)
|
|
}
|
|
} catch (fehler) {
|
|
this.kontext.fehler(`MakeMKV-Auswahlregel: ${String(fehler)}`)
|
|
}
|
|
}
|
|
|
|
/** Antwort des Haupts auf 'installer-starten'. */
|
|
installerErgebnis(fehler: string | null): void {
|
|
this.installerAntwort?.(fehler ?? '')
|
|
this.installerAntwort = null
|
|
}
|
|
|
|
private beschaffungMelden(text: string, anteil: number | null, laeuft: boolean, fehler = false, frage?: string): void {
|
|
this.letzterBeschaffungsText = text
|
|
this.kontext.anFenster({ art: 'beschaffung-status', stand: { werkzeug: 'makemkv', laeuft, text, anteil, fehler, ...(frage !== undefined ? { frage } : {}) } })
|
|
this.kontext.protokoll.schreiben(`MakeMKV-Update: ${text}${frage !== undefined ? ` — FRAGE: ${frage}` : ''}`)
|
|
}
|
|
|
|
/** Antwort des Fensters auf eine offene Frage (beschaffung-antwort). */
|
|
beschaffungAntwort(ja: boolean): void {
|
|
const antwort = this.frageAntwort
|
|
this.frageAntwort = null
|
|
if (antwort === null) {
|
|
this.kontext.hinweis('Es ist keine Frage der MakeMKV-Beschaffung offen.')
|
|
return
|
|
}
|
|
antwort(ja)
|
|
}
|
|
|
|
/** Eine Ja/Nein-Frage ans Fenster; ohne Antwort in zehn Minuten gilt Nein. */
|
|
private frage(text: string): Promise<boolean> {
|
|
return new Promise((fertig) => {
|
|
this.frageAntwort = (ja) => {
|
|
clearTimeout(zeiger)
|
|
this.beschaffungMelden(ja ? 'Freigegeben — die Archiv-Kopie wird geladen.' : 'Nicht freigegeben.', null, true)
|
|
fertig(ja)
|
|
}
|
|
const zeiger = setTimeout(() => {
|
|
if (this.frageAntwort === null) return
|
|
this.frageAntwort = null
|
|
this.beschaffungMelden('Keine Antwort — die Archiv-Kopie wird nicht geladen.', null, true)
|
|
fertig(false)
|
|
}, 10 * 60_000)
|
|
zeiger.unref()
|
|
this.beschaffungMelden(this.letzterBeschaffungsText, null, true, false, text)
|
|
})
|
|
}
|
|
|
|
// ── MakeMKV-Update (§ 9): holen, prüfen, installieren lassen ────────────
|
|
// Der Kern lädt und PRÜFT die Datei; STARTEN kann sie nur das Haupt
|
|
// (shell → UAC-Abfrage; der erhöhte Installer hängt dadurch nicht an der
|
|
// Prozess-Leine und überlebt ein geschlossenes Rippy).
|
|
async makemkvUpdateLauf(): Promise<void> {
|
|
if (this.beschaffungLaeuft) {
|
|
this.beschaffungMelden('Ein MakeMKV-Update läuft bereits.', null, true)
|
|
return
|
|
}
|
|
this.beschaffungLaeuft = true
|
|
try {
|
|
const { istNeuer, makemkvNeuesteVersion, makemkvInstallerHolen, versionsangaben } = await import(
|
|
'../werkzeuge/beschaffen'
|
|
)
|
|
this.beschaffungMelden('Nach der neuesten MakeMKV-Fassung wird gesucht …', null, true)
|
|
const neueste = await makemkvNeuesteVersion()
|
|
if (neueste.length === 0) {
|
|
this.beschaffungMelden(
|
|
'Keine der Quellen konnte sagen, welche Fassung aktuell ist (Hersteller-Seite, Forum, Archiv). ' +
|
|
'Ausweg: MakeMKV von Hand von makemkv.com laden und installieren; Rippy findet es danach von allein.',
|
|
null,
|
|
false,
|
|
true,
|
|
)
|
|
this.beschaffungLaeuft = false
|
|
return
|
|
}
|
|
const installiert = this.schluesselStand?.version ?? ''
|
|
if (installiert.length > 0 && !istNeuer(neueste, installiert)) {
|
|
// Ehrlich bleiben (31.08.2026 gemessen: 1.18.4 ist installiert UND
|
|
// die neueste angekündigte — die Beta ist nur abgelaufen): Ein
|
|
// Download derselben Fassung würde nichts lösen.
|
|
this.beschaffungMelden(
|
|
`MakeMKV ${installiert} ist bereits die neueste angekündigte Fassung. ` +
|
|
'Sobald der Hersteller eine neuere veröffentlicht, holt dieser Knopf sie — ' +
|
|
'bis dahin rippt der Beta-Modus weiter.',
|
|
null,
|
|
false,
|
|
)
|
|
this.beschaffungLaeuft = false
|
|
return
|
|
}
|
|
// Merken, woran eine gelungene Installation zu erkennen ist: Die
|
|
// Versions-Ressource der gefundenen EXE ändert sich (oder es gibt
|
|
// erstmals eine).
|
|
const alteExe = this.makemkvcon()
|
|
const alteVersion = alteExe.length > 0 ? versionsangaben(alteExe).version : ''
|
|
|
|
const ergebnis = await makemkvInstallerHolen(
|
|
neueste,
|
|
(text, anteil) => this.beschaffungMelden(text, anteil, true),
|
|
'',
|
|
undefined,
|
|
undefined,
|
|
undefined,
|
|
// F16: Das Archiv liefert eine unsignierte EXE, die gleich mit
|
|
// Adminrechten läuft — das entscheidet der Nutzer, nicht Rippy.
|
|
(quelle) =>
|
|
this.frage(
|
|
`makemkv.com antwortet nicht. Soll Rippy den MakeMKV-Installer ${neueste} aus dem ${quelle} laden? Das ist eine Kopie von fremder Hand, geprüft wird nur, dass sie ein MakeMKV-Programm ist — er läuft danach mit Adminrechten. Wer sicher gehen will, lädt später selbst von makemkv.com.`,
|
|
),
|
|
)
|
|
this.beschaffungMelden(
|
|
`Geprüft (Quelle: ${ergebnis.quelle}, ${(ergebnis.bytes / 1024 ** 2).toFixed(1)} MB, SHA-256 ${ergebnis.sha256}) — der Installer wird gestartet …`,
|
|
null,
|
|
true,
|
|
)
|
|
|
|
const startFehler = await new Promise<string | null>((fertig) => {
|
|
this.installerAntwort = fertig
|
|
process.parentPort.postMessage({ art: 'installer-starten', pfad: ergebnis.pfad })
|
|
// Falls das Haupt nie antwortet, bleibt der Lauf nicht ewig hängen.
|
|
setTimeout(() => fertig('Keine Antwort vom Haupt-Prozess'), 30_000).unref()
|
|
})
|
|
this.installerAntwort = null
|
|
if (startFehler !== null && startFehler.length > 0) {
|
|
this.beschaffungMelden(`Der Installer wurde nicht gestartet: ${startFehler}`, null, false, true)
|
|
this.beschaffungLaeuft = false
|
|
return
|
|
}
|
|
this.beschaffungMelden(
|
|
`Der MakeMKV-Installer ${ergebnis.version} läuft — bitte die Windows-Nachfrage bestätigen und durchklicken. ` +
|
|
'Rippy merkt die fertige Installation von allein und prüft den Schlüssel neu.',
|
|
null,
|
|
true,
|
|
)
|
|
// Warten, bis die Installation durch ist: Die Versions-Ressource der
|
|
// EXE ändert sich. Alle 15 s nachsehen, höchstens 20 Minuten.
|
|
let runden = 0
|
|
const zeiger = setInterval(() => {
|
|
runden += 1
|
|
const exe = this.makemkvcon()
|
|
const jetztVersion = exe.length > 0 ? versionsangaben(exe).version : ''
|
|
if (jetztVersion.length > 0 && jetztVersion !== alteVersion) {
|
|
clearInterval(zeiger)
|
|
this.beschaffungLaeuft = false
|
|
this.beschaffungMelden(`MakeMKV ${jetztVersion} ist installiert — der Schlüssel wird neu geprüft.`, null, false)
|
|
void this.schluesselLauf().then(() => this.senden())
|
|
} else if (runden >= 80) {
|
|
clearInterval(zeiger)
|
|
this.beschaffungLaeuft = false
|
|
this.beschaffungMelden(
|
|
'Die fertige Installation wurde nicht bemerkt — nach dem Durchklicken hilft „Schlüssel neu prüfen".',
|
|
null,
|
|
false,
|
|
)
|
|
}
|
|
}, 15_000)
|
|
zeiger.unref()
|
|
} catch (fehler) {
|
|
this.beschaffungMelden(fehler instanceof Error ? fehler.message : String(fehler), null, false, true)
|
|
this.beschaffungLaeuft = false
|
|
}
|
|
}
|
|
}
|