fix(extraction): harden daemon termination and drain deadlines
Prevent aborted daemon waiters and pre-aborted requests from spawning one-shot JVM children. Ignore buffered request completion after abort or timeout so permits and disk leases remain owned until real daemon close. Keep incomplete shutdown operations and leases alive past the shared deadline while returning explicit drain status, and make multipart reservations fail closed whenever any deduplicated member size is unknown. Add real daemon boot, busy, abort, timeout, DONE race, deadline, and reservation regression coverage.
This commit is contained in:
+91
-47
@@ -1815,14 +1815,14 @@ function handleDaemonLine(line: string): void {
|
||||
return;
|
||||
}
|
||||
|
||||
if (trimmed.startsWith("RD_REQUEST_DONE ")) {
|
||||
const code = parseInt(trimmed.slice("RD_REQUEST_DONE ".length).trim(), 10);
|
||||
const req = daemonCurrentRequest;
|
||||
if (!req) return;
|
||||
const finalize = (): void => {
|
||||
if (daemonCurrentRequest !== req) {
|
||||
return;
|
||||
}
|
||||
if (trimmed.startsWith("RD_REQUEST_DONE ")) {
|
||||
const code = parseInt(trimmed.slice("RD_REQUEST_DONE ".length).trim(), 10);
|
||||
const req = daemonCurrentRequest;
|
||||
if (!req || req.terminationStarted) return;
|
||||
const finalize = (): void => {
|
||||
if (daemonCurrentRequest !== req || req.terminationStarted) {
|
||||
return;
|
||||
}
|
||||
flushDaemonParseBuffers(req);
|
||||
if (req.parseState.outputError) {
|
||||
failDaemonOutputCallback(req);
|
||||
@@ -1978,26 +1978,59 @@ function startDaemon(layout: JvmExtractorLayout): boolean {
|
||||
}
|
||||
}
|
||||
|
||||
function isDaemonAvailable(layout: JvmExtractorLayout): boolean {
|
||||
function isDaemonAvailable(layout: JvmExtractorLayout): boolean {
|
||||
if (!daemonProcess || !daemonReady) {
|
||||
startDaemon(layout);
|
||||
}
|
||||
return Boolean(daemonProcess && daemonReady && !daemonBusy);
|
||||
}
|
||||
|
||||
function waitForDaemonReady(maxWaitMs: number, signal?: AbortSignal): Promise<boolean> {
|
||||
return new Promise((resolve) => {
|
||||
const start = Date.now();
|
||||
const check = () => {
|
||||
if (signal?.aborted) { resolve(false); return; }
|
||||
if (daemonProcess && daemonReady && !daemonBusy) { resolve(true); return; }
|
||||
if (!daemonProcess) { resolve(false); return; }
|
||||
if (Date.now() - start >= maxWaitMs) { resolve(false); return; }
|
||||
setTimeout(check, 50);
|
||||
};
|
||||
check();
|
||||
});
|
||||
}
|
||||
return Boolean(daemonProcess && daemonReady && !daemonBusy);
|
||||
}
|
||||
|
||||
function abortedJvmExtractResult(): JvmExtractResult {
|
||||
return {
|
||||
ok: false,
|
||||
missingCommand: false,
|
||||
missingRuntime: false,
|
||||
aborted: true,
|
||||
timedOut: false,
|
||||
errorText: "aborted:extract",
|
||||
usedPassword: "",
|
||||
backend: ""
|
||||
};
|
||||
}
|
||||
|
||||
function waitForDaemonReady(maxWaitMs: number, signal?: AbortSignal): Promise<boolean> {
|
||||
return new Promise((resolve) => {
|
||||
const start = Date.now();
|
||||
let settled = false;
|
||||
let timer: NodeJS.Timeout | null = null;
|
||||
const finish = (ready: boolean): void => {
|
||||
if (settled) {
|
||||
return;
|
||||
}
|
||||
settled = true;
|
||||
if (timer) {
|
||||
clearTimeout(timer);
|
||||
timer = null;
|
||||
}
|
||||
signal?.removeEventListener("abort", onAbort);
|
||||
resolve(ready);
|
||||
};
|
||||
const onAbort = (): void => finish(false);
|
||||
const check = () => {
|
||||
if (signal?.aborted) { finish(false); return; }
|
||||
if (daemonProcess && daemonReady && !daemonBusy) { finish(true); return; }
|
||||
if (!daemonProcess) { finish(false); return; }
|
||||
if (Date.now() - start >= maxWaitMs) { finish(false); return; }
|
||||
timer = setTimeout(check, 50);
|
||||
};
|
||||
if (signal?.aborted) {
|
||||
finish(false);
|
||||
return;
|
||||
}
|
||||
signal?.addEventListener("abort", onAbort, { once: true });
|
||||
check();
|
||||
});
|
||||
}
|
||||
|
||||
function sendDaemonRequest(
|
||||
archivePath: string,
|
||||
@@ -2008,8 +2041,11 @@ function sendDaemonRequest(
|
||||
signal?: AbortSignal,
|
||||
timeoutMs?: number,
|
||||
onOutput?: (event: ExtractOutputEvent) => void
|
||||
): Promise<JvmExtractResult> {
|
||||
return new Promise((resolve) => {
|
||||
): Promise<JvmExtractResult> {
|
||||
if (signal?.aborted) {
|
||||
return Promise.resolve(abortedJvmExtractResult());
|
||||
}
|
||||
return new Promise((resolve) => {
|
||||
const mode = effectiveConflictMode(conflictMode);
|
||||
const parseState = { bestPercent: 0, usedPassword: "", backend: "", reportedError: "" };
|
||||
const archiveName = path.basename(archivePath);
|
||||
@@ -2094,13 +2130,9 @@ async function runJvmExtractCommand(
|
||||
signal?: AbortSignal,
|
||||
timeoutMs?: number,
|
||||
onOutput?: (event: ExtractOutputEvent) => void
|
||||
): Promise<JvmExtractResult> {
|
||||
if (signal?.aborted) {
|
||||
return Promise.resolve({
|
||||
ok: false, missingCommand: false, missingRuntime: false,
|
||||
aborted: true, timedOut: false, errorText: "aborted:extract",
|
||||
usedPassword: "", backend: ""
|
||||
});
|
||||
): Promise<JvmExtractResult> {
|
||||
if (signal?.aborted) {
|
||||
return Promise.resolve(abortedJvmExtractResult());
|
||||
}
|
||||
|
||||
if (isDaemonAvailable(layout)) {
|
||||
@@ -2112,18 +2144,25 @@ async function runJvmExtractCommand(
|
||||
if (daemonProcess) {
|
||||
const reason = !daemonReady ? "booting" : "busy";
|
||||
const waitStartedAt = Date.now();
|
||||
logger.info(`JVM Daemon: Warte auf ${reason} Daemon für ${path.basename(archivePath)}...`);
|
||||
const ready = await waitForDaemonReady(15_000, signal);
|
||||
const waitedMs = Date.now() - waitStartedAt;
|
||||
if (ready) {
|
||||
logger.info(`JVM Daemon: Warte auf ${reason} Daemon für ${path.basename(archivePath)}...`);
|
||||
const ready = await waitForDaemonReady(15_000, signal);
|
||||
const waitedMs = Date.now() - waitStartedAt;
|
||||
if (signal?.aborted) {
|
||||
return abortedJvmExtractResult();
|
||||
}
|
||||
if (ready) {
|
||||
lowerExtractProcessPriority(daemonProcess?.pid, currentExtractCpuPriority);
|
||||
logger.info(`JVM Daemon: Bereit nach ${waitedMs}ms — sende Request für ${path.basename(archivePath)}`);
|
||||
return sendDaemonRequest(archivePath, targetDir, conflictMode, passwordCandidates, onArchiveProgress, signal, timeoutMs, onOutput);
|
||||
}
|
||||
logger.warn(`JVM Daemon: Timeout nach ${waitedMs}ms beim Warten — Fallback auf neuen Prozess für ${path.basename(archivePath)}`);
|
||||
}
|
||||
|
||||
logger.info(`JVM Spawn: Neuer Prozess für ${path.basename(archivePath)}`);
|
||||
logger.warn(`JVM Daemon: Timeout nach ${waitedMs}ms beim Warten — Fallback auf neuen Prozess für ${path.basename(archivePath)}`);
|
||||
}
|
||||
|
||||
if (signal?.aborted) {
|
||||
return abortedJvmExtractResult();
|
||||
}
|
||||
|
||||
logger.info(`JVM Spawn: Neuer Prozess für ${path.basename(archivePath)}`);
|
||||
|
||||
const mode = effectiveConflictMode(conflictMode);
|
||||
const jvmTmpDir = path.join(os.tmpdir(), `rd-extract-${crypto.randomUUID()}`);
|
||||
@@ -2147,11 +2186,16 @@ async function runJvmExtractCommand(
|
||||
"--backend",
|
||||
"auto"
|
||||
];
|
||||
for (const password of passwordCandidates) {
|
||||
args.push("--password", password);
|
||||
}
|
||||
|
||||
return new Promise((resolve) => {
|
||||
for (const password of passwordCandidates) {
|
||||
args.push("--password", password);
|
||||
}
|
||||
|
||||
if (signal?.aborted) {
|
||||
fs.rm(jvmTmpDir, { recursive: true, force: true }, () => {});
|
||||
return abortedJvmExtractResult();
|
||||
}
|
||||
|
||||
return new Promise((resolve) => {
|
||||
let settled = false;
|
||||
let output = "";
|
||||
let timeoutId: NodeJS.Timeout | null = null;
|
||||
|
||||
Reference in New Issue
Block a user