Release v1.4.10 with freeze mitigation and extraction throughput fixes
Build and Release / build (push) Has been cancelled

This commit is contained in:
Sucukdeluxe
2026-02-27 20:13:33 +01:00
parent 306826ecb9
commit 6e72c63268
6 changed files with 265 additions and 37 deletions
+84 -16
View File
@@ -173,6 +173,10 @@ export class DownloadManager extends EventEmitter {
private speedBytesLastWindow = 0;
private statsCache: DownloadStats | null = null;
private statsCacheAt = 0;
private lastPersistAt = 0;
private cleanupQueue: Promise<void> = Promise.resolve();
@@ -239,11 +243,10 @@ export class DownloadManager extends EventEmitter {
const paused = this.session.running && this.session.paused;
const speedBps = paused ? 0 : this.speedBytesLastWindow / 3;
let totalItems = Object.keys(this.session.items).length;
let doneItems = Object.values(this.session.items).filter((item) => isFinishedStatus(item.status)).length;
let totalItems = 0;
let doneItems = 0;
if (this.session.running && this.runItemIds.size > 0) {
totalItems = this.runItemIds.size;
doneItems = 0;
for (const itemId of this.runItemIds) {
if (this.runOutcomes.has(itemId)) {
doneItems += 1;
@@ -254,6 +257,14 @@ export class DownloadManager extends EventEmitter {
doneItems += 1;
}
}
} else {
const sessionItems = Object.values(this.session.items);
totalItems = sessionItems.length;
for (const item of sessionItems) {
if (isFinishedStatus(item.status)) {
doneItems += 1;
}
}
}
const elapsed = this.session.runStartedAt > 0 ? (now - this.session.runStartedAt) / 1000 : 0;
const rate = doneItems > 0 && elapsed > 0 ? doneItems / elapsed : 0;
@@ -266,7 +277,7 @@ export class DownloadManager extends EventEmitter {
settings: this.settings,
session: this.session,
summary: this.summary,
stats: this.getStats(),
stats: this.getStats(now),
speedText: `Geschwindigkeit: ${humanSize(Math.max(0, Math.floor(speedBps)))}/s`,
etaText: paused ? "ETA: --" : `ETA: ${formatEta(eta)}`,
canStart: !this.session.running,
@@ -277,7 +288,12 @@ export class DownloadManager extends EventEmitter {
};
}
public getStats(): DownloadStats {
public getStats(now = nowMs()): DownloadStats {
const itemCount = Object.keys(this.session.items).length;
if (this.statsCache && this.session.running && itemCount >= 500 && now - this.statsCacheAt < 1500) {
return this.statsCache;
}
let totalDownloaded = 0;
let totalFiles = 0;
for (const item of Object.values(this.session.items)) {
@@ -300,12 +316,15 @@ export class DownloadManager extends EventEmitter {
totalDownloaded = Math.max(totalDownloaded, this.session.totalDownloadedBytes);
}
return {
const stats = {
totalDownloaded,
totalFiles,
totalPackages: Object.keys(this.session.packages).length,
sessionStartedAt: this.session.runStartedAt
};
this.statsCache = stats;
this.statsCacheAt = now;
return stats;
}
public renamePackage(packageId: string, newName: string): void {
@@ -771,6 +790,7 @@ export class DownloadManager extends EventEmitter {
}
const cleanupTargetsByPackage = new Map<string, Set<string>>();
const dirFilesCache = new Map<string, string[]>();
for (const packageId of this.session.packageOrder) {
const pkg = this.session.packages[packageId];
if (!pkg || pkg.cancelled || pkg.status !== "completed") {
@@ -802,7 +822,20 @@ export class DownloadManager extends EventEmitter {
if (!targetPath || !isArchiveLikePath(targetPath)) {
continue;
}
for (const cleanupTarget of collectArchiveCleanupTargets(targetPath)) {
const dir = path.dirname(targetPath);
let filesInDir = dirFilesCache.get(dir);
if (!filesInDir) {
try {
filesInDir = fs.readdirSync(dir, { withFileTypes: true })
.filter((entry) => entry.isFile())
.map((entry) => entry.name);
} catch {
filesInDir = [];
}
dirFilesCache.set(dir, filesInDir);
}
for (const cleanupTarget of collectArchiveCleanupTargets(targetPath, filesInDir)) {
packageTargets.add(cleanupTarget);
}
}
@@ -860,8 +893,14 @@ export class DownloadManager extends EventEmitter {
if (!rootDir || !fs.existsSync(rootDir)) {
return false;
}
const deadline = nowMs() + 55;
let inspectedDirs = 0;
const stack = [rootDir];
while (stack.length > 0) {
inspectedDirs += 1;
if (inspectedDirs > 6000 || nowMs() > deadline) {
return true;
}
const current = stack.pop() as string;
let entries: fs.Dirent[] = [];
try {
@@ -1215,13 +1254,13 @@ export class DownloadManager extends EventEmitter {
const itemCount = Object.keys(this.session.items).length;
const minGapMs = this.session.running
? itemCount >= 1500
? 1300
? 3000
: itemCount >= 700
? 950
? 2200
: itemCount >= 250
? 700
: 450
: 250;
? 1500
: 700
: 300;
const sinceLastPersist = nowMs() - this.lastPersistAt;
const delay = Math.max(120, minGapMs - sinceLastPersist);
@@ -1251,12 +1290,12 @@ export class DownloadManager extends EventEmitter {
const itemCount = Object.keys(this.session.items).length;
const emitDelay = this.session.running
? itemCount >= 1500
? 900
? 1200
: itemCount >= 700
? 650
? 900
: itemCount >= 250
? 420
: 280
? 560
: 320
: 260;
this.stateEmitTimer = setTimeout(() => {
this.stateEmitTimer = null;
@@ -2568,6 +2607,35 @@ export class DownloadManager extends EventEmitter {
}
pkg.updatedAt = nowMs();
logger.info(`Post-Processing Ende: pkg=${pkg.name}, status=${pkg.status}`);
this.applyPackageDoneCleanup(packageId);
}
private applyPackageDoneCleanup(packageId: string): void {
if (this.settings.completedCleanupPolicy !== "package_done") {
return;
}
const pkg = this.session.packages[packageId];
if (!pkg || pkg.status !== "completed") {
return;
}
const allCompleted = pkg.itemIds.every((itemId) => {
const item = this.session.items[itemId];
return !item || item.status === "completed";
});
if (!allCompleted) {
return;
}
for (const itemId of pkg.itemIds) {
delete this.session.items[itemId];
}
delete this.session.packages[packageId];
this.session.packageOrder = this.session.packageOrder.filter((id) => id !== packageId);
this.runPackageIds.delete(packageId);
this.runCompletedPackages.delete(packageId);
}
private applyCompletedCleanupPolicy(packageId: string, itemId: string): void {
+68 -17
View File
@@ -163,6 +163,21 @@ function archivePasswords(listInput: string): string[] {
return Array.from(new Set(["", ...custom, ...fromEnv, ...DEFAULT_ARCHIVE_PASSWORDS]));
}
function prioritizePassword(passwords: string[], successful: string): string[] {
const target = String(successful || "");
if (!target || passwords.length <= 1) {
return passwords;
}
const index = passwords.findIndex((candidate) => candidate === target);
if (index <= 0) {
return passwords;
}
const next = [...passwords];
const [value] = next.splice(index, 1);
next.unshift(value);
return next;
}
function winRarCandidates(): string[] {
const programFiles = process.env.ProgramFiles || "C:\\Program Files";
const programFilesX86 = process.env["ProgramFiles(x86)"] || "C:\\Program Files (x86)";
@@ -334,7 +349,7 @@ async function runExternalExtract(
passwordCandidates: string[],
onArchiveProgress?: (percent: number) => void,
signal?: AbortSignal
): Promise<void> {
): Promise<string> {
const command = await resolveExtractorCommand();
const passwords = passwordCandidates;
let lastError = "";
@@ -363,7 +378,7 @@ async function runExternalExtract(
}, signal);
if (result.ok) {
onArchiveProgress?.(100);
return;
return password;
}
if (result.aborted) {
@@ -417,18 +432,20 @@ function escapeRegex(value: string): string {
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
}
export function collectArchiveCleanupTargets(sourceArchivePath: string): string[] {
export function collectArchiveCleanupTargets(sourceArchivePath: string, directoryFiles?: string[]): string[] {
const targets = new Set<string>([sourceArchivePath]);
const dir = path.dirname(sourceArchivePath);
const fileName = path.basename(sourceArchivePath);
let filesInDir: string[] = [];
try {
filesInDir = fs.readdirSync(dir, { withFileTypes: true })
.filter((entry) => entry.isFile())
.map((entry) => entry.name);
} catch {
return Array.from(targets);
let filesInDir: string[] = directoryFiles ?? [];
if (!directoryFiles) {
try {
filesInDir = fs.readdirSync(dir, { withFileTypes: true })
.filter((entry) => entry.isFile())
.map((entry) => entry.name);
} catch {
return Array.from(targets);
}
}
const addMatching = (pattern: RegExp): void => {
@@ -476,8 +493,22 @@ function cleanupArchives(sourceFiles: string[], cleanupMode: CleanupMode): numbe
}
const targets = new Set<string>();
const dirFilesCache = new Map<string, string[]>();
for (const sourceFile of sourceFiles) {
for (const target of collectArchiveCleanupTargets(sourceFile)) {
const dir = path.dirname(sourceFile);
let filesInDir = dirFilesCache.get(dir);
if (!filesInDir) {
try {
filesInDir = fs.readdirSync(dir, { withFileTypes: true })
.filter((entry) => entry.isFile())
.map((entry) => entry.name);
} catch {
filesInDir = [];
}
dirFilesCache.set(dir, filesInDir);
}
for (const target of collectArchiveCleanupTargets(sourceFile, filesInDir)) {
targets.add(target);
}
}
@@ -501,8 +532,14 @@ function hasAnyFilesRecursive(rootDir: string): boolean {
if (!fs.existsSync(rootDir)) {
return false;
}
const deadline = Date.now() + 70;
let inspectedDirs = 0;
const stack = [rootDir];
while (stack.length > 0) {
inspectedDirs += 1;
if (inspectedDirs > 8000 || Date.now() > deadline) {
return true;
}
const current = stack.pop() as string;
let entries: fs.Dirent[] = [];
try {
@@ -523,6 +560,17 @@ function hasAnyFilesRecursive(rootDir: string): boolean {
return false;
}
function hasAnyEntries(rootDir: string): boolean {
if (!rootDir || !fs.existsSync(rootDir)) {
return false;
}
try {
return fs.readdirSync(rootDir).length > 0;
} catch {
return false;
}
}
function removeEmptyDirectoryTree(rootDir: string): number {
if (!fs.existsSync(rootDir)) {
return 0;
@@ -572,7 +620,7 @@ export async function extractPackageArchives(options: ExtractOptions): Promise<{
logger.info(`Entpacken gestartet: packageDir=${options.packageDir}, targetDir=${options.targetDir}, archives=${candidates.length}, cleanupMode=${options.cleanupMode}, conflictMode=${options.conflictMode}`);
if (candidates.length === 0) {
const existingResume = readExtractResumeState(options.packageDir);
if (existingResume.size > 0 && hasAnyFilesRecursive(options.targetDir)) {
if (existingResume.size > 0 && hasAnyEntries(options.targetDir)) {
clearExtractResumeState(options.packageDir);
logger.info(`Entpacken übersprungen (Archive bereinigt, Ziel hat Dateien): ${options.packageDir}`);
options.onProgress?.({
@@ -590,7 +638,7 @@ export async function extractPackageArchives(options: ExtractOptions): Promise<{
}
const conflictMode = effectiveConflictMode(options.conflictMode);
const passwordCandidates = archivePasswords(options.passwordList || "");
let passwordCandidates = archivePasswords(options.passwordList || "");
const resumeCompleted = readExtractResumeState(options.packageDir);
const resumeCompletedAtStart = resumeCompleted.size;
const candidateNames = new Set(candidates.map((archivePath) => path.basename(archivePath)));
@@ -664,10 +712,11 @@ export async function extractPackageArchives(options: ExtractOptions): Promise<{
const preferExternal = shouldPreferExternalZip(archivePath);
if (preferExternal) {
try {
await runExternalExtract(archivePath, options.targetDir, options.conflictMode, passwordCandidates, (value) => {
const usedPassword = await runExternalExtract(archivePath, options.targetDir, options.conflictMode, passwordCandidates, (value) => {
archivePercent = Math.max(archivePercent, value);
emitProgress(extracted + failed, archiveName, "extracting", archivePercent, Date.now() - archiveStartedAt);
}, options.signal);
passwordCandidates = prioritizePassword(passwordCandidates, usedPassword);
} catch (error) {
if (isNoExtractorError(String(error))) {
extractZipArchive(archivePath, options.targetDir, options.conflictMode);
@@ -680,17 +729,19 @@ export async function extractPackageArchives(options: ExtractOptions): Promise<{
extractZipArchive(archivePath, options.targetDir, options.conflictMode);
archivePercent = 100;
} catch {
await runExternalExtract(archivePath, options.targetDir, options.conflictMode, passwordCandidates, (value) => {
const usedPassword = await runExternalExtract(archivePath, options.targetDir, options.conflictMode, passwordCandidates, (value) => {
archivePercent = Math.max(archivePercent, value);
emitProgress(extracted + failed, archiveName, "extracting", archivePercent, Date.now() - archiveStartedAt);
}, options.signal);
passwordCandidates = prioritizePassword(passwordCandidates, usedPassword);
}
}
} else {
await runExternalExtract(archivePath, options.targetDir, options.conflictMode, passwordCandidates, (value) => {
const usedPassword = await runExternalExtract(archivePath, options.targetDir, options.conflictMode, passwordCandidates, (value) => {
archivePercent = Math.max(archivePercent, value);
emitProgress(extracted + failed, archiveName, "extracting", archivePercent, Date.now() - archiveStartedAt);
}, options.signal);
passwordCandidates = prioritizePassword(passwordCandidates, usedPassword);
}
extracted += 1;
extractedArchives.add(archivePath);
@@ -722,7 +773,7 @@ export async function extractPackageArchives(options: ExtractOptions): Promise<{
}
if (extracted > 0) {
const hasOutputAfter = hasAnyFilesRecursive(options.targetDir);
const hasOutputAfter = hasAnyEntries(options.targetDir);
const hadResumeProgress = resumeCompletedAtStart > 0;
if (!hasOutputAfter && conflictMode !== "skip" && !hadResumeProgress) {
lastError = "Keine entpackten Dateien erkannt";