Round 7 bug fixes (13 fixes)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
fad0f1060b
commit
75775f2798
@ -317,6 +317,9 @@ export class AppController {
|
|||||||
// Prevent prepareForShutdown from overwriting the restored session file
|
// Prevent prepareForShutdown from overwriting the restored session file
|
||||||
// with the old in-memory session when the app quits after backup restore.
|
// with the old in-memory session when the app quits after backup restore.
|
||||||
this.manager.skipShutdownPersist = true;
|
this.manager.skipShutdownPersist = true;
|
||||||
|
// Block all persistence (including persistSoon from any IPC operations
|
||||||
|
// the user might trigger before restarting) to protect the restored backup.
|
||||||
|
this.manager.blockAllPersistence = true;
|
||||||
return { restored: true, message: "Backup wiederhergestellt. Bitte App neustarten." };
|
return { restored: true, message: "Backup wiederhergestellt. Bitte App neustarten." };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -803,6 +803,10 @@ export class DownloadManager extends EventEmitter {
|
|||||||
|
|
||||||
public skipShutdownPersist = false;
|
public skipShutdownPersist = false;
|
||||||
|
|
||||||
|
/** Block ALL persistence (persistSoon + shutdown). Set after importBackup to prevent
|
||||||
|
* the old in-memory session from overwriting the restored backup on disk. */
|
||||||
|
public blockAllPersistence = false;
|
||||||
|
|
||||||
private debridService: DebridService;
|
private debridService: DebridService;
|
||||||
|
|
||||||
private invalidateMegaSessionFn?: () => void;
|
private invalidateMegaSessionFn?: () => void;
|
||||||
@ -1083,7 +1087,7 @@ export class DownloadManager extends EventEmitter {
|
|||||||
seen.add(id);
|
seen.add(id);
|
||||||
return true;
|
return true;
|
||||||
});
|
});
|
||||||
const remaining = this.session.packageOrder.filter((id) => !valid.includes(id));
|
const remaining = this.session.packageOrder.filter((id) => !seen.has(id));
|
||||||
this.session.packageOrder = [...valid, ...remaining];
|
this.session.packageOrder = [...valid, ...remaining];
|
||||||
this.persistSoon();
|
this.persistSoon();
|
||||||
this.emitState(true);
|
this.emitState(true);
|
||||||
@ -3116,6 +3120,7 @@ export class DownloadManager extends EventEmitter {
|
|||||||
this.session.reconnectUntil = 0;
|
this.session.reconnectUntil = 0;
|
||||||
this.session.reconnectReason = "";
|
this.session.reconnectReason = "";
|
||||||
this.retryAfterByItem.clear();
|
this.retryAfterByItem.clear();
|
||||||
|
this.retryStateByItem.clear();
|
||||||
this.lastGlobalProgressBytes = this.session.totalDownloadedBytes;
|
this.lastGlobalProgressBytes = this.session.totalDownloadedBytes;
|
||||||
this.lastGlobalProgressAt = nowMs();
|
this.lastGlobalProgressAt = nowMs();
|
||||||
this.speedEvents = [];
|
this.speedEvents = [];
|
||||||
@ -3132,12 +3137,13 @@ export class DownloadManager extends EventEmitter {
|
|||||||
active.abortReason = "stop";
|
active.abortReason = "stop";
|
||||||
active.abortController.abort("stop");
|
active.abortController.abort("stop");
|
||||||
}
|
}
|
||||||
// Reset all non-finished items to clean "Wartet" state
|
// Reset all non-finished items to clean "Wartet" / "Paket gestoppt" state
|
||||||
for (const item of Object.values(this.session.items)) {
|
for (const item of Object.values(this.session.items)) {
|
||||||
if (!isFinishedStatus(item.status)) {
|
if (!isFinishedStatus(item.status)) {
|
||||||
item.status = "queued";
|
item.status = "queued";
|
||||||
item.speedBps = 0;
|
item.speedBps = 0;
|
||||||
item.fullStatus = "Wartet";
|
const pkg = this.session.packages[item.packageId];
|
||||||
|
item.fullStatus = pkg && !pkg.enabled ? "Paket gestoppt" : "Wartet";
|
||||||
item.updatedAt = nowMs();
|
item.updatedAt = nowMs();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -3229,7 +3235,7 @@ export class DownloadManager extends EventEmitter {
|
|||||||
this.session.summaryText = "";
|
this.session.summaryText = "";
|
||||||
// Persist synchronously on shutdown to guarantee data is written before process exits
|
// Persist synchronously on shutdown to guarantee data is written before process exits
|
||||||
// Skip if a backup was just imported — the restored session on disk must not be overwritten
|
// Skip if a backup was just imported — the restored session on disk must not be overwritten
|
||||||
if (!this.skipShutdownPersist) {
|
if (!this.skipShutdownPersist && !this.blockAllPersistence) {
|
||||||
saveSession(this.storagePaths, this.session);
|
saveSession(this.storagePaths, this.session);
|
||||||
saveSettings(this.storagePaths, this.settings);
|
saveSettings(this.storagePaths, this.settings);
|
||||||
}
|
}
|
||||||
@ -3494,7 +3500,7 @@ export class DownloadManager extends EventEmitter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private persistSoon(): void {
|
private persistSoon(): void {
|
||||||
if (this.persistTimer) {
|
if (this.persistTimer || this.blockAllPersistence) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -6343,18 +6349,9 @@ export class DownloadManager extends EventEmitter {
|
|||||||
extractCpuPriority: this.settings.extractCpuPriority,
|
extractCpuPriority: this.settings.extractCpuPriority,
|
||||||
onProgress: (progress) => {
|
onProgress: (progress) => {
|
||||||
if (progress.phase === "done") {
|
if (progress.phase === "done") {
|
||||||
// Mark all remaining active archives as done
|
// Do NOT mark remaining archives as "Done" here — some may have
|
||||||
for (const [archName, archItems] of activeHybridArchiveMap) {
|
// failed. The post-extraction code (result.failed check) will
|
||||||
const doneAt = nowMs();
|
// assign the correct label. Only clear the tracking maps.
|
||||||
const startedAt = hybridArchiveStartTimes.get(archName) || doneAt;
|
|
||||||
const doneLabel = formatExtractDone(doneAt - startedAt);
|
|
||||||
for (const entry of archItems) {
|
|
||||||
if (!isExtractedLabel(entry.fullStatus)) {
|
|
||||||
entry.fullStatus = doneLabel;
|
|
||||||
entry.updatedAt = doneAt;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
activeHybridArchiveMap.clear();
|
activeHybridArchiveMap.clear();
|
||||||
hybridArchiveStartTimes.clear();
|
hybridArchiveStartTimes.clear();
|
||||||
return;
|
return;
|
||||||
@ -6639,18 +6636,9 @@ export class DownloadManager extends EventEmitter {
|
|||||||
extractCpuPriority: this.settings.extractCpuPriority,
|
extractCpuPriority: this.settings.extractCpuPriority,
|
||||||
onProgress: (progress) => {
|
onProgress: (progress) => {
|
||||||
if (progress.phase === "done") {
|
if (progress.phase === "done") {
|
||||||
// Mark all remaining active archives as done
|
// Do NOT mark remaining archives as "Done" here — some may have
|
||||||
for (const [archName, items] of activeArchiveItemsMap) {
|
// failed. The post-extraction code (result.failed check) will
|
||||||
const doneAt = nowMs();
|
// assign the correct label. Only clear the tracking maps.
|
||||||
const startedAt = archiveStartTimes.get(archName) || doneAt;
|
|
||||||
const doneLabel = formatExtractDone(doneAt - startedAt);
|
|
||||||
for (const entry of items) {
|
|
||||||
if (!isExtractedLabel(entry.fullStatus)) {
|
|
||||||
entry.fullStatus = doneLabel;
|
|
||||||
entry.updatedAt = doneAt;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
activeArchiveItemsMap.clear();
|
activeArchiveItemsMap.clear();
|
||||||
archiveStartTimes.clear();
|
archiveStartTimes.clear();
|
||||||
emitExtractStatus("Entpacken 100%", true);
|
emitExtractStatus("Entpacken 100%", true);
|
||||||
@ -6786,9 +6774,9 @@ export class DownloadManager extends EventEmitter {
|
|||||||
for (const entry of completedItems) {
|
for (const entry of completedItems) {
|
||||||
if (/^Entpacken/i.test(entry.fullStatus || "") || /^Passwort/i.test(entry.fullStatus || "")) {
|
if (/^Entpacken/i.test(entry.fullStatus || "") || /^Passwort/i.test(entry.fullStatus || "")) {
|
||||||
entry.fullStatus = "Entpacken abgebrochen (wird fortgesetzt)";
|
entry.fullStatus = "Entpacken abgebrochen (wird fortgesetzt)";
|
||||||
}
|
|
||||||
entry.updatedAt = nowMs();
|
entry.updatedAt = nowMs();
|
||||||
}
|
}
|
||||||
|
}
|
||||||
pkg.status = (pkg.enabled && !this.session.paused) ? "queued" : "paused";
|
pkg.status = (pkg.enabled && !this.session.paused) ? "queued" : "paused";
|
||||||
pkg.updatedAt = nowMs();
|
pkg.updatedAt = nowMs();
|
||||||
logger.info(`Post-Processing Entpacken abgebrochen: pkg=${pkg.name}`);
|
logger.info(`Post-Processing Entpacken abgebrochen: pkg=${pkg.name}`);
|
||||||
@ -6801,9 +6789,9 @@ export class DownloadManager extends EventEmitter {
|
|||||||
for (const entry of completedItems) {
|
for (const entry of completedItems) {
|
||||||
if (!isExtractedLabel(entry.fullStatus)) {
|
if (!isExtractedLabel(entry.fullStatus)) {
|
||||||
entry.fullStatus = `Entpack-Fehler: ${reason}`;
|
entry.fullStatus = `Entpack-Fehler: ${reason}`;
|
||||||
}
|
|
||||||
entry.updatedAt = nowMs();
|
entry.updatedAt = nowMs();
|
||||||
}
|
}
|
||||||
|
}
|
||||||
pkg.status = "failed";
|
pkg.status = "failed";
|
||||||
}
|
}
|
||||||
} finally {
|
} finally {
|
||||||
|
|||||||
@ -2195,7 +2195,7 @@ export async function extractPackageArchives(options: ExtractOptions): Promise<{
|
|||||||
for (const nestedArchive of nestedCandidates) {
|
for (const nestedArchive of nestedCandidates) {
|
||||||
if (options.signal?.aborted) throw new Error("aborted:extract");
|
if (options.signal?.aborted) throw new Error("aborted:extract");
|
||||||
const nestedName = path.basename(nestedArchive);
|
const nestedName = path.basename(nestedArchive);
|
||||||
const nestedKey = archiveNameKey(nestedName);
|
const nestedKey = archiveNameKey(`nested:${nestedName}`);
|
||||||
if (resumeCompleted.has(nestedKey)) {
|
if (resumeCompleted.has(nestedKey)) {
|
||||||
logger.info(`Nested-Extraction übersprungen (bereits entpackt): ${nestedName}`);
|
logger.info(`Nested-Extraction übersprungen (bereits entpackt): ${nestedName}`);
|
||||||
continue;
|
continue;
|
||||||
@ -2258,7 +2258,7 @@ export async function extractPackageArchives(options: ExtractOptions): Promise<{
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (extracted > 0) {
|
if (extracted > 0) {
|
||||||
const hasOutputAfter = await hasAnyEntries(options.targetDir);
|
const hasOutputAfter = await hasAnyFilesRecursive(options.targetDir);
|
||||||
const hadResumeProgress = resumeCompletedAtStart > 0;
|
const hadResumeProgress = resumeCompletedAtStart > 0;
|
||||||
if (!hasOutputAfter && conflictMode !== "skip" && !hadResumeProgress) {
|
if (!hasOutputAfter && conflictMode !== "skip" && !hadResumeProgress) {
|
||||||
lastError = "Keine entpackten Dateien erkannt";
|
lastError = "Keine entpackten Dateien erkannt";
|
||||||
|
|||||||
@ -78,6 +78,11 @@ async function sleepWithSignal(ms: number, signal?: AbortSignal): Promise<void>
|
|||||||
await sleep(ms);
|
await sleep(ms);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
// Check before entering the Promise constructor to avoid a race where the timer
|
||||||
|
// resolves before the aborted check runs (especially when ms=0).
|
||||||
|
if (signal.aborted) {
|
||||||
|
throw new Error("aborted");
|
||||||
|
}
|
||||||
await new Promise<void>((resolve, reject) => {
|
await new Promise<void>((resolve, reject) => {
|
||||||
let timer: NodeJS.Timeout | null = setTimeout(() => {
|
let timer: NodeJS.Timeout | null = setTimeout(() => {
|
||||||
timer = null;
|
timer = null;
|
||||||
@ -94,10 +99,6 @@ async function sleepWithSignal(ms: number, signal?: AbortSignal): Promise<void>
|
|||||||
reject(new Error("aborted"));
|
reject(new Error("aborted"));
|
||||||
};
|
};
|
||||||
|
|
||||||
if (signal.aborted) {
|
|
||||||
onAbort();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
signal.addEventListener("abort", onAbort, { once: true });
|
signal.addEventListener("abort", onAbort, { once: true });
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@ -3331,24 +3331,14 @@ const PackageCard = memo(function PackageCard({ pkg, items, packageSpeed, isFirs
|
|||||||
</span>
|
</span>
|
||||||
</span>
|
</span>
|
||||||
);
|
);
|
||||||
case "hoster": return (
|
case "hoster": {
|
||||||
<span key={col} className="pkg-col pkg-col-hoster" title={(() => {
|
const hosterText = [...new Set(items.map((item) => extractHoster(item.url)).filter(Boolean))].join(", ");
|
||||||
const hosters = [...new Set(items.map((item) => extractHoster(item.url)).filter(Boolean))];
|
return <span key={col} className="pkg-col pkg-col-hoster" title={hosterText}>{hosterText}</span>;
|
||||||
return hosters.join(", ");
|
}
|
||||||
})()}>{(() => {
|
case "account": {
|
||||||
const hosters = [...new Set(items.map((item) => extractHoster(item.url)).filter(Boolean))];
|
const accountText = [...new Set(items.map((item) => item.provider).filter(Boolean))].map((p) => providerLabels[p!] || p).join(", ");
|
||||||
return hosters.length > 0 ? hosters.join(", ") : "";
|
return <span key={col} className="pkg-col pkg-col-account" title={accountText}>{accountText}</span>;
|
||||||
})()}</span>
|
}
|
||||||
);
|
|
||||||
case "account": return (
|
|
||||||
<span key={col} className="pkg-col pkg-col-account" title={(() => {
|
|
||||||
const providers = [...new Set(items.map((item) => item.provider).filter(Boolean))];
|
|
||||||
return providers.map((p) => providerLabels[p!] || p).join(", ");
|
|
||||||
})()}>{(() => {
|
|
||||||
const providers = [...new Set(items.map((item) => item.provider).filter(Boolean))];
|
|
||||||
return providers.length > 0 ? providers.map((p) => providerLabels[p!] || p).join(", ") : "";
|
|
||||||
})()}</span>
|
|
||||||
);
|
|
||||||
case "prio": return (
|
case "prio": return (
|
||||||
<span key={col} className={`pkg-col pkg-col-prio${pkg.priority === "high" ? " prio-high" : pkg.priority === "low" ? " prio-low" : ""}`}>{pkg.priority === "high" ? "Hoch" : pkg.priority === "low" ? "Niedrig" : ""}</span>
|
<span key={col} className={`pkg-col pkg-col-prio${pkg.priority === "high" ? " prio-high" : pkg.priority === "low" ? " prio-low" : ""}`}>{pkg.priority === "high" ? "Hoch" : pkg.priority === "low" ? "Niedrig" : ""}</span>
|
||||||
);
|
);
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user