From 113b34fadf49b3d3345e0d507d6dc4d7ab71b105 Mon Sep 17 00:00:00 2001 From: Sucukdeluxe Date: Tue, 10 Mar 2026 19:34:42 +0100 Subject: [PATCH] Fix parallel extraction false positives --- src/main/extractor.ts | 47 +++++++++++++++++++++++++++++++++++++++++ tests/extractor.test.ts | 14 ++++++++++++ 2 files changed, 61 insertions(+) diff --git a/src/main/extractor.ts b/src/main/extractor.ts index 75b781a..1372555 100644 --- a/src/main/extractor.ts +++ b/src/main/extractor.ts @@ -651,6 +651,23 @@ export function classifyExtractionError(errorText: string): ExtractErrorCategory return "unknown"; } +export function shouldSerialRetryParallelFailures( + extractedCount: number, + failedCategories: ExtractErrorCategory[] +): boolean { + if (failedCategories.length === 0) { + return false; + } + if (extractedCount > 0) { + return true; + } + return failedCategories.every((category) => + category === "crc_error" + || category === "wrong_password" + || category === "unknown" + ); +} + export function shouldFallbackLegacyRarToJvm( archivePath: string, configuredMode: ExtractBackendMode, @@ -3001,6 +3018,7 @@ export async function extractPackageArchives(options: ExtractOptions): Promise<{ let learnedPassword = cachedPackagePassword; let packageNeedsFlatMode = false; const extractedArchives = new Set(); + const failedArchiveCategories = new Map(); for (const archivePath of candidates) { if (resumeCompleted.has(archiveNameKey(path.basename(archivePath)))) { extractedArchives.add(archivePath); @@ -3201,6 +3219,7 @@ export async function extractPackageArchives(options: ExtractOptions): Promise<{ } extracted += 1; extractedArchives.add(archivePath); + failedArchiveCategories.delete(archivePath); resumeCompleted.add(archiveResumeKey); await writeExtractResumeState(options.packageDir, resumeCompleted, options.packageId); logger.info(`Entpacken erfolgreich: ${path.basename(archivePath)}`); @@ -3224,6 +3243,7 @@ export async function extractPackageArchives(options: ExtractOptions): Promise<{ failed += 1; lastError = errorText; const errorCategory = classifyExtractionError(errorText); + failedArchiveCategories.set(archivePath, errorCategory); const hintedError = error as ExtractionErrorWithHints; options.onArchiveFailure?.({ archiveName, @@ -3329,6 +3349,33 @@ export async function extractPackageArchives(options: ExtractOptions): Promise<{ if (abortError) throw new Error("aborted:extract"); + if (failed > 0 && extracted === 0) { + const failedArchives = parallelQueue.filter((ap) => !extractedArchives.has(ap) && !resumeCompleted.has(archiveNameKey(path.basename(ap)))); + const failedCategories = failedArchives.map((archivePath) => failedArchiveCategories.get(archivePath) || "unknown"); + if (failedArchives.length > 0 && shouldSerialRetryParallelFailures(extracted, failedCategories)) { + const categorySummary = [...new Set(failedCategories)].join(","); + logger.info( + `Serielle Wiederholung nach Parallel-Fehlstart: ${failedArchives.length} Archive werden einzeln wiederholt ` + + `(categories=${categorySummary || "unknown"})` + ); + let retryRecovered = 0; + for (const archivePath of failedArchives) { + if (options.signal?.aborted || noExtractorEncountered) break; + try { + failed -= 1; + await extractSingleArchive(archivePath); + retryRecovered += 1; + } catch (retryError) { + const errText = String(retryError); + if (isExtractAbortError(errText)) throw retryError; + } + } + if (retryRecovered > 0) { + logger.info(`Serielle Wiederholung nach Parallel-Fehlstart: ${retryRecovered}/${failedArchives.length} Archive erfolgreich entpackt`); + } + } + } + // ── Retry failed wrong_password archives serially ── // Parallel UnRAR processes writing to the same target directory can cause // CRC mismatches that are misreported as "Incorrect password". diff --git a/tests/extractor.test.ts b/tests/extractor.test.ts index fd82002..286ad33 100644 --- a/tests/extractor.test.ts +++ b/tests/extractor.test.ts @@ -12,6 +12,7 @@ import { archiveFilenamePasswords, detectArchiveSignature, classifyExtractionError, + shouldSerialRetryParallelFailures, findArchiveCandidates, orderExtractorCandidatesForArchive, resolveExtractorBackendModeForArchive, @@ -1048,6 +1049,19 @@ describe("extractor", () => { }); }); + describe("shouldSerialRetryParallelFailures", () => { + it("keeps serial recovery enabled after mixed parallel results", () => { + expect(shouldSerialRetryParallelFailures(1, ["wrong_password"])).toBe(true); + expect(shouldSerialRetryParallelFailures(2, ["missing_parts"])).toBe(true); + }); + + it("only retries a total parallel wipe-out for contention-like failures", () => { + expect(shouldSerialRetryParallelFailures(0, ["crc_error", "wrong_password", "unknown"])).toBe(true); + expect(shouldSerialRetryParallelFailures(0, ["missing_parts"])).toBe(false); + expect(shouldSerialRetryParallelFailures(0, ["unsupported_format", "crc_error"])).toBe(false); + }); + }); + describe("password discovery", () => { it("reports per-archive failures through onArchiveFailure", async () => { const root = fs.mkdtempSync(path.join(os.tmpdir(), "rd-extract-failure-"));