Ampel / ampel (push) Successful in 1m41s
Commander: "Rippy wird zwar sauber beendet aber startet NICHT neu in der
neuen version."
Ursache, auf drei Beinen belegt:
1. Bibliothek: electron-updater startet den Installer per
spawn(..., { detached: true }) (BaseUpdater.js, an der installierten
Fassung nachgelesen). `detached` setzt unter Windows KEIN Breakaway.
2. Leine: leine.ts setzt KILL_ON_JOB_CLOSE, und Kinder erben die
Mitgliedschaft. Der Installer war damit Mitglied der Arbeitsgruppe.
3. Platte: pending\RippySetup-5.1.3.exe lag geladen da (18:11:47),
installiert war weiter 5.1.2. Der Download klappte, nur die
Installation nicht.
Der Installer starb also in genau der Sekunde, in der Rippy sich
beendete, um ihm Platz zu machen.
Fix — die Leine bleibt, der Installer klinkt sich aus:
* leine.ts setzt zusaetzlich JOB_OBJECT_LIMIT_BREAKAWAY_OK. Das aendert
fuer sich genommen NICHTS: Kinder erben weiter, ausser sie verlangen
das Ausklinken selbst. Kern, makemkvcon und HandBrakeCLI bleiben
angeleint, die rc10-Waisen-Falle (Paragraph 3.3) ist unveraendert zu.
* Neu: ausserhalbDerLeineStarten() startet EIN Programm per CreateProcessW
mit CREATE_BREAKAWAY_FROM_JOB. Liegt in leine.ts, weil dort die
Arbeitsgruppe verwaltet wird — und weil Waechter R1 koffi nur dort und
in kern/laufwerk/win32.ts erlaubt.
* update.ts installiert jetzt SELBST: autoInstallOnAppQuit ist aus, der
Installerpfad kommt aus downloadUpdate() (laut AppUpdater.d.ts "Paths to
downloaded files"), die Argumente sind die von electron-updater
gemessenen (--updated /S --force-run). Gestartet wird beim Beenden
(will-quit) und ueber den Knopf.
* Neu kommandozeile.ts (pur, ohne koffi/Electron): CreateProcessW nimmt
EINE Zeichenkette. Ohne die Regeln von CommandLineToArgvW zerfiele ein
Pfad wie "C:\Users\Tobi Neu\..." in zwei Argumente.
BEWIESEN, nicht behauptet — neu: beweise/leine-breakaway.js
OHNE Ausklinken (so macht es electron-updater)
lebt nach dem Tod des Elternprozesses: NEIN -> WIE ERWARTET
MIT Ausklinken (so macht es Rippy ab 5.1.4)
lebt nach dem Tod des Elternprozesses: JA -> WIE ERWARTET
Der erste Messlauf zeigte ein falsches Negativ: Der Enkel starb an der
sterbenden KONSOLE des Elternprozesses, nicht an der Arbeitsgruppe. Erst
mit CREATE_NO_WINDOW (eigene Konsole) misst der Versuch, was er messen
soll. Diese Lehre steht als Kommentar im Code und der Flag ist auch im
echten Aufruf gesetzt.
237 Tests gruen (vorher 230), Typpruefung sauber. Version 5.1.4.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
247 lines
9.0 KiB
TypeScript
247 lines
9.0 KiB
TypeScript
// Selbst-Update (KONZEPT § 6.8) — electron-updater gegen die Releases im
|
||
// öffentlichen Gitea (Entscheid 6). Die vier Regeln:
|
||
// 1. NUR HTTPS — ohne Code-Signing (Entscheid 5) ist der Transportweg
|
||
// die einzige Absicherung; eine http://-Adresse wird ABGELEHNT.
|
||
// 2. NIE während eines Rips — weder prüfen, laden noch installieren.
|
||
// 3. Ein Fehlschlag ist sichtbar, aber nicht laut: Der Zeitpunkt der
|
||
// letzten erfolgreichen Prüfung steht im Status.
|
||
// 4. Die laufende Version ist ablesbar (Fenster + Protokollkopf).
|
||
//
|
||
// Ergänzt am 01.09.2026 auf Zuruf des Commanders („irgendwie fehlt der
|
||
// Knopf ‚Auf Updates prüfen' und die Meldung beim Start"): Der Kanal
|
||
// arbeitete korrekt, war aber UNSICHTBAR — der Stand lag im Haupt und
|
||
// wurde nur beim Laden des Fensters einmal mitgeschickt. Jetzt trägt er
|
||
// zusätzlich die gefundene Version, ihre Änderungsnotizen und den
|
||
// Ladefortschritt, und JEDE Änderung meldet sich sofort ans Fenster.
|
||
import { app } from 'electron'
|
||
import electronUpdater from 'electron-updater'
|
||
import { INSTALLER_ARGUMENTE } from './kommandozeile'
|
||
import { ausserhalbDerLeineStarten } from './leine'
|
||
import { neuerungenText, updateUrlPruefen } from './update-regeln'
|
||
|
||
export {
|
||
NEUERUNGEN_HOECHSTENS,
|
||
STANDARD_UPDATE_URL,
|
||
neuerungenText,
|
||
updateUrlPruefen,
|
||
} from './update-regeln'
|
||
|
||
const { autoUpdater } = electronUpdater
|
||
|
||
// EINE Definition für Haupt und Fenster (§ 6.7 sinngemäß: eine Quelle).
|
||
export type { UpdateStand } from '../gemeinsam/nachrichten'
|
||
import type { UpdateStand } from '../gemeinsam/nachrichten'
|
||
|
||
const ANFANG: UpdateStand = {
|
||
letzteErfolgreichePruefung: '',
|
||
text: 'noch nicht geprüft',
|
||
neueVersion: '',
|
||
neuerungen: '',
|
||
bereit: false,
|
||
laeuft: false,
|
||
fortschritt: -1,
|
||
}
|
||
|
||
let stand: UpdateStand = { ...ANFANG }
|
||
let aenderungMelden: () => void = () => {
|
||
// Vor updateEinrichten() gibt es noch kein Fenster, das zuhören könnte.
|
||
}
|
||
let ripLaeuftJetzt: () => boolean = () => false
|
||
let scharf = false
|
||
/** Der geladene Installer — leer, solange nichts bereitliegt. */
|
||
let installerPfad = ''
|
||
/** Einmal gestartet reicht: Der Knopf und der Beenden-Weg dürfen sich
|
||
* nicht gegenseitig zwei Installer auf den Hals hetzen. */
|
||
let installerGestartet = false
|
||
let rippyBeenden: () => void = () => app.quit()
|
||
|
||
export function updateStand(): UpdateStand {
|
||
return { ...stand }
|
||
}
|
||
|
||
/** Stand ändern UND das Fenster benachrichtigen — nie das eine ohne das
|
||
* andere: Ein Stand, den niemand erfährt, ist der Zustand von vorher
|
||
* (der 26.07.-Fund „ein still scheiternder Hintergrund-Prozess"). */
|
||
function setzen(teil: Partial<UpdateStand>): void {
|
||
stand = { ...stand, ...teil }
|
||
aenderungMelden()
|
||
}
|
||
|
||
export function updateEinrichten(
|
||
eigeneUrl: string,
|
||
ripLaeuft: () => boolean,
|
||
melden: (text: string) => void,
|
||
beiAenderung: () => void = () => {
|
||
// ohne Fenster kein Empfänger — der Stand bleibt trotzdem abrufbar
|
||
},
|
||
beenden: () => void = () => app.quit(),
|
||
): void {
|
||
aenderungMelden = beiAenderung
|
||
ripLaeuftJetzt = ripLaeuft
|
||
rippyBeenden = beenden
|
||
if (!app.isPackaged) {
|
||
setzen({ text: 'Entwicklungslauf — Selbst-Update ist nur im installierten Rippy aktiv.' })
|
||
return
|
||
}
|
||
const pruefung = updateUrlPruefen(eigeneUrl)
|
||
if (!pruefung.ok) {
|
||
setzen({ text: pruefung.grund })
|
||
melden(pruefung.grund)
|
||
return
|
||
}
|
||
if (eigeneUrl.trim().length > 0) {
|
||
autoUpdater.setFeedURL({ provider: 'generic', url: eigeneUrl.trim() })
|
||
}
|
||
autoUpdater.autoDownload = false
|
||
// WIR installieren, nicht electron-updater (01.09.2026, am lebenden
|
||
// Objekt bezahlt): Sein eigener Weg startet den Installer per
|
||
// `spawn(…, { detached: true })`, und `detached` setzt unter Windows KEIN
|
||
// Breakaway. Der Installer wurde damit Mitglied der Prozess-Leine (§ 3.3)
|
||
// und starb in genau der Sekunde, in der Rippy sich für ihn beendete.
|
||
// Befund: „Rippy wird zwar sauber beendet aber startet NICHT neu."
|
||
autoUpdater.autoInstallOnAppQuit = false
|
||
scharf = true
|
||
|
||
// Beim Beenden — egal ob über das Tray, den Update-Knopf oder das
|
||
// Herunterfahren von Windows — bekommt ein bereitliegendes Update noch
|
||
// seinen Start. CreateProcessW ist synchron: Der Installer LEBT, bevor
|
||
// diese Zeile zurückkommt.
|
||
app.on('will-quit', () => {
|
||
if (!stand.bereit || ripLaeuftJetzt()) return
|
||
installerAnwerfen()
|
||
})
|
||
|
||
autoUpdater.on('update-available', (info) => {
|
||
const neuerungen = neuerungenText(info.releaseNotes)
|
||
if (ripLaeuft()) {
|
||
// Regel 2: nicht einmal herunterladen, solange gerippt wird — der
|
||
// tägliche Takt versucht es wieder.
|
||
setzen({
|
||
neueVersion: info.version,
|
||
neuerungen,
|
||
laeuft: false,
|
||
fortschritt: -1,
|
||
text: `Version ${info.version} ist da — sie wird geladen, sobald kein Rip mehr läuft.`,
|
||
})
|
||
melden(stand.text)
|
||
return
|
||
}
|
||
setzen({
|
||
neueVersion: info.version,
|
||
neuerungen,
|
||
laeuft: true,
|
||
fortschritt: 0,
|
||
text: `Version ${info.version} wird geladen …`,
|
||
})
|
||
// Der Commander wollte die Meldung sehen, nicht suchen müssen.
|
||
melden(`Update verfügbar: Version ${info.version} wird geladen.`)
|
||
void autoUpdater
|
||
.downloadUpdate()
|
||
.then((pfade: string[]) => {
|
||
// downloadUpdate liefert die abgelegten Dateien zurück
|
||
// (AppUpdater.d.ts: „Paths to downloaded files") — die .exe darin
|
||
// ist der Installer, den wir gleich selbst starten. Liegt die Datei
|
||
// vom letzten Lauf schon im Zwischenspeicher, kommt sie hier
|
||
// ebenfalls heraus, ohne erneut zu laden.
|
||
const exe = (pfade ?? []).find((p) => p.toLowerCase().endsWith('.exe')) ?? ''
|
||
if (exe.length > 0) installerPfad = exe
|
||
})
|
||
.catch((fehler: unknown) => {
|
||
setzen({
|
||
laeuft: false,
|
||
fortschritt: -1,
|
||
text: `Update-Download fehlgeschlagen: ${String(fehler).slice(0, 160)}`,
|
||
})
|
||
})
|
||
})
|
||
autoUpdater.on('download-progress', (fortschritt) => {
|
||
setzen({ laeuft: true, fortschritt: Math.round(fortschritt.percent) })
|
||
})
|
||
autoUpdater.on('update-downloaded', (info) => {
|
||
setzen({
|
||
neueVersion: info.version,
|
||
neuerungen: neuerungenText(info.releaseNotes) || stand.neuerungen,
|
||
bereit: true,
|
||
laeuft: false,
|
||
fortschritt: 100,
|
||
text: `Version ${info.version} liegt bereit — sie wird beim nächsten Start installiert.`,
|
||
})
|
||
melden(stand.text)
|
||
})
|
||
autoUpdater.on('update-not-available', () => {
|
||
setzen({
|
||
letzteErfolgreichePruefung: new Date().toISOString(),
|
||
neueVersion: '',
|
||
neuerungen: '',
|
||
bereit: false,
|
||
laeuft: false,
|
||
fortschritt: -1,
|
||
text: 'Rippy ist aktuell.',
|
||
})
|
||
})
|
||
autoUpdater.on('error', (fehler) => {
|
||
// Regel 3: still scheitern (WLAN weg, Server aus) — aber ablesbar.
|
||
setzen({ laeuft: false, fortschritt: -1, text: `Letzte Prüfung scheiterte: ${String(fehler).slice(0, 160)}` })
|
||
})
|
||
|
||
pruefen()
|
||
setInterval(pruefen, 24 * 60 * 60 * 1000).unref?.()
|
||
}
|
||
|
||
function pruefen(): void {
|
||
if (ripLaeuftJetzt()) return
|
||
setzen({ laeuft: true, text: 'Suche nach Updates …' })
|
||
void autoUpdater.checkForUpdates().catch(() => {
|
||
// der error-Handler oben trägt den Grund ein
|
||
})
|
||
}
|
||
|
||
/** Der Knopf „Jetzt prüfen" im Fenster. Antwortet IMMER etwas — ein
|
||
* Knopf, der nichts sagt, ist schlimmer als keiner. */
|
||
export function updatePruefenJetzt(): void {
|
||
if (!scharf) {
|
||
// Im Entwicklungslauf steht der Grund schon im Text; ihn erneut zu
|
||
// melden hält den Knopf ehrlich, statt ihn ins Leere laufen zu lassen.
|
||
aenderungMelden()
|
||
return
|
||
}
|
||
if (ripLaeuftJetzt()) {
|
||
setzen({ text: 'Während eines Rips wird nicht nach Updates gesucht — das ist Absicht (§ 6.8).' })
|
||
return
|
||
}
|
||
pruefen()
|
||
}
|
||
|
||
/** Den Installer AUSSERHALB der Prozess-Leine anwerfen. Gibt zurück, ob
|
||
* er wirklich läuft — „nichts passiert" darf nicht als Erfolg durchgehen. */
|
||
function installerAnwerfen(): { ok: boolean; grund: string } {
|
||
if (installerGestartet) return { ok: true, grund: '' }
|
||
if (installerPfad.length === 0) {
|
||
return { ok: false, grund: 'Es liegt kein geladener Installer bereit.' }
|
||
}
|
||
const urteil = ausserhalbDerLeineStarten(installerPfad, INSTALLER_ARGUMENTE)
|
||
if (!urteil.ok) {
|
||
setzen({ text: `Update konnte nicht gestartet werden: ${urteil.fehler}` })
|
||
return { ok: false, grund: urteil.fehler }
|
||
}
|
||
installerGestartet = true
|
||
return { ok: true, grund: '' }
|
||
}
|
||
|
||
/** Der Knopf „Jetzt neu starten und installieren". Nur sinnvoll, wenn
|
||
* wirklich etwas bereitliegt — und nie mitten in einem Rip. */
|
||
export function updateInstallierenJetzt(): { ok: boolean; grund: string } {
|
||
if (!scharf || !stand.bereit) {
|
||
return { ok: false, grund: 'Es liegt kein geladenes Update bereit.' }
|
||
}
|
||
if (ripLaeuftJetzt()) {
|
||
return { ok: false, grund: 'Erst wenn der Rip fertig ist — Rippy startet dafür neu (§ 6.8).' }
|
||
}
|
||
// Erst den Installer, DANN beenden: Er wartet von sich aus, bis Rippy
|
||
// weg ist, und startet es hinterher wieder (`--force-run`).
|
||
const urteil = installerAnwerfen()
|
||
if (!urteil.ok) return urteil
|
||
rippyBeenden()
|
||
return { ok: true, grund: '' }
|
||
}
|