From 5b221d5bd5921b425f2a2170034af8b327e95327 Mon Sep 17 00:00:00 2001 From: Sucukdeluxe Date: Thu, 5 Mar 2026 05:59:13 +0100 Subject: [PATCH] Add persistent JVM daemon for extraction, fix caching with Set+Array MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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 + 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 --- .../extractor/JBindExtractorMain$1.class | Bin 1710 -> 1710 bytes .../JBindExtractorMain$Backend.class | Bin 2260 -> 2260 bytes .../JBindExtractorMain$ConflictMode.class | Bin 2014 -> 2014 bytes ...JBindExtractorMain$ExtractionRequest.class | Bin 2539 -> 2539 bytes .../JBindExtractorMain$ProgressTracker.class | Bin 1387 -> 1387 bytes ...ExtractorMain$SevenZipArchiveContext.class | Bin 1663 -> 1663 bytes ...ExtractorMain$SevenZipVolumeCallback.class | Bin 3729 -> 3666 bytes ...ExtractorMain$WrongPasswordException.class | Bin 458 -> 458 bytes .../extractor/JBindExtractorMain.class | Bin 19259 -> 23733 bytes .../extractor/JBindExtractorMain.java | 125 ++++++++ src/main/download-manager.ts | 122 ++++++-- src/main/extractor.ts | 279 +++++++++++++++++- src/main/main.ts | 3 +- 13 files changed, 505 insertions(+), 24 deletions(-) diff --git a/resources/extractor-jvm/classes/com/sucukdeluxe/extractor/JBindExtractorMain$1.class b/resources/extractor-jvm/classes/com/sucukdeluxe/extractor/JBindExtractorMain$1.class index fcefae82f2b36f12c39bfd30335e048c1773f902..aa67331473b01ad9d45a22e0f926de02464f6309 100644 GIT binary patch delta 55 zcmV-70LcHY4XzEat_1;{ldlDhC7%EZ0iXa20iysD0j2;J0i*yI0i^&o0jB^%0jK~? N0jU600jiT>22Hp%4x#`6 delta 55 zcmZ3-yN-9mI#x!m$?I8rRe2dW8TlBv83h@H8ATYx7=;+b8HE|l7)2SJ8O0cU8O0ew L86_qsviSl48aoJF diff --git a/resources/extractor-jvm/classes/com/sucukdeluxe/extractor/JBindExtractorMain$Backend.class b/resources/extractor-jvm/classes/com/sucukdeluxe/extractor/JBindExtractorMain$Backend.class index 41ca622c4afda3cf1d1170888ace9bf531733082..54d4c8fc551815e9499e5e59e247bb352d09d971 100644 GIT binary patch delta 104 zcmca2ctvo-1~w*U*U6jM^gz@jHUlnZ9|ksNUj}YwzsWM}Q{ z7;Ko@8622889bS~7<`#}CZA)s2WjNzum({+91(m>r#(-{<*GZ~DTvlz^oa~K?$ la~WKi^Cxq%2ZB__um|xmCou3bCo)JeCo!l4h4!#30RTrE5y$`l delta 89 zcmcb|e~*6yBO4RbgvrcodLYV}O&dfduyxBZO=A#Yn$950G=o8rX%>Sq(`*KFrnw9b mO!F9Am=;XtWDf+XjA0MrW17go%QT5WifJ-~I#6g2yAlAvTNKCu diff --git a/resources/extractor-jvm/classes/com/sucukdeluxe/extractor/JBindExtractorMain$ExtractionRequest.class b/resources/extractor-jvm/classes/com/sucukdeluxe/extractor/JBindExtractorMain$ExtractionRequest.class index f2ccd338a03a7aa6cc9a6da5a03dc3836582427b..f5701524ff17c2999d5a18261349de4c3b561cf9 100644 GIT binary patch delta 736 zcmZuvO>YuW6g_Wdc*8IwK~S-6Xd|@NPHmw;wUtVg#-d66+L-79Gl6LwC7>OoU1)S? z;>N(+yLVSVu&pMpjVAt){sa^C&e)0zW-({ZedpeD-^uL7_G0?CU!T4J$m5FE(XUTx z4=vZ-Xs*{>hREuDt8Jwpx14%<)p1(|4HAL|G=zl8%G@w;2;t77n>=FRFruBWC$dKj z97U8tx7MrooVLB<)a?+p#SI+C5CdOw8cwUoAfKAL6~YMOVv{A6FvFsFCC`ZOY)lC; zGCEGGQ|5p zndQaLz(?_1R@uCGEv>N|U4BvgkcYzAOs3zL@5(QD&Z|82&I?LWZ1E=vwPaNv*o_*4 zvD|E|)tzeVYO`jmxXdu?uh>?@&*;LGpyWm1BZcB4(bN{WIir>Rnc zOkcpLxs74-9TGk01j!y$^D`!IZ)2(l_LdY3GxUx^0c017+=ec1AS!QGBA|h@`vVh% z5(xt|@L0dk+kpBaL8Ot{myVIQCsobqJ3Yj<{+aZ}bI1r?n+W=yS>(jR0m)-t*xE$J zKRJW5|Ji%@378+i22EVR;sEJD^K8Oh+6nX|bggTol1S zFns<5LEVTIL-2uHp$l>6y6gT2>78k71T%{{-?{gkdoSl8agZ?f{(SikpoW`zU@WDS zS-r_e=hDX%6 z#?#^$xrp{$OX3fu9G2ckiigrV-&LMx&H1(V<5l0?ytTICF~sIt?o+S562lFKsqp%( zn6*PeP<}#~O@x$IgcJm!9mW6#C#sFZAf*h2Dic(hgGetR=X8;FJ|f#ej3D2E?R>@9 z{Vs|f4DOPGVUqqfBwF$zxkJMMt2n+WAm-5eACY7TouU}20K)68Iq%-I{1R3jmSO5S3 diff --git a/resources/extractor-jvm/classes/com/sucukdeluxe/extractor/JBindExtractorMain$ProgressTracker.class b/resources/extractor-jvm/classes/com/sucukdeluxe/extractor/JBindExtractorMain$ProgressTracker.class index 8c1da8f36d621da21b9cd4c61b76568c2756569e..1c0c20a49c0063d2cf122f3791f6ff11065b3781 100644 GIT binary patch delta 118 zcmV~$K?*@p7>41m-g6I*QZBg}Fk@f?MoeTE7GNd973B}1PzW=NumfcicN3l`3t4!7 zh?&YUQGuCa=1NHIN5`{N#LB!IH(ASL%CS+Its3mqVy`|24LI7u^#Uj5Ijh9QzHU_G Usv}^ z2LrtT5Cgve69c{g6a&Ek8w0}tBm>0&Cj-O)C1|pI49MAv& delta 111 zcmV-#0FeLx4F3$U&I188v(E!|0Tu$|00aW$00{!+01g7?022b|02u=4lUoIaIs)tf z2LkQ@5CZT369Vr56aw-98v^tIBm(vTCj#{VC<6EZF9P}iHUj(rIRg6tIs*OxK?45( RNCN?r8U{fE0^O5c1|n9YAMgMG diff --git a/resources/extractor-jvm/classes/com/sucukdeluxe/extractor/JBindExtractorMain$SevenZipVolumeCallback.class b/resources/extractor-jvm/classes/com/sucukdeluxe/extractor/JBindExtractorMain$SevenZipVolumeCallback.class index 10566c8785787dd66b950b221e59af4ccaf3d083..9013963ec033a8fb161bda45ea87a22d569e72d5 100644 GIT binary patch delta 970 zcmaiyOKep|7{`BeXYQGE?>#NOmX@BI7VNDl_TH99OWJ@ELO0#CEZmuhNnv%s2C&Z~pU} zZ{}dbmHPDMyB9A3tGGCszLw1NrpfY3z^lCGtoMkwEnt}KZl$-}+ZnKn-R@bhIrBPi zSoQ|&W4}A>txb*arsb`GgYJgcIIm1?R! zE9?oz@g*kdLowl&rn?(IlyMFZ^miEjEerJbGs}__&8c2xn1It zkXTtfPvIs`MqJrYbwam~MUg(#0S~WiR&HoDBO_dZOL!}9*GF8-@EKTMiR+wtK z%tBV1TDr|5dQ2VtCSsE^vOXEjbk>9&;2x? z{GN(0Bu_Ifk~;r*7Gvd1OUauSnoW@wQ=nj4X)|qfn(k#Wa3khl7Fb3iRvojBSb9*F Zu{&@_yb_cJE5S5hNK6Z6hQ|dicMsn9zZ(Dm delta 1013 zcmaiyTWnNS6o$XuHFNg#%yD37jGbC6o$6G2p;D^FqzxCb2$nV>eIZhCFbzT)noxys zkpbl94IMV3BY3}z%EeZ&wSdGY9(*+6MIU_P(L`RG#=6c7G2w~ioW1s5`(OWB|Jv*K z+WdU~w|}qR06O?(-2XG0-WRZ+PJ_Q~bj6m@k>OWg8S+We8!owkgB)`IL?88D4>-b6 zSL3yL#{*9AhU@n-@wa%}@=m}>-gO7P?#eu;EH2=*yXiH=@|+3yfDhdrFWvP~z*#&) z^rhjE(ILynhN|t^UD>wb?2fH%1269!*`3`oJQQaop9Xw(7dn*PY5BtS+r{Q9cgS`| zzA-Fvf5lSnmVL;4d--p>&zPUxw#x1HbH3zjitdlfhN|yWUM^vfV4co6O!Pf8}-}mC4MMHT=45MVMGGah>reNGbtr6t1-gc8^VMoF(WWajj@`qULYAtM zc9xW}rbnx(RLEI(AgF$FUCRV}3j}I93PfARlp%GK>@|`b!fo>;ul2Q-FI!8O6C7|i zg2lP!2@V(VQn9fk))Z%Nl^JYHk%$RPs3?hA%RCj;MMAonE6Yg9avEg?3*{Mom{oMk zv#gRHdgM9!Weo$emL2juyQG&r(#KKh=Y;xB%0TIP*5D`14AnMAa)w$p?THqk1Zc}iX^ qsi`M2!yHRK6j3fPZ&{78oSTMerK-?E!e(COnp$7e#OdNXrshAET)*4^ diff --git a/resources/extractor-jvm/classes/com/sucukdeluxe/extractor/JBindExtractorMain$WrongPasswordException.class b/resources/extractor-jvm/classes/com/sucukdeluxe/extractor/JBindExtractorMain$WrongPasswordException.class index 6826c5e095029799a4bdf95f507bb9ce8b0b1d67..e4c49b03c84694720a505332bb63ae183d311a7a 100644 GIT binary patch delta 18 ZcmX@be2RI)Dn>TudInbJhRGWkr2sjY1!MpK delta 18 acmX@be2RI)Dn>S@4GgSI8z*mIlmY-eHU-20 diff --git a/resources/extractor-jvm/classes/com/sucukdeluxe/extractor/JBindExtractorMain.class b/resources/extractor-jvm/classes/com/sucukdeluxe/extractor/JBindExtractorMain.class index ef9eef2200ba53fea724241bc25880333765bb05..d4c29cef8a5228dc7477023297da0f6b0d3c805d 100644 GIT binary patch literal 23733 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 literal 19259 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 0) { + request.conflictMode = ConflictMode.fromValue(conflict); + } + String backend = extractJsonString(jsonLine, "backend"); + if (backend.length() > 0) { + request.backend = Backend.fromValue(backend); + } + // Parse passwords array + int pwStart = jsonLine.indexOf("\"passwords\""); + if (pwStart >= 0) { + int arrStart = jsonLine.indexOf('[', pwStart); + int arrEnd = jsonLine.indexOf(']', arrStart); + if (arrStart >= 0 && arrEnd > arrStart) { + String arrContent = jsonLine.substring(arrStart + 1, arrEnd); + int idx = 0; + while (idx < arrContent.length()) { + int qStart = arrContent.indexOf('"', idx); + if (qStart < 0) break; + int qEnd = findClosingQuote(arrContent, qStart + 1); + if (qEnd < 0) break; + request.passwords.add(unescapeJsonString(arrContent.substring(qStart + 1, qEnd))); + idx = qEnd + 1; + } + } + } + if (request.archiveFile == null || !request.archiveFile.exists() || !request.archiveFile.isFile()) { + throw new IllegalArgumentException("Archiv nicht gefunden: " + + (request.archiveFile == null ? "null" : request.archiveFile.getAbsolutePath())); + } + if (request.targetDir == null) { + throw new IllegalArgumentException("--target fehlt"); + } + return request; + } + + private static String extractJsonString(String json, String key) { + String search = "\"" + key + "\""; + int keyIdx = json.indexOf(search); + if (keyIdx < 0) return ""; + int colonIdx = json.indexOf(':', keyIdx + search.length()); + if (colonIdx < 0) return ""; + int qStart = json.indexOf('"', colonIdx + 1); + if (qStart < 0) return ""; + int qEnd = findClosingQuote(json, qStart + 1); + if (qEnd < 0) return ""; + return unescapeJsonString(json.substring(qStart + 1, qEnd)); + } + + private static int findClosingQuote(String s, int from) { + for (int i = from; i < s.length(); i++) { + char c = s.charAt(i); + if (c == '\\') { + i++; // skip escaped character + continue; + } + if (c == '"') return i; + } + return -1; + } + + private static String unescapeJsonString(String s) { + if (s.indexOf('\\') < 0) return s; + StringBuilder sb = new StringBuilder(s.length()); + for (int i = 0; i < s.length(); i++) { + char c = s.charAt(i); + if (c == '\\' && i + 1 < s.length()) { + char next = s.charAt(i + 1); + switch (next) { + case '"': sb.append('"'); i++; break; + case '\\': sb.append('\\'); i++; break; + case '/': sb.append('/'); i++; break; + case 'n': sb.append('\n'); i++; break; + case 'r': sb.append('\r'); i++; break; + case 't': sb.append('\t'); i++; break; + default: sb.append(c); break; + } + } else { + sb.append(c); + } + } + return sb.toString(); + } + private static int runExtraction(ExtractionRequest request) throws Exception { List passwords = normalizePasswords(request.passwords); Exception lastError = null; diff --git a/src/main/download-manager.ts b/src/main/download-manager.ts index a6a33d3..29158dd 100644 --- a/src/main/download-manager.ts +++ b/src/main/download-manager.ts @@ -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 = Object.create(null); - const archiveStartTimesCache: Record = Object.create(null); + const hybridInitializedArchives = new Set(); + 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 = Object.create(null); - const fullStartTimesCache: Record = Object.create(null); + const fullInitializedArchives = new Set(); + 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) { diff --git a/src/main/extractor.ts b/src/main/extractor.ts index d975881..1962bfe 100644 --- a/src/main/extractor.ts +++ b/src/main/extractor.ts @@ -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 { + 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. diff --git a/src/main/main.ts b/src/main/main.ts index 053f413..562e407 100644 --- a/src/main/main.ts +++ b/src/main/main.ts @@ -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) {