Fix: Download-Queue läuft nach fehlgeschlagenem Update weiter (kein Stillstand bis Neustart)
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).
This commit is contained in:
parent
76b3f99476
commit
dfd19260f6
@ -38,6 +38,7 @@ import { initSessionLog, getSessionLogPath, shutdownSessionLog } from "./session
|
|||||||
import { MegaWebFallback } from "./mega-web-fallback";
|
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 { 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 { abortActiveUpdateDownload, checkGitHubUpdate, installLatestUpdate } from "./update";
|
||||||
|
import { runInstallWithResume } from "./update-install-flow";
|
||||||
import { rotateDebugToken, startDebugServer, stopDebugServer } from "./debug-server";
|
import { rotateDebugToken, startDebugServer, stopDebugServer } from "./debug-server";
|
||||||
import { encryptBackup, decryptBackup } from "./backup-crypto";
|
import { encryptBackup, decryptBackup } from "./backup-crypto";
|
||||||
import { buildBackupPayload, planBackupImport } from "./backup-payload";
|
import { buildBackupPayload, planBackupImport } from "./backup-payload";
|
||||||
@ -459,16 +460,14 @@ public async checkDebridAccounts(): Promise<DebridAccountStatus[]> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public async installUpdate(onProgress?: (progress: UpdateInstallProgress) => void): Promise<UpdateInstallResult> {
|
public async installUpdate(onProgress?: (progress: UpdateInstallProgress) => void): Promise<UpdateInstallResult> {
|
||||||
if (this.manager.isSessionRunning()) {
|
|
||||||
this.manager.stop({ parkForRestart: true });
|
|
||||||
}
|
|
||||||
this.manager.persistNowSync();
|
|
||||||
|
|
||||||
const cacheAgeMs = Date.now() - this.lastUpdateCheckAt;
|
const cacheAgeMs = Date.now() - this.lastUpdateCheckAt;
|
||||||
const cached = this.lastUpdateCheck && !this.lastUpdateCheck.error && cacheAgeMs <= 10 * 60 * 1000
|
const cached = this.lastUpdateCheck && !this.lastUpdateCheck.error && cacheAgeMs <= 10 * 60 * 1000
|
||||||
? this.lastUpdateCheck
|
? this.lastUpdateCheck
|
||||||
: undefined;
|
: 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) {
|
if (result.started) {
|
||||||
this.lastUpdateCheck = null;
|
this.lastUpdateCheck = null;
|
||||||
this.lastUpdateCheckAt = 0;
|
this.lastUpdateCheckAt = 0;
|
||||||
|
|||||||
34
src/main/update-install-flow.ts
Normal file
34
src/main/update-install-flow.ts
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
export interface InstallResumeManager {
|
||||||
|
isSessionRunning(): boolean;
|
||||||
|
stop(options: { parkForRestart: boolean }): void;
|
||||||
|
persistNowSync(): void;
|
||||||
|
start(): Promise<void> | void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function runInstallWithResume<T extends { started: boolean }>(
|
||||||
|
manager: InstallResumeManager,
|
||||||
|
doInstall: () => Promise<T>
|
||||||
|
): Promise<T> {
|
||||||
|
const wasRunning = manager.isSessionRunning();
|
||||||
|
if (wasRunning) {
|
||||||
|
manager.stop({ parkForRestart: true });
|
||||||
|
}
|
||||||
|
manager.persistNowSync();
|
||||||
|
|
||||||
|
const resumeIfParked = async (): Promise<void> => {
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
63
tests/update-install-flow.test.ts
Normal file
63
tests/update-install-flow.test.ts
Normal file
@ -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);
|
||||||
|
});
|
||||||
|
});
|
||||||
Loading…
Reference in New Issue
Block a user