M2 sauber gelöst: Auto-Relaunch nach Backup-Import

Problem: nach importBackup hielt der Manager weiter die STALE In-Memory-Session
(Import schrieb nur auf Disk). blockAllPersistence wurde gesetzt um Überschreiben
zu verhindern, aber nie zurückgesetzt → ignorierte der User die manuelle
"Bitte neustarten"-Aufforderung und arbeitete weiter, ging bei hartem Crash
alles verloren (stille Persistenz-Blockade).

In-Memory-Reload verworfen: aborted activeTasks settlen ASYNC und greifen in
ihren finally-Blöcken auf this.session.items[id] zu — ein Session-Swap würde
dagegen racen. Sicherer Reload bräuchte async-Refactor (alle Tasks awaiten).

Lösung: Auto-Relaunch. Nach restored===true startet main.ts die App automatisch
neu (1.5s Delay für Toast). Der frische Prozess lädt die restored Session sauber
über den bewährten Startup-Pfad — null Stale-State-Risiko. Main-getrieben (nicht
Renderer), damit ein Renderer-Fehler den Restart nicht verhindern kann.
skipShutdownPersist/blockAllPersistence schützen weiterhin das kurze Fenster +
den Quit (prepareForShutdown:5680 überspringt Persistenz sauber, fasst die stale
Session nicht an). Nach Relaunch: frischer Prozess, Flags zurückgesetzt — Footgun weg.

621 Tests grün, tsc-Fehlerzahl unverändert (9 pre-existing).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
Sucukdeluxe 2026-05-23 17:35:51 +02:00
parent 6dc32303a0
commit 682bd1d759
3 changed files with 26 additions and 5 deletions

View File

@ -673,15 +673,23 @@ export class AppController {
resetHistoryForRetention(this.storagePaths, this.settings.historyRetentionMode); resetHistoryForRetention(this.storagePaths, this.settings.historyRetentionMode);
// Prevent prepareForShutdown from overwriting the restored data // Block runtime + shutdown persistence so the STALE in-memory session (the
// manager still holds the PRE-import session — importBackup only wrote to disk)
// cannot overwrite the restored data in the brief window before the auto-relaunch.
// The relaunch (triggered in main.ts when restored===true) starts a fresh process
// that loads the restored session cleanly via the normal startup path, so these
// flags never linger in a live session.
// M2-Fix: vorher blieb blockAllPersistence dauerhaft true wenn der User die
// manuelle "Bitte neustarten"-Aufforderung ignorierte → stille Persistenz-Blockade,
// alle weiteren Änderungen gingen bei hartem Crash verloren. Jetzt: Auto-Relaunch.
this.manager.skipShutdownPersist = true; this.manager.skipShutdownPersist = true;
this.manager.blockAllPersistence = true; this.manager.blockAllPersistence = true;
logger.info("Backup wiederhergestellt (verschlüsseltes Format)"); logger.info("Backup wiederhergestellt — App startet automatisch neu");
this.audit("WARN", "Backup importiert", { this.audit("WARN", "Backup importiert", {
historyEntries: Array.isArray(parsed.history) ? parsed.history.length : 0, historyEntries: Array.isArray(parsed.history) ? parsed.history.length : 0,
accountSummary: buildAccountSummary(this.settings) accountSummary: buildAccountSummary(this.settings)
}); });
return { restored: true, message: "Backup wiederhergestellt. Bitte App neustarten." }; return { restored: true, message: "Backup wiederhergestellt App startet automatisch neu…" };
} }
public getSessionLogPath(): string | null { public getSessionLogPath(): string | null {

View File

@ -664,7 +664,20 @@ function registerIpcHandlers(): void {
return { restored: false, message: `Backup-Datei zu groß (max 50 MB, Datei hat ${(stat.size / 1024 / 1024).toFixed(1)} MB)` }; return { restored: false, message: `Backup-Datei zu groß (max 50 MB, Datei hat ${(stat.size / 1024 / 1024).toFixed(1)} MB)` };
} }
const data = await fs.promises.readFile(filePath); const data = await fs.promises.readFile(filePath);
return controller.importBackup(data); const importResult = controller.importBackup(data);
if (importResult.restored) {
// M2: Nach erfolgreichem Import die App automatisch neu starten. Der frische
// Prozess lädt die wiederhergestellte Session sauber vom Disk (kein Stale-
// In-Memory-State, kein dauerhaft blockierter Persist in einer lebenden Session).
// Vom Main getrieben (nicht Renderer), damit ein Renderer-Fehler den Restart
// nicht verhindern kann. Kurze Verzögerung, damit das Ergebnis den Renderer
// erreicht (Toast "App startet automatisch neu…").
setTimeout(() => {
app.relaunch();
app.quit();
}, 1500);
}
return importResult;
}); });
controller.onState = (snapshot) => { controller.onState = (snapshot) => {

View File

@ -64,7 +64,7 @@ App läuft headless auf Windows-Server → Nutzer sitzt nicht davor. Größte L
- ✅ **N1** — toter Disk-Fallback-Block in `findReadyArchiveSets` + verwaiste `pendingItemStatus`-Map entfernt (verhaltensneutral). - ✅ **N1** — toter Disk-Fallback-Block in `findReadyArchiveSets` + verwaiste `pendingItemStatus`-Map entfernt (verhaltensneutral).
**Bewusst NICHT umgesetzt (mit Begründung):** **Bewusst NICHT umgesetzt (mit Begründung):**
- ⏭️ **M2** (`blockAllPersistence` nie zurückgesetzt) — der vom Report vorgeschlagene Reset wäre **unsicher**: nach Backup-Import ist die In-Memory-Session stale (Import schreibt nur auf Disk, lädt den Manager nicht neu). Ein Reset würde beim nächsten persist die restored Daten mit Stale-State überschreiben. `blockAllPersistence` ist absichtlich bis-Neustart. Sauberer Fix = In-Memory-Reload nach Import (größerer Umbau, separat). - **M2** (`blockAllPersistence` nie zurückgesetzt) — GELÖST in v1.7.159 via **Auto-Relaunch**. In-Memory-Reload wäre unsicher (Task-finally-Blöcke settlen async gegen `this.session.items[id]` → Race beim Session-Swap, bräuchte async-Refactor). Stattdessen: nach erfolgreichem Import startet die App automatisch neu (main-getrieben in main.ts, nicht Renderer — robust gegen Renderer-Fehler). Der frische Prozess lädt die restored Session sauber via Standard-Startup-Pfad. `skipShutdownPersist`/`blockAllPersistence` schützen das ~1.5s-Fenster + den Quit (verifiziert: prepareForShutdown:5680 überspringt Persistenz sauber). Footgun eliminiert — User kann nicht mehr im blockierten Zustand weiterarbeiten.
- ⏭️ **M3** (`cancelPendingAsyncSaves` wartet nicht auf laufenden Save) — Report stuft selbst als reines I/O-Overlap ein; die Generation-Guard (storage.ts:1022) schützt die Datenintegrität bereits (stale Write wird verworfen). Kein Korrektheitsgewinn, daher kein Eingriff. - ⏭️ **M3** (`cancelPendingAsyncSaves` wartet nicht auf laufenden Save) — Report stuft selbst als reines I/O-Overlap ein; die Generation-Guard (storage.ts:1022) schützt die Datenintegrität bereits (stale Write wird verworfen). Kein Korrektheitsgewinn, daher kein Eingriff.
**Verifikation:** 30 Test-Dateien, 621 Tests grün. Build sauber. Advisor-Review vor Implementierung (fing H2-Falle: Hybrid-Controller nicht in die Deferred-Map legen, sonst killt `runDeferredPostExtraction` sie selbst). **Verifikation:** 30 Test-Dateien, 621 Tests grün. Build sauber. Advisor-Review vor Implementierung (fing H2-Falle: Hybrid-Controller nicht in die Deferred-Map legen, sonst killt `runDeferredPostExtraction` sie selbst).