From 3fb9e85ba2f5bb656fa5edda2f51c1d3beceb452 Mon Sep 17 00:00:00 2001 From: Sucukdeluxe Date: Wed, 10 Jun 2026 00:34:31 +0200 Subject: [PATCH] =?UTF-8?q?Fix:=20Run-Lebenszyklus=20=E2=80=94=20Stop-Summ?= =?UTF-8?q?ary,=20Scheduler-Race,=20ehrliche=20Run-Ende-Meldung,=20Boot-Re?= =?UTF-8?q?-Arm?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Audit-Befunde RUN-1 bis RUN-4: - RUN-1: Manueller Stop beendete den Durchlauf, ohne dass je eine Run-Summary kam (der Scheduler bricht an der while-Bedingung ab, finishRun wird nie erreicht). stop() schickt jetzt "Durchlauf gestoppt" mit den bis dahin gesammelten Zahlen (nur wenn der Run lief; beim Restart/Shutdown-Pfad unterdrueckt — der Prozess stirbt gleich). - RUN-2: Stop->Start innerhalb des Scheduler-Sleeps (~120-220ms) liess den neuen Run fuehrerlos zurueck: ensureScheduler returnte (scheduleRunning noch true), die alte Schleife exitete auf Generation-Mismatch — danach lief KEIN Scheduler mehr, obwohl running=true: keine Downloads, kein finishRun, kein Webhook, und erneutes Start() heilte nichts (early-return wegen running). Die finally respawnt jetzt den Scheduler, wenn der Run aktiv ist und die Generation weitergezogen wurde. - RUN-3: Geplanter Start ueberlebte keinen App-Neustart (Timer lebte nur im Prozess, Setting blieb stehen) — Auto-Update/Reboot verschluckte den geplanten Run still. armScheduledStart extrahiert, beim Boot re-armt; vergangene Zeit beim Boot wird geloggt+geleert statt blind zu starten (Konflikt mit autoResumeOnStart-Gate). - RUN-4: "Durchlauf beendet" feuerte in der Default-Konfiguration (autoExtractWhenStopped) waehrend das Entpacken noch lief. Titel sagt jetzt "Downloads beendet" + Hinweis "Entpacken laeuft noch — Paket-Meldungen folgen", wenn Post-Processing aussteht. --- src/main/download-manager.ts | 31 +++++++++++++++++++-- src/main/main.ts | 52 +++++++++++++++++++++++------------- 2 files changed, 63 insertions(+), 20 deletions(-) diff --git a/src/main/download-manager.ts b/src/main/download-manager.ts index 2dd9cad..7dc66ef 100644 --- a/src/main/download-manager.ts +++ b/src/main/download-manager.ts @@ -5640,6 +5640,7 @@ export class DownloadManager extends EventEmitter { const parkForRestart = options?.parkForRestart === true; const abortReason: "stop" | "shutdown" = parkForRestart ? "shutdown" : "stop"; const keepExtraction = this.settings.autoExtractWhenStopped; + const wasRunning = this.session.running; this.schedulerGeneration += 1; this.session.running = false; this.session.paused = false; @@ -5685,6 +5686,19 @@ export class DownloadManager extends EventEmitter { pkg.updatedAt = nowMs(); } } + // A manual stop ends the run without ever reaching finishRun (the scheduler + // loop exits at its while condition), so the run summary would be lost. + // Suppressed for the restart/shutdown path: the process is about to die. + if (wasRunning && !parkForRestart && this.settings.notifyOnRunFinished && this.runItemIds.size > 0) { + const outcomes = Array.from(this.runOutcomes.values()); + const success = outcomes.filter((s) => s === "completed").length; + const failed = outcomes.filter((s) => s === "failed").length; + void sendNotification(this.settings.notifyUrl, { + title: "⏹️ Durchlauf gestoppt", + message: `${success}/${this.runItemIds.size} erfolgreich, ${failed} fehlgeschlagen — Rest zurueck in der Warteschlange`, + mention: this.settings.notifyMention + }); + } this.persistSoon(); this.emitState(true); } @@ -8248,6 +8262,14 @@ export class DownloadManager extends EventEmitter { } finally { this.scheduleRunning = false; logger.info(`Scheduler beendet (gen=${myGeneration})`); + // Stop->Start race: a new run can begin while this loop sleeps (start()'s + // ensureScheduler early-returns on scheduleRunning, then this loop exits on + // the generation mismatch). Without a respawn the run sits leaderless + // forever: running=true, but nothing schedules and finishRun never comes. + if (this.session.running && this.schedulerGeneration !== myGeneration) { + logger.warn(`Scheduler-Respawn: Run aktiv, alte Generation ${myGeneration} beendet (Stop->Start-Race)`); + void this.ensureScheduler().catch((error) => logger.error(`Scheduler-Respawn fehlgeschlagen: ${compactErrorText(error)}`)); + } } } @@ -12181,9 +12203,14 @@ export class DownloadManager extends EventEmitter { }; this.session.summaryText = `Summary: Dauer ${duration}s, Ø Speed ${humanSize(avgSpeed)}/s, Erfolg ${success}/${total}`; if (this.settings.notifyOnRunFinished && total > 0) { + // With autoExtractWhenStopped (default) the run ends as soon as downloads + // are done while extraction may still be running — say so instead of + // claiming the whole run is finished. + const postProcessPending = this.packagePostProcessTasks.size > 0 || this.hasAnyDeferredPostProcessPending(); + const scope = postProcessPending ? "Downloads beendet" : "Durchlauf beendet"; void sendNotification(this.settings.notifyUrl, { - title: failed > 0 ? "⚠️ Durchlauf beendet" : "🏁 Durchlauf beendet", - message: `${success}/${total} erfolgreich, ${failed} fehlgeschlagen, ${cancelled} abgebrochen\nDauer ${duration}s, Durchschnitt ${humanSize(avgSpeed)}/s`, + title: failed > 0 ? `⚠️ ${scope}` : `🏁 ${scope}`, + message: `${success}/${total} erfolgreich, ${failed} fehlgeschlagen, ${cancelled} abgebrochen\nDauer ${duration}s, Durchschnitt ${humanSize(avgSpeed)}/s${postProcessPending ? "\nEntpacken laeuft noch — Paket-Meldungen folgen." : ""}`, mention: this.settings.notifyMention }); } diff --git a/src/main/main.ts b/src/main/main.ts index 65aaf1f..4930521 100644 --- a/src/main/main.ts +++ b/src/main/main.ts @@ -77,6 +77,35 @@ function isDevMode(): boolean { return process.env.NODE_ENV === "development"; } +// Single owner of the scheduled-start timer. startOnPast: a past time entered +// interactively starts right away; at boot a stale past time is cleared instead +// (an unattended auto-start at boot would race autoResumeOnStart's conflict gate). +function armScheduledStart(schedMs: number, opts: { startOnPast: boolean }): void { + if (scheduledStartTimer !== null) { + clearTimeout(scheduledStartTimer); + scheduledStartTimer = null; + } + if (!schedMs || schedMs <= 0) { + return; + } + const delay = schedMs - Date.now(); + if (delay <= 0) { + if (opts.startOnPast) { + void controller.start().catch((err) => logger.warn(`Scheduled-Start Fehler: ${String(err)}`)); + } else { + logger.warn(`Geplanter Start (${new Date(schedMs).toLocaleString()}) lag beim App-Start in der Vergangenheit — verworfen`); + } + controller.updateSettings({ scheduledStartEpochMs: 0 }); + return; + } + scheduledStartTimer = setTimeout(() => { + scheduledStartTimer = null; + void controller.start().catch((err) => logger.warn(`Scheduled-Start Fehler: ${String(err)}`)); + controller.updateSettings({ scheduledStartEpochMs: 0 }); + }, delay); + logger.info(`Geplanter Start gearmt: ${new Date(schedMs).toLocaleString()}`); +} + function createWindow(): BrowserWindow { const window = new BrowserWindow({ width: 1920, @@ -328,24 +357,7 @@ function registerIpcHandlers(): void { const result = controller.updateSettings(validated as Partial); updateClipboardWatcher(); updateTray(); - if (scheduledStartTimer !== null) { - clearTimeout(scheduledStartTimer); - scheduledStartTimer = null; - } - const schedMs = result.scheduledStartEpochMs || 0; - if (schedMs > 0) { - const delay = schedMs - Date.now(); - if (delay <= 0) { - void controller.start().catch((err) => logger.warn(`Scheduled-Start Fehler: ${String(err)}`)); - controller.updateSettings({ scheduledStartEpochMs: 0 }); - } else { - scheduledStartTimer = setTimeout(() => { - scheduledStartTimer = null; - void controller.start().catch((err) => logger.warn(`Scheduled-Start Fehler: ${String(err)}`)); - controller.updateSettings({ scheduledStartEpochMs: 0 }); - }, delay); - } - } + armScheduledStart(result.scheduledStartEpochMs || 0, { startOnPast: true }); return result; }); ipcMain.handle(IPC_CHANNELS.RESET_PROVIDER_DAILY_USAGE, (_event: IpcMainInvokeEvent, provider: string) => { @@ -807,6 +819,10 @@ app.whenReady().then(() => { bindMainWindowLifecycle(mainWindow); updateClipboardWatcher(); updateTray(); + // A scheduled start persists in the settings but its timer lived only in this + // process — without re-arming it here, any restart (auto-update, reboot, + // crash) silently swallowed the planned run. + armScheduledStart(controller.getSettings().scheduledStartEpochMs || 0, { startOnPast: false }); app.on("activate", () => { if (BrowserWindow.getAllWindows().length === 0) {