Compare commits

..

No commits in common. "d594c5082b6c5aed39baaa73d6ea70cde61060e4" and "8f3681b160146d70b08fd9e15f4bd0b9a9fe6349" have entirely different histories.

5 changed files with 35 additions and 130 deletions

View File

@ -1,6 +1,6 @@
{ {
"name": "real-debrid-downloader", "name": "real-debrid-downloader",
"version": "1.7.225", "version": "1.7.224",
"description": "Desktop downloader", "description": "Desktop downloader",
"main": "build/main/main/main.js", "main": "build/main/main/main.js",
"author": "Sucukdeluxe", "author": "Sucukdeluxe",

View File

@ -39,7 +39,7 @@ import { getItemLogPath, initItemLogs, shutdownItemLogs } from "./item-log";
import { getPackageLogPath, initPackageLogs, shutdownPackageLogs } from "./package-log"; import { getPackageLogPath, initPackageLogs, shutdownPackageLogs } from "./package-log";
import { initSessionLog, getSessionLogPath, shutdownSessionLog } from "./session-log"; import { initSessionLog, getSessionLogPath, shutdownSessionLog } from "./session-log";
import { MegaWebFallback } from "./mega-web-fallback"; import { MegaWebFallback } from "./mega-web-fallback";
import { addHistoryEntry, addHistoryEntryForRetention, cancelPendingAsyncSaves, clearHistory, createStoragePaths, loadHistory, loadHistoryForRetention, loadSessionWithStatus, 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 { runInstallWithResume } from "./update-install-flow";
import { rotateDebugToken, startDebugServer, stopDebugServer, restartDebugServer, getDebugServerRuntimeStatus, getActiveDebugToken, getDebugAllowlist, writeDebugServerConfig, clearDebugToken } from "./debug-server"; import { rotateDebugToken, startDebugServer, stopDebugServer, restartDebugServer, getDebugServerRuntimeStatus, getActiveDebugToken, getDebugAllowlist, writeDebugServerConfig, clearDebugToken } from "./debug-server";
@ -112,8 +112,7 @@ export class AppController {
initTraceLog(this.storagePaths.baseDir); initTraceLog(this.storagePaths.baseDir);
this.settings = loadSettings(this.storagePaths); this.settings = loadSettings(this.storagePaths);
resetHistoryForRetention(this.storagePaths, this.settings.historyRetentionMode); resetHistoryForRetention(this.storagePaths, this.settings.historyRetentionMode);
const loadResult = loadSessionWithStatus(this.storagePaths); const session = loadSession(this.storagePaths);
const session = loadResult.session;
this.megaWebFallback = new MegaWebFallback(() => ({ this.megaWebFallback = new MegaWebFallback(() => ({
login: this.settings.megaLogin, login: this.settings.megaLogin,
password: this.settings.megaPassword password: this.settings.megaPassword
@ -127,7 +126,6 @@ export class AppController {
realDebridWebUnrestrict: (link: string, signal?: AbortSignal) => this.realDebridWebFallback.unrestrict(link, signal), realDebridWebUnrestrict: (link: string, signal?: AbortSignal) => this.realDebridWebFallback.unrestrict(link, signal),
bestDebridWebUnrestrict: (link: string, signal?: AbortSignal) => this.bestDebridWebFallback.unrestrict(link, signal), bestDebridWebUnrestrict: (link: string, signal?: AbortSignal) => this.bestDebridWebFallback.unrestrict(link, signal),
invalidateMegaSession: () => this.megaWebFallback.invalidateSession(), invalidateMegaSession: () => this.megaWebFallback.invalidateSession(),
protectEmptyClobber: loadResult.status === "empty-unreadable",
onHistoryEntry: (entry: HistoryEntry) => { onHistoryEntry: (entry: HistoryEntry) => {
addHistoryEntryForRetention(this.storagePaths, this.settings.historyRetentionMode, entry, this.historyLimits()); addHistoryEntryForRetention(this.storagePaths, this.settings.historyRetentionMode, entry, this.historyLimits());
} }

View File

@ -363,7 +363,6 @@ type DownloadManagerOptions = {
bestDebridWebUnrestrict?: BestDebridWebUnrestrictor; bestDebridWebUnrestrict?: BestDebridWebUnrestrictor;
invalidateMegaSession?: () => void; invalidateMegaSession?: () => void;
onHistoryEntry?: HistoryEntryCallback; onHistoryEntry?: HistoryEntryCallback;
protectEmptyClobber?: boolean;
}; };
function generateHistoryId(): string { function generateHistoryId(): string {
@ -1689,10 +1688,6 @@ export class DownloadManager extends EventEmitter {
public blockAllPersistence = false; public blockAllPersistence = false;
private protectAgainstEmptyClobber = false;
private emptyClobberProtectionLogged = false;
private debridService: DebridService; private debridService: DebridService;
private invalidateMegaSessionFn?: () => void; private invalidateMegaSessionFn?: () => void;
@ -1836,10 +1831,6 @@ export class DownloadManager extends EventEmitter {
this.session = session; this.session = session;
this.itemCount = Object.keys(this.session.items).length; this.itemCount = Object.keys(this.session.items).length;
this.storagePaths = storagePaths; this.storagePaths = storagePaths;
this.protectAgainstEmptyClobber = Boolean(options.protectEmptyClobber);
if (this.protectAgainstEmptyClobber) {
logger.warn("Session-Schutz aktiv: Start mit unlesbarer Session — leere Speicherungen blockiert, bis echte Daten vorliegen");
}
this.debridService = new DebridService(settings, { this.debridService = new DebridService(settings, {
megaWebUnrestrict: options.megaWebUnrestrict, megaWebUnrestrict: options.megaWebUnrestrict,
allDebridWebUnrestrict: options.allDebridWebUnrestrict, allDebridWebUnrestrict: options.allDebridWebUnrestrict,
@ -5830,9 +5821,7 @@ export class DownloadManager extends EventEmitter {
const itemCount = Object.keys(this.session.items).length; const itemCount = Object.keys(this.session.items).length;
logger.info(`Shutdown-Save: ${pkgCount} Pakete, ${itemCount} Items`); logger.info(`Shutdown-Save: ${pkgCount} Pakete, ${itemCount} Items`);
this.foldRuntimeIntoSettings(nowMs()); this.foldRuntimeIntoSettings(nowMs());
if (!this.guardBlocksSessionSave()) { saveSession(this.storagePaths, this.session);
saveSession(this.storagePaths, this.session);
}
saveSettings(this.storagePaths, this.settings); saveSettings(this.storagePaths, this.settings);
} else { } else {
logger.info(`Shutdown-Save übersprungen: skipShutdownPersist=${this.skipShutdownPersist}, blockAllPersistence=${this.blockAllPersistence}`); logger.info(`Shutdown-Save übersprungen: skipShutdownPersist=${this.skipShutdownPersist}, blockAllPersistence=${this.blockAllPersistence}`);
@ -6135,29 +6124,10 @@ export class DownloadManager extends EventEmitter {
}, delay); }, delay);
} }
private guardBlocksSessionSave(): boolean {
if (!this.protectAgainstEmptyClobber) {
return false;
}
const isEmpty = Object.keys(this.session.packages).length === 0 && Object.keys(this.session.items).length === 0;
if (isEmpty) {
if (!this.emptyClobberProtectionLogged) {
logger.warn("Leere Session-Speicherung uebersprungen (Schutz nach unlesbarem Start) — vorhandene Datei bleibt unangetastet");
this.emptyClobberProtectionLogged = true;
}
return true;
}
this.protectAgainstEmptyClobber = false;
logger.info("Session-Schutz aufgehoben: nicht-leere Session wird wieder normal gespeichert");
return false;
}
private persistNow(): void { private persistNow(): void {
const now = nowMs(); const now = nowMs();
this.lastPersistAt = now; this.lastPersistAt = now;
if (!this.guardBlocksSessionSave()) { void saveSessionAsync(this.storagePaths, this.session).catch((err) => logger.warn(`saveSessionAsync Fehler: ${compactErrorText(err)}`));
void saveSessionAsync(this.storagePaths, this.session).catch((err) => logger.warn(`saveSessionAsync Fehler: ${compactErrorText(err)}`));
}
if (now - this.lastSettingsPersistAt >= 30000) { if (now - this.lastSettingsPersistAt >= 30000) {
this.foldRuntimeIntoSettings(now); this.foldRuntimeIntoSettings(now);
this.lastSettingsPersistAt = now; this.lastSettingsPersistAt = now;
@ -6171,9 +6141,7 @@ export class DownloadManager extends EventEmitter {
const itemCount = Object.keys(this.session.items).length; const itemCount = Object.keys(this.session.items).length;
logger.info(`Pre-Update Sync-Save: ${pkgCount} Pakete, ${itemCount} Items`); logger.info(`Pre-Update Sync-Save: ${pkgCount} Pakete, ${itemCount} Items`);
this.foldRuntimeIntoSettings(nowMs()); this.foldRuntimeIntoSettings(nowMs());
if (!this.guardBlocksSessionSave()) { saveSession(this.storagePaths, this.session);
saveSession(this.storagePaths, this.session);
}
saveSettings(this.storagePaths, this.settings); saveSettings(this.storagePaths, this.settings);
} }

View File

@ -890,50 +890,21 @@ export function normalizeLoadedSessionTransientFields(session: SessionState): Se
return session; return session;
} }
const TRANSIENT_READ_CODES = new Set(["EBUSY", "EPERM", "EAGAIN"]);
function sleepSyncMs(ms: number): void {
if (ms <= 0) {
return;
}
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
}
function readSessionFile(filePath: string): SessionState | null { function readSessionFile(filePath: string): SessionState | null {
let raw: string | null = null;
const maxAttempts = 5;
for (let attempt = 1; attempt <= maxAttempts; attempt += 1) {
try {
raw = fs.readFileSync(filePath, "utf8");
break;
} catch (error) {
const code = (error as NodeJS.ErrnoException)?.code || "";
if (TRANSIENT_READ_CODES.has(code) && attempt < maxAttempts) {
const backoffMs = 100 * 2 ** (attempt - 1);
logger.warn(`Session-Datei vorübergehend gesperrt (${code}), Versuch ${attempt}/${maxAttempts}, warte ${backoffMs}ms: ${filePath}`);
sleepSyncMs(backoffMs);
continue;
}
if (code === "EACCES" || code === "EPERM") {
logger.error(`Session-Datei nicht zugreifbar (${code}): ${filePath} - pruefe Datei-/Ordner-Berechtigungen fuer Benutzer ${process.env.USERNAME || process.env.USER || "?"}`);
} else {
logger.error(`Session-Datei nicht lesbar (${code || "?"}): ${filePath}: ${String(error)}`);
}
return null;
}
}
if (raw === null) {
return null;
}
try { try {
const parsed = JSON.parse(raw) as unknown; const parsed = JSON.parse(fs.readFileSync(filePath, "utf8")) as unknown;
const session = normalizeLoadedSessionTransientFields(normalizeLoadedSession(parsed)); const session = normalizeLoadedSessionTransientFields(normalizeLoadedSession(parsed));
const pkgCount = Object.keys(session.packages).length; const pkgCount = Object.keys(session.packages).length;
const itemCount = Object.keys(session.items).length; const itemCount = Object.keys(session.items).length;
logger.info(`Session geladen: ${filePath} (${pkgCount} Pakete, ${itemCount} Items)`); logger.info(`Session geladen: ${filePath} (${pkgCount} Pakete, ${itemCount} Items)`);
return session; return session;
} catch (error) { } catch (error) {
logger.error(`Session-Datei beschädigt (JSON ungültig): ${filePath}: ${String(error)}`); const code = (error as NodeJS.ErrnoException)?.code || "";
if (code === "EACCES" || code === "EPERM") {
logger.error(`Session-Datei nicht zugreifbar (${code}): ${filePath} - pruefe Datei-/Ordner-Berechtigungen fuer Benutzer ${process.env.USERNAME || process.env.USER || "?"}`);
} else {
logger.error(`Session-Datei nicht lesbar: ${filePath}: ${String(error)}`);
}
return null; return null;
} }
} }
@ -1033,31 +1004,17 @@ export function emptySession(): SessionState {
}; };
} }
export type SessionLoadStatus = export function loadSession(paths: StoragePaths): SessionState {
| "ok"
| "recovered-backup"
| "recovered-temp"
| "empty-fresh"
| "empty-unreadable";
export interface SessionLoadResult {
session: SessionState;
status: SessionLoadStatus;
}
export function loadSessionWithStatus(paths: StoragePaths): SessionLoadResult {
ensureBaseDir(paths.baseDir); ensureBaseDir(paths.baseDir);
const backupFile = sessionBackupPath(paths.sessionFile); const backupFile = sessionBackupPath(paths.sessionFile);
const syncTempFile = sessionTempPath(paths.sessionFile, "sync");
const asyncTempFile = sessionTempPath(paths.sessionFile, "async");
const primaryExists = fs.existsSync(paths.sessionFile); const primaryExists = fs.existsSync(paths.sessionFile);
const backupExists = fs.existsSync(backupFile);
const anyTempExists = fs.existsSync(syncTempFile) || fs.existsSync(asyncTempFile);
if (!primaryExists) { if (!primaryExists) {
if (!backupExists && !anyTempExists) { const hasRecoverable = fs.existsSync(backupFile)
|| fs.existsSync(sessionTempPath(paths.sessionFile, "sync"))
|| fs.existsSync(sessionTempPath(paths.sessionFile, "async"));
if (!hasRecoverable) {
logger.info("Keine Session-Datei vorhanden, starte mit leerer Session"); logger.info("Keine Session-Datei vorhanden, starte mit leerer Session");
return { session: emptySession(), status: "empty-fresh" }; return emptySession();
} }
logger.warn("Session-Primaerdatei fehlt, aber Backup/Temp vorhanden — Wiederherstellung wird versucht"); logger.warn("Session-Primaerdatei fehlt, aber Backup/Temp vorhanden — Wiederherstellung wird versucht");
} }
@ -1066,7 +1023,7 @@ export function loadSessionWithStatus(paths: StoragePaths): SessionLoadResult {
if (primary) { if (primary) {
const primaryPkgCount = Object.keys(primary.packages).length; const primaryPkgCount = Object.keys(primary.packages).length;
if (primaryPkgCount === 0 && backupExists) { if (primaryPkgCount === 0 && fs.existsSync(backupFile)) {
const backup = readSessionFile(backupFile); const backup = readSessionFile(backupFile);
if (backup) { if (backup) {
const backupPkgCount = Object.keys(backup.packages).length; const backupPkgCount = Object.keys(backup.packages).length;
@ -1074,27 +1031,29 @@ export function loadSessionWithStatus(paths: StoragePaths): SessionLoadResult {
logger.warn(`Session-Datei ist leer (0 Pakete), aber Backup hat ${backupPkgCount} Pakete — verwende Backup`); logger.warn(`Session-Datei ist leer (0 Pakete), aber Backup hat ${backupPkgCount} Pakete — verwende Backup`);
try { try {
const payload = JSON.stringify({ ...backup, updatedAt: Date.now() }, safeJsonReplacer); const payload = JSON.stringify({ ...backup, updatedAt: Date.now() }, safeJsonReplacer);
fs.writeFileSync(syncTempFile, payload, "utf8"); const tempPath = sessionTempPath(paths.sessionFile, "sync");
syncRenameWithExdevFallback(syncTempFile, paths.sessionFile); fs.writeFileSync(tempPath, payload, "utf8");
syncRenameWithExdevFallback(tempPath, paths.sessionFile);
} catch { } catch {
} }
return { session: backup, status: "recovered-backup" }; return backup;
} }
} }
} }
return { session: primary, status: "ok" }; return primary;
} }
const backup = backupExists ? readSessionFile(backupFile) : null; const backup = fs.existsSync(backupFile) ? readSessionFile(backupFile) : null;
if (backup) { if (backup) {
logger.warn("Session defekt, Backup-Datei wird verwendet"); logger.warn("Session defekt, Backup-Datei wird verwendet");
try { try {
const payload = JSON.stringify({ ...backup, updatedAt: Date.now() }, safeJsonReplacer); const payload = JSON.stringify({ ...backup, updatedAt: Date.now() }, safeJsonReplacer);
fs.writeFileSync(syncTempFile, payload, "utf8"); const tempPath = sessionTempPath(paths.sessionFile, "sync");
syncRenameWithExdevFallback(syncTempFile, paths.sessionFile); fs.writeFileSync(tempPath, payload, "utf8");
syncRenameWithExdevFallback(tempPath, paths.sessionFile);
} catch { } catch {
} }
return { session: backup, status: "recovered-backup" }; return backup;
} }
for (const kind of ["sync", "async"] as const) { for (const kind of ["sync", "async"] as const) {
@ -1108,21 +1067,13 @@ export function loadSessionWithStatus(paths: StoragePaths): SessionLoadResult {
fs.writeFileSync(paths.sessionFile, payload, "utf8"); fs.writeFileSync(paths.sessionFile, payload, "utf8");
} catch { } catch {
} }
return { session: tmpSession, status: "recovered-temp" }; return tmpSession;
} }
} }
} }
if (primaryExists || backupExists || anyTempExists) { logger.error("Session konnte nicht geladen werden (Primary, Backup und Temp-Dateien fehlgeschlagen)");
logger.error("Session konnte nicht geladen werden (Primary, Backup und Temp-Dateien fehlgeschlagen) — Schutz gegen leeres Ueberschreiben aktiv"); return emptySession();
return { session: emptySession(), status: "empty-unreadable" };
}
return { session: emptySession(), status: "empty-fresh" };
}
export function loadSession(paths: StoragePaths): SessionState {
return loadSessionWithStatus(paths).session;
} }
export function saveSession(paths: StoragePaths, session: SessionState): void { export function saveSession(paths: StoragePaths, session: SessionState): void {
@ -1137,13 +1088,7 @@ export function saveSession(paths: StoragePaths, session: SessionState): void {
const payload = JSON.stringify({ ...session, updatedAt: Date.now() }, safeJsonReplacer); const payload = JSON.stringify({ ...session, updatedAt: Date.now() }, safeJsonReplacer);
const tempPath = sessionTempPath(paths.sessionFile, "sync"); const tempPath = sessionTempPath(paths.sessionFile, "sync");
try { try {
const fd = fs.openSync(tempPath, "w"); fs.writeFileSync(tempPath, payload, "utf8");
try {
fs.writeSync(fd, payload);
fs.fsyncSync(fd);
} finally {
fs.closeSync(fd);
}
syncRenameWithExdevFallback(tempPath, paths.sessionFile); syncRenameWithExdevFallback(tempPath, paths.sessionFile);
} catch (error) { } catch (error) {
try { fs.rmSync(tempPath, { force: true }); } catch { } try { fs.rmSync(tempPath, { force: true }); } catch { }
@ -1159,13 +1104,7 @@ async function writeSessionPayload(paths: StoragePaths, payload: string, generat
await fs.promises.mkdir(paths.baseDir, { recursive: true }); await fs.promises.mkdir(paths.baseDir, { recursive: true });
await fsp.copyFile(paths.sessionFile, sessionBackupPath(paths.sessionFile)).catch(() => {}); await fsp.copyFile(paths.sessionFile, sessionBackupPath(paths.sessionFile)).catch(() => {});
const tempPath = sessionTempPath(paths.sessionFile, "async"); const tempPath = sessionTempPath(paths.sessionFile, "async");
const handle = await fsp.open(tempPath, "w"); await fsp.writeFile(tempPath, payload, "utf8");
try {
await handle.writeFile(payload, "utf8");
await handle.sync();
} finally {
await handle.close();
}
if (generation < syncSaveGeneration) { if (generation < syncSaveGeneration) {
await fsp.rm(tempPath, { force: true }).catch(() => {}); await fsp.rm(tempPath, { force: true }).catch(() => {});
return; return;

Binary file not shown.