From dfd19260f63e2e651f683c3f6cf5ca96193c1753 Mon Sep 17 00:00:00 2001 From: Sucukdeluxe Date: Wed, 17 Jun 2026 05:50:55 +0200 Subject: [PATCH] =?UTF-8?q?Fix:=20Download-Queue=20l=C3=A4uft=20nach=20feh?= =?UTF-8?q?lgeschlagenem=20Update=20weiter=20(kein=20Stillstand=20bis=20Ne?= =?UTF-8?q?ustart)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Beim Klick auf "Update" stoppt das Tool zuerst die laufende Session (stop({parkForRestart:true}) → running=false, alle Items auf "Wartet") und startet dann die Installation. Schlug die Installation fehl — und das ist auf realistischen Wegen möglich (git.24-music.de-Aussetzer, Netz weg über alle Versuche, oder ein Release mit fehlendem/falschem Digest → Integritätsprüfung schlägt fehl) — kam KEIN App-Neustart (nur bei Erfolg wird app.quit geplant) und auch kein In-Process-Resume. Folge: die komplette Queue stand still (alle Items "Wartet"), bis ein Mensch "Start" klickte oder das Programm neu startete — auf einem unbeaufsichtigten Server mit ~1 TB/h ein echter Stillstand. Fix: Der Stop-Installieren-Resume-Ablauf liegt jetzt in runInstallWithResume(): War vorher eine Session aktiv und schlägt die Installation fehl, wird die Session wieder gestartet (manager.start(), idempotent — die Items sind bereits "queued", der Scheduler nimmt sie normal auf). Der Resume greift auf BEIDEN Fehlerpfaden: started:false UND geworfene Ausnahme (try/catch). Bei Erfolg (started:true) wird NICHT resumed (die App beendet sich gleich), und war keine Session aktiv, passiert nichts. Test: started:false → resume; install wirft → resume + rethrow; started:true → kein resume; nicht-laufend → kein stop/resume. Logik in eine reine, testbare Funktion ausgelagert (kein schwergewichtiger AppController-Konstruktor nötig). --- src/main/app-controller.ts | 11 +++--- src/main/update-install-flow.ts | 34 +++++++++++++++++ tests/update-install-flow.test.ts | 63 +++++++++++++++++++++++++++++++ 3 files changed, 102 insertions(+), 6 deletions(-) create mode 100644 src/main/update-install-flow.ts create mode 100644 tests/update-install-flow.test.ts diff --git a/src/main/app-controller.ts b/src/main/app-controller.ts index c3fc94e..9a1a980 100644 --- a/src/main/app-controller.ts +++ b/src/main/app-controller.ts @@ -38,6 +38,7 @@ import { initSessionLog, getSessionLogPath, shutdownSessionLog } from "./session import { MegaWebFallback } from "./mega-web-fallback"; import { addHistoryEntry, addHistoryEntryForRetention, cancelPendingAsyncSaves, clearHistory, createStoragePaths, loadHistory, loadHistoryForRetention, loadSession, loadSettings, normalizeHistoryEntry, normalizeLoadedSession, normalizeLoadedSessionTransientFields, normalizeSettings, removeHistoryEntry, resetHistoryForRetention, saveHistory, saveSession, saveSettings } from "./storage"; import { abortActiveUpdateDownload, checkGitHubUpdate, installLatestUpdate } from "./update"; +import { runInstallWithResume } from "./update-install-flow"; import { rotateDebugToken, startDebugServer, stopDebugServer } from "./debug-server"; import { encryptBackup, decryptBackup } from "./backup-crypto"; import { buildBackupPayload, planBackupImport } from "./backup-payload"; @@ -459,16 +460,14 @@ public async checkDebridAccounts(): Promise { } public async installUpdate(onProgress?: (progress: UpdateInstallProgress) => void): Promise { - if (this.manager.isSessionRunning()) { - this.manager.stop({ parkForRestart: true }); - } - this.manager.persistNowSync(); - const cacheAgeMs = Date.now() - this.lastUpdateCheckAt; const cached = this.lastUpdateCheck && !this.lastUpdateCheck.error && cacheAgeMs <= 10 * 60 * 1000 ? this.lastUpdateCheck : undefined; - const result = await installLatestUpdate(this.settings.updateRepo, cached, onProgress); + const result = await runInstallWithResume( + this.manager, + () => installLatestUpdate(this.settings.updateRepo, cached, onProgress) + ); if (result.started) { this.lastUpdateCheck = null; this.lastUpdateCheckAt = 0; diff --git a/src/main/update-install-flow.ts b/src/main/update-install-flow.ts new file mode 100644 index 0000000..c2c626e --- /dev/null +++ b/src/main/update-install-flow.ts @@ -0,0 +1,34 @@ +export interface InstallResumeManager { + isSessionRunning(): boolean; + stop(options: { parkForRestart: boolean }): void; + persistNowSync(): void; + start(): Promise | void; +} + +export async function runInstallWithResume( + manager: InstallResumeManager, + doInstall: () => Promise +): Promise { + const wasRunning = manager.isSessionRunning(); + if (wasRunning) { + manager.stop({ parkForRestart: true }); + } + manager.persistNowSync(); + + const resumeIfParked = async (): Promise => { + if (wasRunning && !manager.isSessionRunning()) { + await manager.start(); + } + }; + + try { + const result = await doInstall(); + if (!result.started) { + await resumeIfParked(); + } + return result; + } catch (error) { + await resumeIfParked(); + throw error; + } +} diff --git a/tests/update-install-flow.test.ts b/tests/update-install-flow.test.ts new file mode 100644 index 0000000..36dd959 --- /dev/null +++ b/tests/update-install-flow.test.ts @@ -0,0 +1,63 @@ +import { describe, expect, it } from "vitest"; +import { runInstallWithResume, InstallResumeManager } from "../src/main/update-install-flow"; + +function makeManager(running: boolean): InstallResumeManager & { startCalls: number; stopCalls: number; persistCalls: number; sessionRunning: boolean } { + return { + sessionRunning: running, + startCalls: 0, + stopCalls: 0, + persistCalls: 0, + isSessionRunning() { + return this.sessionRunning; + }, + stop() { + this.stopCalls += 1; + this.sessionRunning = false; + }, + persistNowSync() { + this.persistCalls += 1; + }, + async start() { + this.startCalls += 1; + this.sessionRunning = true; + } + }; +} + +describe("runInstallWithResume", () => { + it("resumes a running session when the install returns started:false", async () => { + const m = makeManager(true); + const result = await runInstallWithResume(m, async () => ({ started: false })); + expect(result.started).toBe(false); + expect(m.stopCalls).toBe(1); + expect(m.startCalls).toBe(1); + expect(m.isSessionRunning()).toBe(true); + }); + + it("resumes a running session when the install THROWS, then rethrows", async () => { + const m = makeManager(true); + await expect( + runInstallWithResume(m, async () => { + throw new Error("network down"); + }) + ).rejects.toThrow("network down"); + expect(m.stopCalls).toBe(1); + expect(m.startCalls).toBe(1); + expect(m.isSessionRunning()).toBe(true); + }); + + it("does NOT resume when the install succeeds (started:true) — the app is about to quit", async () => { + const m = makeManager(true); + const result = await runInstallWithResume(m, async () => ({ started: true })); + expect(result.started).toBe(true); + expect(m.startCalls).toBe(0); + expect(m.isSessionRunning()).toBe(false); + }); + + it("does NOT resume when no session was running before the install", async () => { + const m = makeManager(false); + await runInstallWithResume(m, async () => ({ started: false })); + expect(m.stopCalls).toBe(0); + expect(m.startCalls).toBe(0); + }); +});