Fix: Einstellungen gehen nicht mehr durch zeitgleiches Speichern verloren (Lost Update)
Der asynchrone Settings-Writer hatte — anders als der Session-Writer — keinen Generations-Schutz. Lief eine periodische async Settings-Speicherung gerade, während gleichzeitig synchron saveSettings() lief (z.B. Nutzer ändert eine Option, oder ein Settings-Backup wird wiederhergestellt), konnte der async rename die frisch synchron geschriebene Datei wieder mit dem ALTEN Stand überschreiben. Die gerade gespeicherte Änderung war damit auf der Platte (und im .bak) verloren — bis zur nächsten Speicherung. Fix: Der Settings-Pfad spiegelt jetzt exakt den bereits abgesicherten Session-Pfad: - Eigener Generations-Zähler syncSettingsSaveGeneration; saveSettings() (sync) erhöht ihn. - writeSettingsPayload bekommt die zum Zeitpunkt des Aufrufs erfasste Generation und bricht vor rename UND vor dem EXDEV-Copy ab, wenn inzwischen eine synchrone Speicherung passiert ist (generation < aktuell). - saveSettingsAsync/saveSettingsPayloadAsync trägt die ORIGINAL-Generation auch durch die Warteschlange (vorher wurde beim Abarbeiten der Queue eine frische Generation erfasst → hätte den Schutz ausgehebelt). - Eigener Zähler statt Wiederverwendung von syncSaveGeneration, damit eine synchrone Settings-Speicherung keine laufenden async SESSION-Schreibvorgänge fälschlich verwirft (keine Datei-übergreifende Kopplung). Zusätzlich (verwandt): shutdown() ruft jetzt cancelPendingAsyncSaves() vor der finalen synchronen Speicherung auf, damit eine noch laufende/eingereihte async Settings-Schreibung den Shutdown-Save nicht mehr überholen kann. Test: in-flight + eingereihte async Settings-Speicherung (ALT), dann synchron NEU — nach dem Settle steht NEU auf der Platte. Ohne den Generations-Schutz gewinnt ALT (rot bewiesen).
This commit is contained in:
@@ -789,6 +789,7 @@ public async checkDebridAccounts(): Promise<DebridAccountStatus[]> {
|
||||
}
|
||||
stopDebugServer();
|
||||
abortActiveUpdateDownload();
|
||||
cancelPendingAsyncSaves();
|
||||
this.manager.prepareForShutdown();
|
||||
this.megaWebFallback.dispose();
|
||||
this.realDebridWebFallback.dispose();
|
||||
|
||||
+24
-8
@@ -897,6 +897,7 @@ function readSessionFile(filePath: string): SessionState | null {
|
||||
}
|
||||
|
||||
export function saveSettings(paths: StoragePaths, settings: AppSettings): void {
|
||||
syncSettingsSaveGeneration += 1;
|
||||
ensureBaseDir(paths.baseDir);
|
||||
if (fs.existsSync(paths.configFile)) {
|
||||
try {
|
||||
@@ -917,17 +918,26 @@ export function saveSettings(paths: StoragePaths, settings: AppSettings): void {
|
||||
}
|
||||
|
||||
let asyncSettingsSaveRunning = false;
|
||||
let asyncSettingsSaveQueued: { paths: StoragePaths; settings: AppSettings } | null = null;
|
||||
let asyncSettingsSaveQueued: { paths: StoragePaths; payload: string; generation: number } | null = null;
|
||||
let syncSettingsSaveGeneration = 0;
|
||||
|
||||
async function writeSettingsPayload(paths: StoragePaths, payload: string): Promise<void> {
|
||||
async function writeSettingsPayload(paths: StoragePaths, payload: string, generation: number): Promise<void> {
|
||||
await fs.promises.mkdir(paths.baseDir, { recursive: true });
|
||||
await fsp.copyFile(paths.configFile, `${paths.configFile}.bak`).catch(() => {});
|
||||
const tempPath = `${paths.configFile}.settings.tmp`;
|
||||
await fsp.writeFile(tempPath, payload, "utf8");
|
||||
if (generation < syncSettingsSaveGeneration) {
|
||||
await fsp.rm(tempPath, { force: true }).catch(() => {});
|
||||
return;
|
||||
}
|
||||
try {
|
||||
await fsp.rename(tempPath, paths.configFile);
|
||||
} catch (renameError: unknown) {
|
||||
if (renameError && typeof renameError === "object" && "code" in renameError && (renameError as NodeJS.ErrnoException).code === "EXDEV") {
|
||||
if (generation < syncSettingsSaveGeneration) {
|
||||
await fsp.rm(tempPath, { force: true }).catch(() => {});
|
||||
return;
|
||||
}
|
||||
await fsp.copyFile(tempPath, paths.configFile);
|
||||
await fsp.rm(tempPath, { force: true }).catch(() => {});
|
||||
} else {
|
||||
@@ -937,16 +947,14 @@ async function writeSettingsPayload(paths: StoragePaths, payload: string): Promi
|
||||
}
|
||||
}
|
||||
|
||||
export async function saveSettingsAsync(paths: StoragePaths, settings: AppSettings): Promise<void> {
|
||||
const persisted = sanitizeCredentialPersistence(normalizeSettings(settings));
|
||||
const payload = JSON.stringify(persisted, safeJsonReplacer, 2);
|
||||
async function saveSettingsPayloadAsync(paths: StoragePaths, payload: string, generation: number): Promise<void> {
|
||||
if (asyncSettingsSaveRunning) {
|
||||
asyncSettingsSaveQueued = { paths, settings };
|
||||
asyncSettingsSaveQueued = { paths, payload, generation };
|
||||
return;
|
||||
}
|
||||
asyncSettingsSaveRunning = true;
|
||||
try {
|
||||
await writeSettingsPayload(paths, payload);
|
||||
await writeSettingsPayload(paths, payload, generation);
|
||||
} catch (error) {
|
||||
logger.error(`Async Settings-Save fehlgeschlagen: ${String(error)}`);
|
||||
} finally {
|
||||
@@ -954,11 +962,18 @@ export async function saveSettingsAsync(paths: StoragePaths, settings: AppSettin
|
||||
if (asyncSettingsSaveQueued) {
|
||||
const queued = asyncSettingsSaveQueued;
|
||||
asyncSettingsSaveQueued = null;
|
||||
void saveSettingsAsync(queued.paths, queued.settings);
|
||||
void saveSettingsPayloadAsync(queued.paths, queued.payload, queued.generation);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export async function saveSettingsAsync(paths: StoragePaths, settings: AppSettings): Promise<void> {
|
||||
const generation = syncSettingsSaveGeneration;
|
||||
const persisted = sanitizeCredentialPersistence(normalizeSettings(settings));
|
||||
const payload = JSON.stringify(persisted, safeJsonReplacer, 2);
|
||||
await saveSettingsPayloadAsync(paths, payload, generation);
|
||||
}
|
||||
|
||||
export function emptySession(): SessionState {
|
||||
return {
|
||||
version: 2,
|
||||
@@ -1122,6 +1137,7 @@ export function cancelPendingAsyncSaves(): void {
|
||||
asyncSaveQueued = null;
|
||||
asyncSettingsSaveQueued = null;
|
||||
syncSaveGeneration += 1;
|
||||
syncSettingsSaveGeneration += 1;
|
||||
}
|
||||
|
||||
export async function saveSessionAsync(paths: StoragePaths, session: SessionState): Promise<void> {
|
||||
|
||||
Reference in New Issue
Block a user