Fix extraction completion and password prioritization

This commit is contained in:
Sucukdeluxe 2026-03-05 14:11:30 +01:00
parent 6e00bbab53
commit babcd8edb7
2 changed files with 98 additions and 32 deletions

View File

@ -6396,6 +6396,7 @@ export class DownloadManager extends EventEmitter {
const hybridResolvedItems = new Map<string, DownloadItem[]>();
const hybridStartTimes = new Map<string, number>();
let hybridLastEmitAt = 0;
let hybridLastProgressCurrent: number | null = null;
// Mark items based on whether their archive is actually ready for extraction.
// Only items whose archive is in readyArchives get "Ausstehend"; others keep
@ -6443,9 +6444,15 @@ export class DownloadManager extends EventEmitter {
if (progress.phase === "done") {
hybridResolvedItems.clear();
hybridStartTimes.clear();
hybridLastProgressCurrent = null;
return;
}
const currentCount = Math.max(0, Number(progress.current ?? 0));
const archiveFinished = progress.archiveDone === true
|| (hybridLastProgressCurrent !== null && currentCount > hybridLastProgressCurrent);
hybridLastProgressCurrent = currentCount;
if (progress.archiveName) {
// Resolve items for this archive if not yet tracked
if (!hybridResolvedItems.has(progress.archiveName)) {
@ -6470,11 +6477,14 @@ export class DownloadManager extends EventEmitter {
}
const archItems = hybridResolvedItems.get(progress.archiveName) || [];
// If archive is at 100%, mark its items as done and remove from active
if (Number(progress.archivePercent ?? 0) >= 100) {
// Only mark as finished on explicit archive-done signal (or real current increment),
// never on raw 100% archivePercent, because password retries can report 100% mid-run.
if (archiveFinished) {
const doneAt = nowMs();
const startedAt = hybridStartTimes.get(progress.archiveName) || doneAt;
const doneLabel = formatExtractDone(doneAt - startedAt);
const doneLabel = progress.archiveSuccess === false
? "Entpacken - Error"
: formatExtractDone(doneAt - startedAt);
for (const entry of archItems) {
if (!isExtractedLabel(entry.fullStatus)) {
entry.fullStatus = doneLabel;
@ -6484,7 +6494,7 @@ export class DownloadManager extends EventEmitter {
hybridResolvedItems.delete(progress.archiveName);
hybridStartTimes.delete(progress.archiveName);
// Show transitional label while next archive initializes
const done = progress.current + 1;
const done = currentCount;
if (done < progress.total) {
pkg.postProcessLabel = `Entpacken (${done}/${progress.total}) - Naechstes Archiv...`;
this.emitState();
@ -6516,7 +6526,7 @@ export class DownloadManager extends EventEmitter {
}
// Update package-level label with overall extraction progress
const activeArchive = Number(progress.archivePercent ?? 0) > 0 ? 1 : 0;
const activeArchive = !archiveFinished && Number(progress.archivePercent ?? 0) > 0 ? 1 : 0;
const currentDisplay = Math.max(0, Math.min(progress.total, progress.current + activeArchive));
if (progress.passwordFound) {
pkg.postProcessLabel = `Passwort gefunden · ${progress.archiveName || ""}`;
@ -6777,6 +6787,7 @@ export class DownloadManager extends EventEmitter {
// Track archives for parallel extraction progress
const fullResolvedItems = new Map<string, DownloadItem[]>();
const fullStartTimes = new Map<string, number>();
let fullLastProgressCurrent: number | null = null;
const result = await extractPackageArchives({
packageDir: pkg.outputDir,
@ -6802,10 +6813,16 @@ export class DownloadManager extends EventEmitter {
if (progress.phase === "done") {
fullResolvedItems.clear();
fullStartTimes.clear();
fullLastProgressCurrent = null;
emitExtractStatus("Entpacken 100%", true);
return;
}
const currentCount = Math.max(0, Number(progress.current ?? 0));
const archiveFinished = progress.archiveDone === true
|| (fullLastProgressCurrent !== null && currentCount > fullLastProgressCurrent);
fullLastProgressCurrent = currentCount;
if (progress.archiveName) {
// Resolve items for this archive if not yet tracked
if (!fullResolvedItems.has(progress.archiveName)) {
@ -6829,11 +6846,14 @@ export class DownloadManager extends EventEmitter {
}
const archiveItems = fullResolvedItems.get(progress.archiveName) || [];
// If archive is at 100%, mark its items as done and remove from active
if (Number(progress.archivePercent ?? 0) >= 100) {
// Only finalize on explicit archive completion (or real current increment),
// not on plain 100% archivePercent.
if (archiveFinished) {
const doneAt = nowMs();
const startedAt = fullStartTimes.get(progress.archiveName) || doneAt;
const doneLabel = formatExtractDone(doneAt - startedAt);
const doneLabel = progress.archiveSuccess === false
? "Entpacken - Error"
: formatExtractDone(doneAt - startedAt);
for (const entry of archiveItems) {
if (!isExtractedLabel(entry.fullStatus)) {
entry.fullStatus = doneLabel;
@ -6843,7 +6863,7 @@ export class DownloadManager extends EventEmitter {
fullResolvedItems.delete(progress.archiveName);
fullStartTimes.delete(progress.archiveName);
// Show transitional label while next archive initializes
const done = progress.current + 1;
const done = currentCount;
if (done < progress.total) {
emitExtractStatus(`Entpacken (${done}/${progress.total}) - Naechstes Archiv...`, true);
}
@ -6878,7 +6898,7 @@ export class DownloadManager extends EventEmitter {
const elapsed = progress.elapsedMs && progress.elapsedMs >= 1000
? ` · ${Math.floor(progress.elapsedMs / 1000)}s`
: "";
const activeArchive = Number(progress.archivePercent ?? 0) > 0 ? 1 : 0;
const activeArchive = !archiveFinished && Number(progress.archivePercent ?? 0) > 0 ? 1 : 0;
const currentDisplay = Math.max(0, Math.min(progress.total, progress.current + activeArchive));
let overallLabel: string;
if (progress.passwordFound) {

View File

@ -123,6 +123,8 @@ export interface ExtractProgressUpdate {
passwordAttempt?: number;
passwordTotal?: number;
passwordFound?: boolean;
archiveDone?: boolean;
archiveSuccess?: boolean;
}
const MAX_EXTRACT_OUTPUT_BUFFER = 48 * 1024;
@ -378,6 +380,19 @@ function parseProgressPercent(chunk: string): number | null {
return latest;
}
function nextArchivePercent(previous: number, incoming: number): number {
const prev = Math.max(0, Math.min(100, Math.floor(Number(previous) || 0)));
const next = Math.max(0, Math.min(100, Math.floor(Number(incoming) || 0)));
if (next >= prev) {
return next;
}
// Wrong-password retries can emit a fresh 0..100 run for the same archive.
if (prev >= 95 && next <= 5) {
return next;
}
return prev;
}
async function shouldPreferExternalZip(archivePath: string): Promise<boolean> {
if (extractorBackendMode() !== "legacy") {
return true;
@ -529,9 +544,12 @@ function prioritizePassword(passwords: string[], successful: string): string[] {
return passwords;
}
const index = passwords.findIndex((candidate) => candidate === target);
if (index <= 0) {
if (index === 0) {
return passwords;
}
if (index < 0) {
return [target, ...passwords.filter((candidate) => candidate !== target)];
}
const next = [...passwords];
const [value] = next.splice(index, 1);
next.unshift(value);
@ -961,9 +979,12 @@ function parseJvmLine(
if (trimmed.startsWith("RD_PROGRESS ")) {
const parsed = parseProgressPercent(trimmed);
if (parsed !== null && parsed > state.bestPercent) {
state.bestPercent = parsed;
onArchiveProgress?.(parsed);
if (parsed !== null) {
const next = nextArchivePercent(state.bestPercent, parsed);
if (next !== state.bestPercent) {
state.bestPercent = next;
onArchiveProgress?.(next);
}
}
return;
}
@ -1742,6 +1763,10 @@ async function runExternalExtractInner(
onArchiveProgress?.(0);
}
passwordAttempt += 1;
if (passwordAttempt > 1 && bestPercent > 0) {
bestPercent = 0;
onArchiveProgress?.(0);
}
const quotedPw = password === "" ? '""' : `"${password}"`;
logger.info(`Legacy-Passwort-Versuch ${passwordAttempt}/${passwords.length} für ${path.basename(archivePath)}: ${quotedPw}`);
if (passwords.length > 1) {
@ -1750,11 +1775,14 @@ async function runExternalExtractInner(
let args = buildExternalExtractArgs(command, archivePath, targetDir, conflictMode, password, usePerformanceFlags, hybridMode);
let result = await runExtractCommand(command, args, (chunk) => {
const parsed = parseProgressPercent(chunk);
if (parsed === null || parsed <= bestPercent) {
if (parsed === null) {
return;
}
bestPercent = parsed;
onArchiveProgress?.(bestPercent);
const next = nextArchivePercent(bestPercent, parsed);
if (next !== bestPercent) {
bestPercent = next;
onArchiveProgress?.(bestPercent);
}
}, signal, timeoutMs);
if (!result.ok && usePerformanceFlags && isUnsupportedExtractorSwitchError(result.errorText)) {
@ -1764,11 +1792,14 @@ async function runExternalExtractInner(
args = buildExternalExtractArgs(command, archivePath, targetDir, conflictMode, password, false, hybridMode);
result = await runExtractCommand(command, args, (chunk) => {
const parsed = parseProgressPercent(chunk);
if (parsed === null || parsed <= bestPercent) {
if (parsed === null) {
return;
}
bestPercent = parsed;
onArchiveProgress?.(bestPercent);
const next = nextArchivePercent(bestPercent, parsed);
if (next !== bestPercent) {
bestPercent = next;
onArchiveProgress?.(bestPercent);
}
}, signal, timeoutMs);
}
@ -2258,6 +2289,7 @@ export async function extractPackageArchives(options: ExtractOptions): Promise<{
let extracted = candidates.length - pendingCandidates.length;
let failed = 0;
let lastError = "";
let learnedPassword = "";
const extractedArchives = new Set<string>();
for (const archivePath of candidates) {
if (resumeCompleted.has(archiveNameKey(path.basename(archivePath)))) {
@ -2271,7 +2303,8 @@ export async function extractPackageArchives(options: ExtractOptions): Promise<{
phase: "extracting" | "done",
archivePercent?: number,
elapsedMs?: number,
pwInfo?: { passwordAttempt?: number; passwordTotal?: number; passwordFound?: boolean }
pwInfo?: { passwordAttempt?: number; passwordTotal?: number; passwordFound?: boolean },
archiveInfo?: { archiveDone?: boolean; archiveSuccess?: boolean }
): void => {
if (!options.onProgress) {
return;
@ -2292,6 +2325,7 @@ export async function extractPackageArchives(options: ExtractOptions): Promise<{
archivePercent,
elapsedMs,
phase,
...(archiveInfo || {}),
...(pwInfo || {})
});
} catch (error) {
@ -2306,7 +2340,7 @@ export async function extractPackageArchives(options: ExtractOptions): Promise<{
// rather than leaving them as "Entpacken - Ausstehend" until all extraction finishes.
for (const archivePath of candidates) {
if (resumeCompleted.has(archiveNameKey(path.basename(archivePath)))) {
emitProgress(extracted, path.basename(archivePath), "extracting", 100, 0);
emitProgress(extracted, path.basename(archivePath), "extracting", 100, 0, undefined, { archiveDone: true, archiveSuccess: true });
}
}
@ -2329,11 +2363,14 @@ export async function extractPackageArchives(options: ExtractOptions): Promise<{
emitProgress(extracted + failed, archiveName, "extracting", archivePercent, Date.now() - archiveStartedAt);
}, 1100);
const hybrid = Boolean(options.hybridMode);
// Insert archive-filename-derived passwords after "" but before custom passwords
// Before the first successful extraction, filename-derived candidates are useful.
// After a known password is learned, try that first to avoid per-archive delays.
const filenamePasswords = archiveFilenamePasswords(archiveName);
const archivePasswordCandidates = filenamePasswords.length > 0
? Array.from(new Set(["", ...filenamePasswords, ...passwordCandidates.filter((p) => p !== "")]))
: passwordCandidates;
const nonEmptyBasePasswords = passwordCandidates.filter((p) => p !== "");
const orderedNonEmpty = learnedPassword
? [learnedPassword, ...nonEmptyBasePasswords.filter((p) => p !== learnedPassword), ...filenamePasswords]
: [...filenamePasswords, ...nonEmptyBasePasswords];
const archivePasswordCandidates = Array.from(new Set(["", ...orderedNonEmpty]));
// Validate generic .001 splits via file signature before attempting extraction
const isGenericSplit = /\.\d{3}$/i.test(archiveName) && !/\.(zip|7z)\.\d{3}$/i.test(archiveName);
@ -2368,9 +2405,12 @@ export async function extractPackageArchives(options: ExtractOptions): Promise<{
if (preferExternal) {
try {
const usedPassword = await runExternalExtract(archivePath, options.targetDir, options.conflictMode, archivePasswordCandidates, (value) => {
archivePercent = Math.max(archivePercent, value);
archivePercent = nextArchivePercent(archivePercent, value);
emitProgress(extracted + failed, archiveName, "extracting", archivePercent, Date.now() - archiveStartedAt);
}, options.signal, hybrid, onPwAttempt);
if (usedPassword) {
learnedPassword = usedPassword;
}
passwordCandidates = prioritizePassword(passwordCandidates, usedPassword);
} catch (error) {
if (isNoExtractorError(String(error))) {
@ -2389,9 +2429,12 @@ export async function extractPackageArchives(options: ExtractOptions): Promise<{
}
try {
const usedPassword = await runExternalExtract(archivePath, options.targetDir, options.conflictMode, archivePasswordCandidates, (value) => {
archivePercent = Math.max(archivePercent, value);
archivePercent = nextArchivePercent(archivePercent, value);
emitProgress(extracted + failed, archiveName, "extracting", archivePercent, Date.now() - archiveStartedAt);
}, options.signal, hybrid, onPwAttempt);
if (usedPassword) {
learnedPassword = usedPassword;
}
passwordCandidates = prioritizePassword(passwordCandidates, usedPassword);
} catch (externalError) {
if (isNoExtractorError(String(externalError)) || isUnsupportedArchiveFormatError(String(externalError))) {
@ -2403,9 +2446,12 @@ export async function extractPackageArchives(options: ExtractOptions): Promise<{
}
} else {
const usedPassword = await runExternalExtract(archivePath, options.targetDir, options.conflictMode, archivePasswordCandidates, (value) => {
archivePercent = Math.max(archivePercent, value);
archivePercent = nextArchivePercent(archivePercent, value);
emitProgress(extracted + failed, archiveName, "extracting", archivePercent, Date.now() - archiveStartedAt);
}, options.signal, hybrid, onPwAttempt);
if (usedPassword) {
learnedPassword = usedPassword;
}
passwordCandidates = prioritizePassword(passwordCandidates, usedPassword);
}
extracted += 1;
@ -2415,9 +2461,9 @@ export async function extractPackageArchives(options: ExtractOptions): Promise<{
logger.info(`Entpacken erfolgreich: ${path.basename(archivePath)}`);
archivePercent = 100;
if (hasManyPasswords) {
emitProgress(extracted + failed, archiveName, "extracting", archivePercent, Date.now() - archiveStartedAt, { passwordFound: true });
emitProgress(extracted + failed, archiveName, "extracting", archivePercent, Date.now() - archiveStartedAt, { passwordFound: true }, { archiveDone: true, archiveSuccess: true });
} else {
emitProgress(extracted + failed, archiveName, "extracting", archivePercent, Date.now() - archiveStartedAt);
emitProgress(extracted + failed, archiveName, "extracting", archivePercent, Date.now() - archiveStartedAt, undefined, { archiveDone: true, archiveSuccess: true });
}
} catch (error) {
const errorText = String(error);
@ -2428,7 +2474,7 @@ export async function extractPackageArchives(options: ExtractOptions): Promise<{
lastError = errorText;
const errorCategory = classifyExtractionError(errorText);
logger.error(`Entpack-Fehler ${path.basename(archivePath)} [${errorCategory}]: ${errorText}`);
emitProgress(extracted + failed, archiveName, "extracting", archivePercent, Date.now() - archiveStartedAt);
emitProgress(extracted + failed, archiveName, "extracting", archivePercent, Date.now() - archiveStartedAt, undefined, { archiveDone: true, archiveSuccess: false });
if (isNoExtractorError(errorText)) {
noExtractorEncountered = true;
}