Add persistent JVM daemon for extraction, fix caching with Set+Array
- JVM extractor now supports --daemon mode: starts once, processes multiple archives via stdin JSON protocol, eliminating ~5s JVM boot per archive - TypeScript side: daemon manager starts JVM once, sends requests via stdin, falls back to spawning new process if daemon is busy - Fix extraction progress caching: replaced Object.create(null) + in operator with Set<string> + linear Array scan — both Map.has() and the in operator mysteriously failed to find keys that were just set - Daemon auto-shutdown on app quit via shutdownDaemon() in before-quit Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.6
parent
c36549ca69
commit
5b221d5bd5
+100
-22
@@ -6369,10 +6369,46 @@ export class DownloadManager extends EventEmitter {
|
||||
// Track multiple active archives for parallel hybrid extraction.
|
||||
// Using plain object instead of Map — Map.has() was mysteriously
|
||||
// returning false despite Map.set() being called with the same key.
|
||||
const resolvedItemsCache: Record<string, DownloadItem[]> = Object.create(null);
|
||||
const archiveStartTimesCache: Record<string, number> = Object.create(null);
|
||||
const hybridInitializedArchives = new Set<string>();
|
||||
const hybridResolvedItems: Array<{ key: string; items: DownloadItem[] }> = [];
|
||||
const hybridStartTimes: Array<{ key: string; time: number }> = [];
|
||||
let hybridLastEmitAt = 0;
|
||||
|
||||
const findHybridResolved = (key: string): DownloadItem[] | undefined => {
|
||||
for (let i = 0; i < hybridResolvedItems.length; i++) {
|
||||
if (hybridResolvedItems[i].key === key) return hybridResolvedItems[i].items;
|
||||
}
|
||||
return undefined;
|
||||
};
|
||||
const setHybridResolved = (key: string, items: DownloadItem[]): void => {
|
||||
for (let i = 0; i < hybridResolvedItems.length; i++) {
|
||||
if (hybridResolvedItems[i].key === key) { hybridResolvedItems[i].items = items; return; }
|
||||
}
|
||||
hybridResolvedItems.push({ key, items });
|
||||
};
|
||||
const removeHybridResolved = (key: string): void => {
|
||||
for (let i = hybridResolvedItems.length - 1; i >= 0; i--) {
|
||||
if (hybridResolvedItems[i].key === key) { hybridResolvedItems.splice(i, 1); return; }
|
||||
}
|
||||
};
|
||||
const findHybridStartTime = (key: string): number | undefined => {
|
||||
for (let i = 0; i < hybridStartTimes.length; i++) {
|
||||
if (hybridStartTimes[i].key === key) return hybridStartTimes[i].time;
|
||||
}
|
||||
return undefined;
|
||||
};
|
||||
const setHybridStartTime = (key: string, time: number): void => {
|
||||
for (let i = 0; i < hybridStartTimes.length; i++) {
|
||||
if (hybridStartTimes[i].key === key) { hybridStartTimes[i].time = time; return; }
|
||||
}
|
||||
hybridStartTimes.push({ key, time });
|
||||
};
|
||||
const removeHybridStartTime = (key: string): void => {
|
||||
for (let i = hybridStartTimes.length - 1; i >= 0; i--) {
|
||||
if (hybridStartTimes[i].key === key) { hybridStartTimes.splice(i, 1); return; }
|
||||
}
|
||||
};
|
||||
|
||||
// Mark items based on whether their archive is actually ready for extraction.
|
||||
// Only items whose archive is in readyArchives get "Ausstehend"; others keep
|
||||
// "Warten auf Parts" to avoid flicker between hybrid runs.
|
||||
@@ -6420,17 +6456,19 @@ export class DownloadManager extends EventEmitter {
|
||||
// Do NOT mark remaining archives as "Done" here — some may have
|
||||
// failed. The post-extraction code (result.failed check) will
|
||||
// assign the correct label. Only clear the tracking caches.
|
||||
for (const key of Object.keys(resolvedItemsCache)) delete resolvedItemsCache[key];
|
||||
for (const key of Object.keys(archiveStartTimesCache)) delete archiveStartTimesCache[key];
|
||||
hybridInitializedArchives.clear();
|
||||
hybridResolvedItems.length = 0;
|
||||
hybridStartTimes.length = 0;
|
||||
return;
|
||||
}
|
||||
|
||||
if (progress.archiveName) {
|
||||
// Resolve items for this archive if not yet tracked
|
||||
if (!(progress.archiveName in resolvedItemsCache)) {
|
||||
if (!hybridInitializedArchives.has(progress.archiveName)) {
|
||||
hybridInitializedArchives.add(progress.archiveName);
|
||||
const resolved = resolveArchiveItems(progress.archiveName);
|
||||
resolvedItemsCache[progress.archiveName] = resolved;
|
||||
archiveStartTimesCache[progress.archiveName] = nowMs();
|
||||
setHybridResolved(progress.archiveName, resolved);
|
||||
setHybridStartTime(progress.archiveName, nowMs());
|
||||
if (resolved.length === 0) {
|
||||
logger.warn(`resolveArchiveItems (hybrid): KEINE Items gefunden für archiveName="${progress.archiveName}", items.length=${items.length}, itemNames=[${items.slice(0, 5).map((i) => path.basename(i.targetPath || i.fileName || "?")).join(", ")}]`);
|
||||
} else {
|
||||
@@ -6449,12 +6487,12 @@ export class DownloadManager extends EventEmitter {
|
||||
this.emitState(true);
|
||||
}
|
||||
}
|
||||
const archItems = resolvedItemsCache[progress.archiveName] || [];
|
||||
const archItems = findHybridResolved(progress.archiveName) || [];
|
||||
|
||||
// If archive is at 100%, mark its items as done and remove from active
|
||||
if (Number(progress.archivePercent ?? 0) >= 100) {
|
||||
const doneAt = nowMs();
|
||||
const startedAt = archiveStartTimesCache[progress.archiveName] || doneAt;
|
||||
const startedAt = findHybridStartTime(progress.archiveName) || doneAt;
|
||||
const doneLabel = formatExtractDone(doneAt - startedAt);
|
||||
for (const entry of archItems) {
|
||||
if (!isExtractedLabel(entry.fullStatus)) {
|
||||
@@ -6462,8 +6500,9 @@ export class DownloadManager extends EventEmitter {
|
||||
entry.updatedAt = doneAt;
|
||||
}
|
||||
}
|
||||
delete resolvedItemsCache[progress.archiveName];
|
||||
delete archiveStartTimesCache[progress.archiveName];
|
||||
hybridInitializedArchives.delete(progress.archiveName);
|
||||
removeHybridResolved(progress.archiveName);
|
||||
removeHybridStartTime(progress.archiveName);
|
||||
// Show transitional label while next archive initializes
|
||||
const done = progress.current + 1;
|
||||
if (done < progress.total) {
|
||||
@@ -6757,8 +6796,44 @@ export class DownloadManager extends EventEmitter {
|
||||
try {
|
||||
// Track multiple active archives for parallel extraction.
|
||||
// Using plain object — Map.has() had a mysterious caching failure.
|
||||
const fullResolvedCache: Record<string, DownloadItem[]> = Object.create(null);
|
||||
const fullStartTimesCache: Record<string, number> = Object.create(null);
|
||||
const fullInitializedArchives = new Set<string>();
|
||||
const fullResolvedItems: Array<{ key: string; items: DownloadItem[] }> = [];
|
||||
const fullStartTimes: Array<{ key: string; time: number }> = [];
|
||||
|
||||
const findFullResolved = (key: string): DownloadItem[] | undefined => {
|
||||
for (let i = 0; i < fullResolvedItems.length; i++) {
|
||||
if (fullResolvedItems[i].key === key) return fullResolvedItems[i].items;
|
||||
}
|
||||
return undefined;
|
||||
};
|
||||
const setFullResolved = (key: string, items: DownloadItem[]): void => {
|
||||
for (let i = 0; i < fullResolvedItems.length; i++) {
|
||||
if (fullResolvedItems[i].key === key) { fullResolvedItems[i].items = items; return; }
|
||||
}
|
||||
fullResolvedItems.push({ key, items });
|
||||
};
|
||||
const removeFullResolved = (key: string): void => {
|
||||
for (let i = fullResolvedItems.length - 1; i >= 0; i--) {
|
||||
if (fullResolvedItems[i].key === key) { fullResolvedItems.splice(i, 1); return; }
|
||||
}
|
||||
};
|
||||
const findFullStartTime = (key: string): number | undefined => {
|
||||
for (let i = 0; i < fullStartTimes.length; i++) {
|
||||
if (fullStartTimes[i].key === key) return fullStartTimes[i].time;
|
||||
}
|
||||
return undefined;
|
||||
};
|
||||
const setFullStartTime = (key: string, time: number): void => {
|
||||
for (let i = 0; i < fullStartTimes.length; i++) {
|
||||
if (fullStartTimes[i].key === key) { fullStartTimes[i].time = time; return; }
|
||||
}
|
||||
fullStartTimes.push({ key, time });
|
||||
};
|
||||
const removeFullStartTime = (key: string): void => {
|
||||
for (let i = fullStartTimes.length - 1; i >= 0; i--) {
|
||||
if (fullStartTimes[i].key === key) { fullStartTimes.splice(i, 1); return; }
|
||||
}
|
||||
};
|
||||
|
||||
const result = await extractPackageArchives({
|
||||
packageDir: pkg.outputDir,
|
||||
@@ -6785,18 +6860,20 @@ export class DownloadManager extends EventEmitter {
|
||||
// Do NOT mark remaining archives as "Done" here — some may have
|
||||
// failed. The post-extraction code (result.failed check) will
|
||||
// assign the correct label. Only clear the tracking caches.
|
||||
for (const key of Object.keys(fullResolvedCache)) delete fullResolvedCache[key];
|
||||
for (const key of Object.keys(fullStartTimesCache)) delete fullStartTimesCache[key];
|
||||
fullInitializedArchives.clear();
|
||||
fullResolvedItems.length = 0;
|
||||
fullStartTimes.length = 0;
|
||||
emitExtractStatus("Entpacken 100%", true);
|
||||
return;
|
||||
}
|
||||
|
||||
if (progress.archiveName) {
|
||||
// Resolve items for this archive if not yet tracked
|
||||
if (!(progress.archiveName in fullResolvedCache)) {
|
||||
if (!fullInitializedArchives.has(progress.archiveName)) {
|
||||
fullInitializedArchives.add(progress.archiveName);
|
||||
const resolved = resolveArchiveItems(progress.archiveName);
|
||||
fullResolvedCache[progress.archiveName] = resolved;
|
||||
fullStartTimesCache[progress.archiveName] = nowMs();
|
||||
setFullResolved(progress.archiveName, resolved);
|
||||
setFullStartTime(progress.archiveName, nowMs());
|
||||
if (resolved.length === 0) {
|
||||
logger.warn(`resolveArchiveItems (full): KEINE Items für archiveName="${progress.archiveName}", completedItems=${completedItems.length}, names=[${completedItems.slice(0, 5).map((i) => path.basename(i.targetPath || i.fileName || "?")).join(", ")}]`);
|
||||
} else {
|
||||
@@ -6813,12 +6890,12 @@ export class DownloadManager extends EventEmitter {
|
||||
emitExtractStatus(`Entpacken ${progress.percent}% · ${progress.archiveName}`, true);
|
||||
}
|
||||
}
|
||||
const archiveItems = fullResolvedCache[progress.archiveName] || [];
|
||||
const archiveItems = findFullResolved(progress.archiveName) || [];
|
||||
|
||||
// If archive is at 100%, mark its items as done and remove from active
|
||||
if (Number(progress.archivePercent ?? 0) >= 100) {
|
||||
const doneAt = nowMs();
|
||||
const startedAt = fullStartTimesCache[progress.archiveName] || doneAt;
|
||||
const startedAt = findFullStartTime(progress.archiveName) || doneAt;
|
||||
const doneLabel = formatExtractDone(doneAt - startedAt);
|
||||
for (const entry of archiveItems) {
|
||||
if (!isExtractedLabel(entry.fullStatus)) {
|
||||
@@ -6826,8 +6903,9 @@ export class DownloadManager extends EventEmitter {
|
||||
entry.updatedAt = doneAt;
|
||||
}
|
||||
}
|
||||
delete fullResolvedCache[progress.archiveName];
|
||||
delete fullStartTimesCache[progress.archiveName];
|
||||
fullInitializedArchives.delete(progress.archiveName);
|
||||
removeFullResolved(progress.archiveName);
|
||||
removeFullStartTime(progress.archiveName);
|
||||
// Show transitional label while next archive initializes
|
||||
const done = progress.current + 1;
|
||||
if (done < progress.total) {
|
||||
|
||||
+278
-1
@@ -1,7 +1,7 @@
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
import os from "node:os";
|
||||
import { spawn, spawnSync } from "node:child_process";
|
||||
import { spawn, spawnSync, type ChildProcess } from "node:child_process";
|
||||
import AdmZip from "adm-zip";
|
||||
import { CleanupMode, ConflictMode } from "../shared/types";
|
||||
import { logger } from "./logger";
|
||||
@@ -988,6 +988,274 @@ function parseJvmLine(
|
||||
}
|
||||
}
|
||||
|
||||
// ── Persistent JVM Daemon ──
|
||||
// Keeps a single JVM process alive across multiple extraction requests,
|
||||
// eliminating the ~5s JVM boot overhead per archive.
|
||||
|
||||
interface DaemonRequest {
|
||||
resolve: (result: JvmExtractResult) => void;
|
||||
onArchiveProgress?: (percent: number) => void;
|
||||
signal?: AbortSignal;
|
||||
timeoutMs?: number;
|
||||
parseState: { bestPercent: number; usedPassword: string; backend: string; reportedError: string };
|
||||
}
|
||||
|
||||
let daemonProcess: ChildProcess | null = null;
|
||||
let daemonReady = false;
|
||||
let daemonBusy = false;
|
||||
let daemonCurrentRequest: DaemonRequest | null = null;
|
||||
let daemonStdoutBuffer = "";
|
||||
let daemonStderrBuffer = "";
|
||||
let daemonOutput = "";
|
||||
let daemonTimeoutId: NodeJS.Timeout | null = null;
|
||||
let daemonAbortHandler: (() => void) | null = null;
|
||||
let daemonLayout: JvmExtractorLayout | null = null;
|
||||
|
||||
export function shutdownDaemon(): void {
|
||||
if (daemonProcess) {
|
||||
try { daemonProcess.stdin?.end(); } catch { /* ignore */ }
|
||||
try { killProcessTree(daemonProcess); } catch { /* ignore */ }
|
||||
daemonProcess = null;
|
||||
}
|
||||
daemonReady = false;
|
||||
daemonBusy = false;
|
||||
daemonCurrentRequest = null;
|
||||
daemonStdoutBuffer = "";
|
||||
daemonStderrBuffer = "";
|
||||
daemonOutput = "";
|
||||
if (daemonTimeoutId) { clearTimeout(daemonTimeoutId); daemonTimeoutId = null; }
|
||||
if (daemonAbortHandler) { daemonAbortHandler = null; }
|
||||
daemonLayout = null;
|
||||
}
|
||||
|
||||
function finishDaemonRequest(result: JvmExtractResult): void {
|
||||
const req = daemonCurrentRequest;
|
||||
if (!req) return;
|
||||
daemonCurrentRequest = null;
|
||||
daemonBusy = false;
|
||||
daemonStdoutBuffer = "";
|
||||
daemonStderrBuffer = "";
|
||||
daemonOutput = "";
|
||||
if (daemonTimeoutId) { clearTimeout(daemonTimeoutId); daemonTimeoutId = null; }
|
||||
if (req.signal && daemonAbortHandler) {
|
||||
req.signal.removeEventListener("abort", daemonAbortHandler);
|
||||
daemonAbortHandler = null;
|
||||
}
|
||||
req.resolve(result);
|
||||
}
|
||||
|
||||
function handleDaemonLine(line: string): void {
|
||||
const trimmed = String(line || "").trim();
|
||||
if (!trimmed) return;
|
||||
|
||||
// Check for daemon ready signal
|
||||
if (trimmed === "RD_DAEMON_READY") {
|
||||
daemonReady = true;
|
||||
logger.info("JVM Daemon bereit (persistent)");
|
||||
return;
|
||||
}
|
||||
|
||||
// Check for request completion
|
||||
if (trimmed.startsWith("RD_REQUEST_DONE ")) {
|
||||
const code = parseInt(trimmed.slice("RD_REQUEST_DONE ".length).trim(), 10);
|
||||
const req = daemonCurrentRequest;
|
||||
if (!req) return;
|
||||
|
||||
if (code === 0) {
|
||||
req.onArchiveProgress?.(100);
|
||||
finishDaemonRequest({
|
||||
ok: true, missingCommand: false, missingRuntime: false,
|
||||
aborted: false, timedOut: false, errorText: "",
|
||||
usedPassword: req.parseState.usedPassword, backend: req.parseState.backend
|
||||
});
|
||||
} else {
|
||||
const message = cleanErrorText(req.parseState.reportedError || daemonOutput) || `Exit Code ${code}`;
|
||||
finishDaemonRequest({
|
||||
ok: false, missingCommand: false, missingRuntime: isJvmRuntimeMissingError(message),
|
||||
aborted: false, timedOut: false, errorText: message,
|
||||
usedPassword: req.parseState.usedPassword, backend: req.parseState.backend
|
||||
});
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Regular progress/status lines — delegate to parseJvmLine
|
||||
if (daemonCurrentRequest) {
|
||||
parseJvmLine(trimmed, daemonCurrentRequest.onArchiveProgress, daemonCurrentRequest.parseState);
|
||||
}
|
||||
}
|
||||
|
||||
function startDaemon(layout: JvmExtractorLayout): boolean {
|
||||
if (daemonProcess && daemonReady) return true;
|
||||
shutdownDaemon();
|
||||
|
||||
const jvmTmpDir = path.join(os.tmpdir(), `rd-extract-daemon-${crypto.randomUUID()}`);
|
||||
fs.mkdirSync(jvmTmpDir, { recursive: true });
|
||||
|
||||
const args = [
|
||||
"-Dfile.encoding=UTF-8",
|
||||
`-Djava.io.tmpdir=${jvmTmpDir}`,
|
||||
"-Xms512m",
|
||||
"-Xmx8g",
|
||||
"-XX:+UseSerialGC",
|
||||
"-cp",
|
||||
layout.classPath,
|
||||
JVM_EXTRACTOR_MAIN_CLASS,
|
||||
"--daemon"
|
||||
];
|
||||
|
||||
try {
|
||||
const child = spawn(layout.javaCommand, args, {
|
||||
windowsHide: true,
|
||||
stdio: ["pipe", "pipe", "pipe"]
|
||||
});
|
||||
lowerExtractProcessPriority(child.pid, currentExtractCpuPriority);
|
||||
daemonProcess = child;
|
||||
daemonLayout = layout;
|
||||
|
||||
child.stdout!.on("data", (chunk) => {
|
||||
const raw = String(chunk || "");
|
||||
daemonOutput = appendLimited(daemonOutput, raw);
|
||||
daemonStdoutBuffer += raw;
|
||||
const lines = daemonStdoutBuffer.split(/\r?\n/);
|
||||
daemonStdoutBuffer = lines.pop() || "";
|
||||
for (const line of lines) {
|
||||
handleDaemonLine(line);
|
||||
}
|
||||
});
|
||||
|
||||
child.stderr!.on("data", (chunk) => {
|
||||
const raw = String(chunk || "");
|
||||
daemonOutput = appendLimited(daemonOutput, raw);
|
||||
daemonStderrBuffer += raw;
|
||||
const lines = daemonStderrBuffer.split(/\r?\n/);
|
||||
daemonStderrBuffer = lines.pop() || "";
|
||||
for (const line of lines) {
|
||||
if (daemonCurrentRequest) {
|
||||
parseJvmLine(line, daemonCurrentRequest.onArchiveProgress, daemonCurrentRequest.parseState);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
child.on("error", () => {
|
||||
if (daemonCurrentRequest) {
|
||||
finishDaemonRequest({
|
||||
ok: false, missingCommand: true, missingRuntime: true,
|
||||
aborted: false, timedOut: false, errorText: "Daemon process error",
|
||||
usedPassword: "", backend: ""
|
||||
});
|
||||
}
|
||||
shutdownDaemon();
|
||||
});
|
||||
|
||||
child.on("close", () => {
|
||||
if (daemonCurrentRequest) {
|
||||
const req = daemonCurrentRequest;
|
||||
finishDaemonRequest({
|
||||
ok: false, missingCommand: false, missingRuntime: false,
|
||||
aborted: false, timedOut: false,
|
||||
errorText: cleanErrorText(req.parseState.reportedError || daemonOutput) || "Daemon process exited unexpectedly",
|
||||
usedPassword: req.parseState.usedPassword, backend: req.parseState.backend
|
||||
});
|
||||
}
|
||||
// Clean up tmp dir
|
||||
fs.rm(jvmTmpDir, { recursive: true, force: true }, () => {});
|
||||
daemonProcess = null;
|
||||
daemonReady = false;
|
||||
daemonBusy = false;
|
||||
daemonLayout = null;
|
||||
});
|
||||
|
||||
logger.info(`JVM Daemon gestartet (PID ${child.pid})`);
|
||||
return true;
|
||||
} catch (error) {
|
||||
logger.warn(`JVM Daemon Start fehlgeschlagen: ${String(error)}`);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
function isDaemonAvailable(layout: JvmExtractorLayout): boolean {
|
||||
// Start daemon if not running yet
|
||||
if (!daemonProcess || !daemonReady) {
|
||||
startDaemon(layout);
|
||||
}
|
||||
return Boolean(daemonProcess && daemonReady && !daemonBusy);
|
||||
}
|
||||
|
||||
function sendDaemonRequest(
|
||||
archivePath: string,
|
||||
targetDir: string,
|
||||
conflictMode: ConflictMode,
|
||||
passwordCandidates: string[],
|
||||
onArchiveProgress?: (percent: number) => void,
|
||||
signal?: AbortSignal,
|
||||
timeoutMs?: number
|
||||
): Promise<JvmExtractResult> {
|
||||
return new Promise((resolve) => {
|
||||
const mode = effectiveConflictMode(conflictMode);
|
||||
const parseState = { bestPercent: 0, usedPassword: "", backend: "", reportedError: "" };
|
||||
|
||||
daemonBusy = true;
|
||||
daemonOutput = "";
|
||||
daemonCurrentRequest = { resolve, onArchiveProgress, signal, timeoutMs, parseState };
|
||||
|
||||
// Set up timeout
|
||||
if (timeoutMs && timeoutMs > 0) {
|
||||
daemonTimeoutId = setTimeout(() => {
|
||||
// Timeout — kill the daemon and restart fresh for next request
|
||||
const req = daemonCurrentRequest;
|
||||
if (req) {
|
||||
finishDaemonRequest({
|
||||
ok: false, missingCommand: false, missingRuntime: false,
|
||||
aborted: false, timedOut: true,
|
||||
errorText: `Entpacken Timeout nach ${Math.ceil(timeoutMs / 1000)}s`,
|
||||
usedPassword: parseState.usedPassword, backend: parseState.backend
|
||||
});
|
||||
}
|
||||
shutdownDaemon();
|
||||
}, timeoutMs);
|
||||
}
|
||||
|
||||
// Set up abort handler
|
||||
if (signal) {
|
||||
daemonAbortHandler = () => {
|
||||
const req = daemonCurrentRequest;
|
||||
if (req) {
|
||||
finishDaemonRequest({
|
||||
ok: false, missingCommand: false, missingRuntime: false,
|
||||
aborted: true, timedOut: false, errorText: "aborted:extract",
|
||||
usedPassword: parseState.usedPassword, backend: parseState.backend
|
||||
});
|
||||
}
|
||||
// Kill daemon on abort — cleaner than trying to interrupt mid-extraction
|
||||
shutdownDaemon();
|
||||
};
|
||||
signal.addEventListener("abort", daemonAbortHandler, { once: true });
|
||||
}
|
||||
|
||||
// Build and send JSON request
|
||||
const jsonRequest = JSON.stringify({
|
||||
archive: archivePath,
|
||||
target: targetDir,
|
||||
conflict: mode,
|
||||
backend: "auto",
|
||||
passwords: passwordCandidates
|
||||
});
|
||||
|
||||
try {
|
||||
daemonProcess!.stdin!.write(jsonRequest + "\n");
|
||||
} catch (error) {
|
||||
finishDaemonRequest({
|
||||
ok: false, missingCommand: false, missingRuntime: false,
|
||||
aborted: false, timedOut: false,
|
||||
errorText: `Daemon stdin write failed: ${String(error)}`,
|
||||
usedPassword: "", backend: ""
|
||||
});
|
||||
shutdownDaemon();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function runJvmExtractCommand(
|
||||
layout: JvmExtractorLayout,
|
||||
archivePath: string,
|
||||
@@ -1011,6 +1279,15 @@ function runJvmExtractCommand(
|
||||
});
|
||||
}
|
||||
|
||||
// Try persistent daemon first — saves ~5s JVM boot per archive
|
||||
if (isDaemonAvailable(layout)) {
|
||||
logger.info(`JVM Daemon: Sende Request für ${path.basename(archivePath)}`);
|
||||
return sendDaemonRequest(archivePath, targetDir, conflictMode, passwordCandidates, onArchiveProgress, signal, timeoutMs);
|
||||
}
|
||||
|
||||
// Fallback: spawn a new JVM process (daemon busy or not available)
|
||||
logger.info(`JVM Spawn: Neuer Prozess für ${path.basename(archivePath)}${daemonBusy ? " (Daemon busy)" : ""}`);
|
||||
|
||||
const mode = effectiveConflictMode(conflictMode);
|
||||
// Each JVM process needs its own temp dir so parallel SevenZipJBinding
|
||||
// instances don't fight over the same native DLL file lock.
|
||||
|
||||
+2
-1
@@ -7,7 +7,7 @@ import { IPC_CHANNELS } from "../shared/ipc";
|
||||
import { getLogFilePath, logger } from "./logger";
|
||||
import { APP_NAME } from "./constants";
|
||||
import { extractHttpLinksFromText } from "./utils";
|
||||
import { cleanupStaleSubstDrives } from "./extractor";
|
||||
import { cleanupStaleSubstDrives, shutdownDaemon } from "./extractor";
|
||||
|
||||
/* ── IPC validation helpers ────────────────────────────────────── */
|
||||
function validateString(value: unknown, name: string): string {
|
||||
@@ -515,6 +515,7 @@ app.on("before-quit", () => {
|
||||
if (updateQuitTimer) { clearTimeout(updateQuitTimer); updateQuitTimer = null; }
|
||||
stopClipboardWatcher();
|
||||
destroyTray();
|
||||
shutdownDaemon();
|
||||
try {
|
||||
controller.shutdown();
|
||||
} catch (error) {
|
||||
|
||||
Reference in New Issue
Block a user