From c13754ded3960e080b09b95c4deae72200b513a1 Mon Sep 17 00:00:00 2001 From: BrendanC23 <4711227+BrendanC23@users.noreply.github.com> Date: Sun, 1 Feb 2026 20:57:49 -0600 Subject: [PATCH 01/11] Add runOptions.timeout This commit adds new `runOptions.timeout` and `runOptions.killSignal` options that allow a process to be killed after a specified number of milliseconds have elapsed. See the [Node ChildProcess docs](https://nodejs.org/api/child_process.html#child_processspawncommand-args-options). This commit includes several additional changes that were necessary as part of the implementation of `timeout`: * Fix to `JavaCallerTester.java` that replaces an `==` string comparison with a `args[0].equals("--sleep")`. This fixes the `sleep` command line argument, which previously never triggered because the comparison was always false. * Recompile `JavaCallerTester.class` and `JavaCallerTester.jar` * Fixed `should call JavaCallerTester.class detached` test. This began failing after the call fix for `--sleep`. The test has been rewritten to demonstrate the expected behavior for `detached`. Now, the test will check that the Java process is initially still running and then check the return status code once it exits. * Update the NPM script `java:compile` to use `--release 8` instead of `-source 8 -target 1.8`. This fixes an error that occurred when running the previous script: >warning: [options] bootstrap class path is not set in conjunction with -source 8 not setting the bootstrap class path may lead to class files that cannot run on JDK 8 --release 8 is recommended instead of -source 8 -target 1.8 because it sets the bootstrap class path automatically See [this StackOverflow post](https://stackoverflow.com/a/61715683) for more details. Note that a warning is still generated: >warning: [options] target value 8 is obsolete and will be removed in a future release warning: [options] To suppress warnings about obsolete options, use -Xlint:-options. Fixes https://github.com/nvuillam/node-java-caller/issues/144 --- README.md | 4 +- lib/java-caller.js | 24 +++++++-- package.json | 4 +- test/java-caller.test.js | 49 +++++++++++++++++- .../javacaller/JavaCallerTester.class | Bin 1670 -> 1732 bytes test/java/jar/JavaCallerTester.jar | Bin 1465 -> 1484 bytes test/java/jar/JavaCallerTesterRunnable.jar | Bin 1501 -> 1515 bytes .../nvuillam/javacaller/JavaCallerTester.java | 6 +-- 8 files changed, 74 insertions(+), 13 deletions(-) diff --git a/README.md b/README.md index 620d9b9..23d62a2 100644 --- a/README.md +++ b/README.md @@ -66,13 +66,15 @@ Example: `["-Xms256m", "--someflagwithvalue myVal", "-c"]` | Parameter | Description | Default | Example | |-----------|-------------|---------|---------| -| [detached](https://nodejs.org/api/child_process.html#child_process_child_process_spawn_command_args_options) | If set to true, node will node wait for the java command to be completed.
In that case, `childJavaProcess` property will be returned, but `stdout` and `stderr` may be empty, except if an error is triggered at command execution | `false` | `true` +| [detached](https://nodejs.org/api/child_process.html#child_process_child_process_spawn_command_args_options) | If set to true, node will not wait for the java command to be completed.
In that case, `childJavaProcess` property will be returned, but `stdout` and `stderr` may be empty, except if an error is triggered at command execution | `false` | `true` | [stdoutEncoding](https://nodejs.org/api/stream.html#readablesetencodingencoding) | Adds control on spawn process stdout | `utf8` | `ucs2` | | waitForErrorMs | If detached is true, number of milliseconds to wait to detect an error before exiting JavaCaller run | `500` | `2000` | | [cwd](https://nodejs.org/api/child_process.html#child_process_child_process_spawn_command_args_options) | You can override cwd of spawn called by JavaCaller runner | `process.cwd()` | `some/other/cwd/folder` | | javaArgs | List of arguments for JVM only, not the JAR or the class | `[]` | `['--add-opens=java.base/java.lang=ALL-UNNAMED']` | | [windowsVerbatimArguments](https://nodejs.org/api/child_process.html#child_process_child_process_spawn_command_args_options) | No quoting or escaping of arguments is done on Windows. Ignored on Unix. This is set to true automatically when shell is specified and is CMD. | `true` | `false` | | [windowless](https://docs.oracle.com/en/java/javase/17/docs/specs/man/java.html#:~:text=main()%20method.-,javaw,information%20if%20a%20launch%20fails.) | If windowless is true, JavaCaller calls javaw instead of java to not create any windows, useful when using detached on Windows. Ignored on Unix. | false | true +| [timeout](https://nodejs.org/api/child_process.html#child_process_child_process_spawn_command_args_options) | In milliseconds the maximum amount of time the process is allowed to run. | `undefined` | `1000` +| [killSignal](https://nodejs.org/api/child_process.html#child_process_child_process_spawn_command_args_options) | The signal value to be used when the spawned process will be killed by timeout or abort signal. | `SIGTERM` | `SIGINT` ## Examples diff --git a/lib/java-caller.js b/lib/java-caller.js index 16e7197..d78e7d0 100644 --- a/lib/java-caller.js +++ b/lib/java-caller.js @@ -69,12 +69,14 @@ class JavaCaller { * Runs java command of a JavaCaller instance * @param {string[]} [userArguments] - Java command line arguments * @param {object} [runOptions] - Run options - * @param {boolean} [runOptions.detached = false] - If set to true, node will node wait for the java command to be completed. In that case, childJavaProcess property will be returned, but stdout and stderr may be empty + * @param {boolean} [runOptions.detached = false] - If set to true, node will not wait for the java command to be completed. In that case, childJavaProcess property will be returned, but stdout and stderr may be empty * @param {string} [runOptions.stdoutEncoding = 'utf8'] - Adds control on spawn process stdout * @param {number} [runOptions.waitForErrorMs = 500] - If detached is true, number of milliseconds to wait to detect an error before exiting JavaCaller run * @param {string} [runOptions.cwd = .] - You can override cwd of spawn called by JavaCaller runner * @param {string} [runOptions.javaArgs = []] - You can override cwd of spawn called by JavaCaller runner * @param {string} [runOptions.windowsVerbatimArguments = true] - No quoting or escaping of arguments is done on Windows. Ignored on Unix. This is set to true automatically when shell is specified and is CMD. + * @param {number} [runOptions.timeout] - In milliseconds the maximum amount of time the process is allowed to run + * @param {NodeJS.Signals | number} [runOptions.killSignal = "SIGTERM"] - The signal value to be used when the spawned process will be killed by timeout or abort signal. * @return {Promise<{status:number, stdout:string, stderr:string, childJavaProcess:ChildProcess}>} - Command result (status, stdout, stderr, childJavaProcess) */ async run(userArguments, runOptions = {}) { @@ -84,6 +86,7 @@ class JavaCaller { runOptions.stdoutEncoding = typeof runOptions.stdoutEncoding === "undefined" ? "utf8" : runOptions.stdoutEncoding; runOptions.windowsVerbatimArguments = typeof runOptions.windowsVerbatimArguments === "undefined" ? true : runOptions.windowsVerbatimArguments; runOptions.windowless = typeof runOptions.windowless === "undefined" ? false : os.platform() !== "win32" ? false : runOptions.windowless; + runOptions.killSignal = typeof runOptions.killSignal === "undefined" ? "SIGTERM" : runOptions.killSignal; this.commandJavaArgs = (runOptions.javaArgs || []).concat(this.additionalJavaArgs); let javaExe = runOptions.windowless ? this.javaExecutableWindowless : this.javaExecutable; @@ -111,6 +114,8 @@ class JavaCaller { stdio: this.output === "console" ? "inherit" : runOptions.detached ? "ignore" : "pipe", windowsHide: true, windowsVerbatimArguments: runOptions.windowsVerbatimArguments, + timeout: runOptions.timeout, + killSignal: runOptions.killSignal, }; if (javaExeToUse.includes(" ")) { spawnOptions.shell = true; @@ -136,10 +141,19 @@ class JavaCaller { }); // Catch status code - child.on("close", (code) => { - this.status = code; - resolve(); - }); + child.on("close", (code, signal) => { + if (code === null && runOptions.timeout && signal === runOptions.killSignal) { + // Process was _likely_ terminated because of a timeout + // There is no way to determine if a timeout occurred or if another process sent killSignal + // See https://github.com/nodejs/node/issues/51561 + this.status = 666; + stderr += `Process timed out with ${runOptions.killSignal} after ${runOptions.timeout}ms.`; + } else { + this.status = code; + } + + resolve(); + }); // Detach from main process in case detached === true if (runOptions.detached) { diff --git a/package.json b/package.json index f574f02..5603e8c 100644 --- a/package.json +++ b/package.json @@ -8,7 +8,7 @@ ], "scripts": { "lint:fix": "eslint **/*.js --fix && prettier --write \"./lib/**/*.{js,jsx,json}\" --tab-width 4 --print-width 150", - "java:compile": "javac -d test/java/dist -source 8 -target 1.8 test/java/src/com/nvuillam/javacaller/JavaCallerTester.java", + "java:compile": "javac -d test/java/dist --release 8 test/java/src/com/nvuillam/javacaller/JavaCallerTester.java", "java:jar": "cd test/java/dist && jar -cvfm ./../jar/JavaCallerTester.jar ./../jar/manifest/Manifest.txt com/nvuillam/javacaller/*.class && jar -cvfm ./../jar/JavaCallerTesterRunnable.jar ./../jar/manifest-runnable/Manifest.txt com/nvuillam/javacaller/*.class", "test": "mocha \"test/**/*.test.js\"", "test:coverage": "nyc npm run test", @@ -74,4 +74,4 @@ ], "all": true } -} +} \ No newline at end of file diff --git a/test/java-caller.test.js b/test/java-caller.test.js index d69cd78..c3c7c05 100644 --- a/test/java-caller.test.js +++ b/test/java-caller.test.js @@ -31,9 +31,21 @@ describe("Call with classes", () => { classPath: 'test/java/dist', mainClass: 'com.nvuillam.javacaller.JavaCallerTester' }); + + // JavaCallerTester will sleep for 1000 ms + // After waitForErrorMs (500 ms), the promise will return const { status, stdout, stderr, childJavaProcess } = await java.run(['--sleep'], { detached: true }); - childJavaProcess.kill('SIGINT'); - checkStatus(0, status, stdout, stderr); + + // Java process is still running + checkStatus(null, status, stdout, stderr); + + return new Promise(resolve => { + // Java process has finished executing and the exit code can be read + childJavaProcess.on('exit', () => { + checkStatus(0, childJavaProcess.exitCode, stdout, stderr); + resolve(); + }); + }); }); it("should call JavaCallerTester.class using javaw", async () => { @@ -175,4 +187,37 @@ describe("Call with classes", () => { await javaCli.process(); }); + it("should terminate once timeout is reached", async () => { + const java = new JavaCaller({ + classPath: 'test/java/dist', + mainClass: 'com.nvuillam.javacaller.JavaCallerTester' + }); + const { status, stdout, stderr } = await java.run(["--sleep"], { timeout: 500 }); + + checkStatus(666, status, stdout, stderr); + checkStdErrIncludes(`timed out`, stdout, stderr); + }); + + it("should not terminate if process finished before timeout is reached", async () => { + const java = new JavaCaller({ + classPath: 'test/java/dist', + mainClass: 'com.nvuillam.javacaller.JavaCallerTester' + }); + const { status, stdout, stderr } = await java.run(["--sleep"], { timeout: 1500 }); + + checkStatus(0, status, stdout, stderr); + checkStdOutIncludes(`JavaCallerTester is called !`, stdout, stderr); + }); + + it("should terminate with custom killSignal when timeout is reached", async () => { + const java = new JavaCaller({ + classPath: 'test/java/dist', + mainClass: 'com.nvuillam.javacaller.JavaCallerTester' + }); + const { status, stdout, stderr } = await java.run(["--sleep"], { timeout: 500, killSignal: "SIGINT" }); + + checkStatus(666, status, stdout, stderr); + checkStdErrIncludes(`timed out`, stdout, stderr); + checkStdErrIncludes(`SIGINT`, stdout, stderr); + }); }); diff --git a/test/java/dist/com/nvuillam/javacaller/JavaCallerTester.class b/test/java/dist/com/nvuillam/javacaller/JavaCallerTester.class index 0f905f798fb644f5e131bac0d55d6bcc93d95820..ea7e8ac9c2a1828acea369c372d7dc3b9b543c2a 100644 GIT binary patch delta 746 zcmXX^O;Zy=5Ph?gWW#1+U=avXB;hNB6^TJWAfQ1&%@?333iuUZMKA;ifh#u;o~)V^ zCy!jbkSZ2Rt1J(e2ToS`C-gs9mc4;JbibZ{-P8Sc-g&=!wX=VpzXIsP_K`Ngycz-s zst5_RpjCe3b=@v=dpDQgVW^8_)^mHgXg*h1jZQDEo6Dtm*FqKA(V-$N(20o5vep`r zO=dF5*~#(gsfk&JU`Ecc=5X9ex6sFbYgExKa1A{S&YiqzZZS9_sjfK{*X1$m@bwDZ zfMDQevFJv>{KMMVP3cwqY)FQcpd-%EAcMTY&@c?{#x0sHMU#b!yM@x)h8fy3i#uzZ zg^-<32#n#5e6Pek6PQ$SSKuD*%b!ZZJ&cr{OiMp+p&%>Jh$%V78?VeT2z%F_i%uHc zcqlN7N3z6+s^=J#SdT5$Q-OIb$X~qE9l$ez=XfC%N2l)<1FI001eUQP2OaI6*H~5Y zMqrKNRmX?a1e9|{KsvxhcAes8@{GLGH~~8%!0?u=owjWi^4K73Q_sLFElfG)tl#f9 zc<&EH$?8oOnh(%kM%!g{S-5%#f2NF>-eNLd|8)L{knC{T$9 zyrd$URx#kFdio0&du(0o5!0tn@gQyJk1aeofY)%kEG(uEQE%ukE#yX;l#kF1PU|hE hjZ>JPsO$Mq0mvCwz;9nu1?4XqRRnZuzgb~y{RjJYf`0%2 delta 707 zcmXX@&r?!S6#nje4<0-&VXr|kG7&`dWq@RcL1m?tphl^bWhoUAW@z|h>#mtLjWc)E zrdq|O4J@8aHE#N`=s)OxX`0R>Hs^fje&_q{J?DORzjAB8{(e3LFpB4OZH&2{IEh{r z0ggTd#Tyn>)!FH}#f2&Hm4!}d#P)N9F~DHkS}zti8I-}yP+rBLP^7TO;5Y@&AQ!7u z2TqHm6k;*4Ecw`o*pYgbGYnm#B6k@Y5|AAjWAJ7QyM;h?r?OpsQ4H)Bt6SxbO2Fci z9OF1IeoHA=8W&Vt$ zWsV1UC|)Z4u2mFNJmPpv^#^5tFAa${3CN)Bz)tZARXvm$Wt(vTRwuwvq-fDrbmA#W zgss*eB(*G78&Gw(2@hqX2`?ez@EziYocIZYqVE5QU;{mxiBJu`nFfaR;XP`rfwzkI zfAJS2mJTqgpEYr=iIj=UCMJoh(=}lXBZf#Trx2wR>sbkKiq_Ix&vzu7V v*3fC_OD688{nC4AhU~XaY3h|vWJ`|{uk9h9b%hhsA9Sn8>Ev8)k!$||uaSGw diff --git a/test/java/jar/JavaCallerTester.jar b/test/java/jar/JavaCallerTester.jar index 8482fad46f8985bd233bdff394673199c73e4254..3f53caca8420dbd6aff4dc07ab06c108f3970984 100644 GIT binary patch delta 1229 zcmdnVeTJJaz?+$ci-CcIfkAVrW6VT8S+Ix*0|y5OM8wF-|e{$6LC|NrM--UjOmy~W&uoZ=!bg2_9!q`#5g zm3kwzcJY?urx~4d?rk~ypt!BWEU?~3p|6D{($Rd1@v~ND`v<0*ie5dLa;9DC@3RF;G)nn;;`UC)Zg!r^kN1^tAhIA65R8Fv=c9x{3Q!yo#d_sw|UOq*&C( zee0A<;30ROmB9&?v)LYAz1t8zEq=;Q=^1^xX}0x?A0!*T;K+PDLv{Y=%hsj7{oX&G zq+})DSsQ!)=S_`Bo4)fBZRahg+aA83Yn3LcxBdG^$Gcm!?o2jp_mDYqa7o96Z?DQ6 zwl4d=^SSVaEf@4PcJ#AfsW@D^Gd6L?nCA}l2eF!)>{YRIhwzcWqd+JM5 zuarC#F}ia3wsw8Ogw-}0W{oZ7D(V;3aG0!iDbxMFZ%%Q9uu5#Y#09IOyvb?$eaoYk zo-&!7FCwzx=mpIdpWr2;C-!%rrcEr_<8g{$j?6b&U)RI zZ3h4S4u1d9sQB=@cU||h=BRbfjZ>bgvx%u`YHBneQ=D1Wy8lK+{jWZrD_fVZ@VXJ2 z!1a0G<~wV;{Eo*S{%o1sd8FH~)%eRZ-`VT0XRAuJCrD~$wPkY0SkH}PYn$hkFlGK) z$-L8XdKRVY&&bb8nw&HL-INZ;@|w=LlONm~JIg0sv%S@OY5qm~S(Uusx>(xMw;Vfk zSVCw+K=#ais^Nl7@{U2(QZMS4)<2W*oMe<=cQ8lI=(BoB)YXY>Zt;(P39jQ!Sk%bR zU#ME&lKGWyrkoS!Q~%kF{|?-le0@gvd(#g`4aKBSm`+f?s_nHjan`(Y+dujb^yYG8 z7zbpCs=R9R+SJy)0bq!CBGhL>-jlxUA$ttlJcV|T+jDU3>FUN z5?bqgWkaHqp;Hi7X3s;mwc87I&&+qYeqUnJgdolJ!n=0;X98t>9<96;mzWtCjx~g delta 1195 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 diff --git a/test/java/src/com/nvuillam/javacaller/JavaCallerTester.java b/test/java/src/com/nvuillam/javacaller/JavaCallerTester.java index 4ca851d..66a4b30 100644 --- a/test/java/src/com/nvuillam/javacaller/JavaCallerTester.java +++ b/test/java/src/com/nvuillam/javacaller/JavaCallerTester.java @@ -9,16 +9,16 @@ public static void main(String[] args) { System.out.println("JavaCallerTester is called !"); System.out.println(java.util.Arrays.toString(args)); - if (args.length > 0 && args[0] != null && args[0] == "--sleep") { + if (args.length > 0 && args[0] != null && args[0].equals("--sleep")) { try { - TimeUnit.MINUTES.sleep(1); + TimeUnit.MILLISECONDS.sleep(1000); } catch (InterruptedException eInterrupt) { System.err.println("JavaCallerTester interrupted !"); } catch (Throwable t) { System.err.println("JavaCallerTester crashed !"); } } - System.out.println("Java runtime version "+getVersion()); + System.out.println("Java runtime version " + getVersion()); } private static int getVersion() { From 456a2f1481ce36f20c921cfaf4fba15c67b0033c Mon Sep 17 00:00:00 2001 From: BrendanC23 <4711227+BrendanC23@users.noreply.github.com> Date: Sun, 1 Feb 2026 21:18:57 -0600 Subject: [PATCH 02/11] Add to changelog --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 31d2e55..fbc1a54 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,8 @@ ## Unreleased +- Add `timeout` and `killSignal` run options + ## [4.1.1] 2024-08-30 - Upgrade to njre v1.4.0 From 115c25e8dac5675f90600c5893bc531f9603870c Mon Sep 17 00:00:00 2001 From: BrendanC23 <4711227+BrendanC23@users.noreply.github.com> Date: Tue, 3 Feb 2026 21:25:21 -0600 Subject: [PATCH 03/11] Add timeout options to JavaCallerRunOptions type --- lib/index.d.ts | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/lib/index.d.ts b/lib/index.d.ts index a007ab4..2e3dca3 100644 --- a/lib/index.d.ts +++ b/lib/index.d.ts @@ -117,6 +117,20 @@ export interface JavaCallerRunOptions { * @default false */ windowless?: boolean; + + /** + * The number of milliseconds to wait before the Java process will time out. When this occurs, + * killSignal will ben + * @default undefined + */ + timeout?: number; + + /** + * If windowless is true, JavaCaller calls javaw instead of java to not create any windows, + * useful when using detached on Windows. Ignored on Unix. + * @default undefined + */ + killSignal?: number | NodeJS.Signals; } /** From ad39b013405ad6cab65918515d90259079d7aa55 Mon Sep 17 00:00:00 2001 From: BrendanC23 <4711227+BrendanC23@users.noreply.github.com> Date: Tue, 3 Feb 2026 22:11:06 -0600 Subject: [PATCH 04/11] Remove null check for code when checking for timeout This commit removes the check for `exitCode === null`, which will only be the case on Windows. On Linux, the `exitCode` will be populated. The [Node docs](https://nodejs.org/api/child_process.html#event-exit), say: >The exit code if the child process exited on its own, or null if the child process terminated due to a signal. >The 'exit' event is emitted after the child process ends. If the process exited, code is the final exit code of the process, otherwise null. --- lib/java-caller.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/java-caller.js b/lib/java-caller.js index c15b3ca..1fabcc2 100644 --- a/lib/java-caller.js +++ b/lib/java-caller.js @@ -142,7 +142,7 @@ class JavaCaller { // Catch status code child.on("close", (code, signal) => { - if (code === null && runOptions.timeout && signal === runOptions.killSignal) { + if (runOptions.timeout && signal === runOptions.killSignal) { // Process was _likely_ terminated because of a timeout // There is no way to determine if a timeout occurred or if another process sent killSignal // See https://github.com/nodejs/node/issues/51561 From 3b79cf1e4698612f947f15aaa4c3eac9e1fa9ed5 Mon Sep 17 00:00:00 2001 From: BrendanC23 <4711227+BrendanC23@users.noreply.github.com> Date: Tue, 3 Feb 2026 22:25:27 -0600 Subject: [PATCH 05/11] Fix killSignal default in TypeScript docs --- lib/index.d.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/index.d.ts b/lib/index.d.ts index 2e3dca3..5d7714c 100644 --- a/lib/index.d.ts +++ b/lib/index.d.ts @@ -128,7 +128,7 @@ export interface JavaCallerRunOptions { /** * If windowless is true, JavaCaller calls javaw instead of java to not create any windows, * useful when using detached on Windows. Ignored on Unix. - * @default undefined + * @default "SIGTERM" */ killSignal?: number | NodeJS.Signals; } From 9d1d97ef33f092140615ed18eceb13ecc33d3fd3 Mon Sep 17 00:00:00 2001 From: BrendanC23 <4711227+BrendanC23@users.noreply.github.com> Date: Tue, 3 Feb 2026 22:36:28 -0600 Subject: [PATCH 06/11] Remove no-undef ESLint check for *.ts files As recommended in the [TypeScript ESLint docs](https://typescript-eslint.io/troubleshooting/faqs/eslint#i-get-errors-from-the-no-undef-rule-about-global-variables-not-being-defined-even-though-there-are-no-typescript-errors), the `no-undef` rule should not be used for `*.ts` files because it gives false positives. This causes issues with using `NodeJS.Signals` in `index.d.ts`. --- .eslintrc.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.eslintrc.js b/.eslintrc.js index f7f5ce4..ed5bf82 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -34,7 +34,8 @@ module.exports = { files: ["**/*.d.ts"], parser: "@typescript-eslint/parser", rules: { - "getter-return": "off" + "getter-return": "off", + "no-undef": "off" } } ], From ea5297e8c7fe9736931722a93085a99cb573827b Mon Sep 17 00:00:00 2001 From: BrendanC23 <4711227+BrendanC23@users.noreply.github.com> Date: Tue, 3 Feb 2026 22:57:44 -0600 Subject: [PATCH 07/11] Add temporary logging --- lib/java-caller.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/lib/java-caller.js b/lib/java-caller.js index 1fabcc2..c049468 100644 --- a/lib/java-caller.js +++ b/lib/java-caller.js @@ -142,6 +142,8 @@ class JavaCaller { // Catch status code child.on("close", (code, signal) => { + console.log({code, signal}); + if (runOptions.timeout && signal === runOptions.killSignal) { // Process was _likely_ terminated because of a timeout // There is no way to determine if a timeout occurred or if another process sent killSignal From 0b98883cc3a0f1d817a8ca86eedd75ee3b2089ae Mon Sep 17 00:00:00 2001 From: Nicolas Vuillamy Date: Wed, 4 Feb 2026 21:55:34 +0100 Subject: [PATCH 08/11] Handle cross-platform timeout --- lib/java-caller.js | 33 +++++++++++++++------ test/java/jar/JavaCallerTester.jar | Bin 1484 -> 1502 bytes test/java/jar/JavaCallerTesterRunnable.jar | Bin 1515 -> 1533 bytes 3 files changed, 24 insertions(+), 9 deletions(-) diff --git a/lib/java-caller.js b/lib/java-caller.js index c049468..4ea48a2 100644 --- a/lib/java-caller.js +++ b/lib/java-caller.js @@ -100,10 +100,12 @@ class JavaCaller { const javaExeToUse = this.javaExecutableFromNodeJavaCaller ?? javaExe; const classPathStr = this.buildClasspathStr(); - const javaArgs = this.buildArguments(classPathStr, (userArguments || []).concat(this.commandJavaArgs)); + const javaArgs = this.buildArguments(classPathStr, (userArguments || []).concat(this.commandJavaArgs), runOptions.windowsVerbatimArguments); let stdout = ""; let stderr = ""; let child; + let timeoutId; + let killedByTimeout = false; const prom = new Promise((resolve) => { // Spawn java command line debug(`Java command: ${javaExeToUse} ${javaArgs.join(" ")}`); @@ -122,6 +124,19 @@ class JavaCaller { } child = spawn(javaExeToUse, javaArgs, spawnOptions); + if (runOptions.timeout) { + timeoutId = setTimeout(() => { + if (!child.killed) { + killedByTimeout = true; + try { + child.kill(runOptions.killSignal); + } catch (err) { + stderr += `Failed to kill process after ${runOptions.timeout}ms: ${err.message}`; + } + } + }, runOptions.timeout); + } + // Gather stdout and stderr if they must be returned if (spawnOptions.stdio === "pipe") { child.stdout.setEncoding(`${runOptions.stdoutEncoding}`); @@ -142,20 +157,20 @@ class JavaCaller { // Catch status code child.on("close", (code, signal) => { - console.log({code, signal}); - - if (runOptions.timeout && signal === runOptions.killSignal) { - // Process was _likely_ terminated because of a timeout - // There is no way to determine if a timeout occurred or if another process sent killSignal - // See https://github.com/nodejs/node/issues/51561 + if (timeoutId) { + clearTimeout(timeoutId); + } + + if (killedByTimeout || (runOptions.timeout && signal === runOptions.killSignal)) { + // Process was terminated because of the timeout, either via our fallback timer or the built-in spawn timeout this.status = 666; stderr += `Process timed out with ${runOptions.killSignal} after ${runOptions.timeout}ms.`; } else { this.status = code; } - resolve(); - }); + resolve(); + }); // Detach from main process in case detached === true if (runOptions.detached) { diff --git a/test/java/jar/JavaCallerTester.jar b/test/java/jar/JavaCallerTester.jar index 3f53caca8420dbd6aff4dc07ab06c108f3970984..66abda783fb1ade62a8e8fb06501fcacb36c01f3 100644 GIT binary patch delta 205 zcmX@ZeUFm!p)9_YsPEI7m`w1 z89UUTr-+Fx*RojlM0`rhv}akLiar)Gf=yUFCDFnOsM`_61kZIY8~3QP2#PSkEsX)$ w!8G|3i;+Cg2{Fj}L_kt-U_05FRhb!R!sJX=X~u&SPZ>_$#wx(}o&}@=05EbbSpWb4 delta 187 zcmcb|eTJJiz?+$ci-CcIfkAVr<3wILuz&~y2L}gOz{|~UOOS!lRpSfBAr}gK&z;oK z@Cz;QIjOI4Qs?Z|Cr=+eWnu{MX6F$1Otf%fU|?_nVvvy#6U3J~Zak;TBFM!6w;~3p xfPrB$3#*Yl(83sGeIj5f7@<8mlvSA-Xu{-bR%yod6Ym&KKEW!$c9#XD000y}Dxv@Y diff --git a/test/java/jar/JavaCallerTesterRunnable.jar b/test/java/jar/JavaCallerTesterRunnable.jar index edbfb45fa9eb832fe4167c7e1dc0cb35337f1af6..e12ebf267404f729e0209ce13c7a77026ce41f8e 100644 GIT binary patch delta 168 zcmaFO{g<0Jz?+#xgn@&DgTZ&5%S2u|d1i(HZ*~p_AYcL!K!B>mVga+~I+u;NRapc@ z7~nc%Knj^A3$hx?1I>vkXU}9#1d8MWu?R>C4s0hUu`0_0O^D&q%3E=XnStR58v}zL VTpq-DF!80~Z&g_YxftN;Vi-VR zvNEfYJWy9mIeR8^B2XX~h($m$aG*W8gjE@2!sJ=3(v0UO{x+QaoK=AB4+}^I07g6+ AS^xk5 From 24fc64ed5e538582aa96c0c166005102d2344148 Mon Sep 17 00:00:00 2001 From: Nicolas Vuillamy Date: Wed, 4 Feb 2026 21:59:52 +0100 Subject: [PATCH 09/11] unify timeout codes --- lib/java-caller.js | 22 +++++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/lib/java-caller.js b/lib/java-caller.js index 4ea48a2..545b379 100644 --- a/lib/java-caller.js +++ b/lib/java-caller.js @@ -106,6 +106,26 @@ class JavaCaller { let child; let timeoutId; let killedByTimeout = false; + + const wasKilledByTimeout = (code, signal) => { + if (!runOptions.timeout) { + return false; + } + if (killedByTimeout) { + return true; + } + if (signal && signal === runOptions.killSignal) { + return true; + } + const signals = os.constants && os.constants.signals ? os.constants.signals : {}; + if (typeof runOptions.killSignal === "string" && signals[runOptions.killSignal] && code === 128 + signals[runOptions.killSignal]) { + return true; + } + if (typeof runOptions.killSignal === "number" && code === 128 + runOptions.killSignal) { + return true; + } + return false; + }; const prom = new Promise((resolve) => { // Spawn java command line debug(`Java command: ${javaExeToUse} ${javaArgs.join(" ")}`); @@ -161,7 +181,7 @@ class JavaCaller { clearTimeout(timeoutId); } - if (killedByTimeout || (runOptions.timeout && signal === runOptions.killSignal)) { + if (wasKilledByTimeout(code, signal)) { // Process was terminated because of the timeout, either via our fallback timer or the built-in spawn timeout this.status = 666; stderr += `Process timed out with ${runOptions.killSignal} after ${runOptions.timeout}ms.`; From 962b23e255a4bfb11b17245cd357b90938e20ae6 Mon Sep 17 00:00:00 2001 From: BrendanC23 <4711227+BrendanC23@users.noreply.github.com> Date: Thu, 5 Feb 2026 20:15:14 -0600 Subject: [PATCH 10/11] Simplify timeout logic This commit simplifies the timeout logic by no longer passing `timeout` to `spawn`. Instead, the full timeout logic is handled by `run`, which sets a timeout and determines whether the process was killed. Now that there is no duplicate logic, `wasKilledByTimeout` can be removed in favor of checking `killedByTimeout` directly. --- lib/java-caller.js | 32 ++++++++------------------------ test/java-caller.test.js | 2 +- 2 files changed, 9 insertions(+), 25 deletions(-) diff --git a/lib/java-caller.js b/lib/java-caller.js index 545b379..fb26464 100644 --- a/lib/java-caller.js +++ b/lib/java-caller.js @@ -107,25 +107,6 @@ class JavaCaller { let timeoutId; let killedByTimeout = false; - const wasKilledByTimeout = (code, signal) => { - if (!runOptions.timeout) { - return false; - } - if (killedByTimeout) { - return true; - } - if (signal && signal === runOptions.killSignal) { - return true; - } - const signals = os.constants && os.constants.signals ? os.constants.signals : {}; - if (typeof runOptions.killSignal === "string" && signals[runOptions.killSignal] && code === 128 + signals[runOptions.killSignal]) { - return true; - } - if (typeof runOptions.killSignal === "number" && code === 128 + runOptions.killSignal) { - return true; - } - return false; - }; const prom = new Promise((resolve) => { // Spawn java command line debug(`Java command: ${javaExeToUse} ${javaArgs.join(" ")}`); @@ -136,8 +117,6 @@ class JavaCaller { stdio: this.output === "console" ? "inherit" : runOptions.detached ? "ignore" : "pipe", windowsHide: true, windowsVerbatimArguments: runOptions.windowsVerbatimArguments, - timeout: runOptions.timeout, - killSignal: runOptions.killSignal, }; if (javaExeToUse.includes(" ")) { spawnOptions.shell = true; @@ -147,9 +126,9 @@ class JavaCaller { if (runOptions.timeout) { timeoutId = setTimeout(() => { if (!child.killed) { - killedByTimeout = true; try { child.kill(runOptions.killSignal); + killedByTimeout = true; } catch (err) { stderr += `Failed to kill process after ${runOptions.timeout}ms: ${err.message}`; } @@ -172,6 +151,11 @@ class JavaCaller { child.on("error", (data) => { this.status = 666; stderr += "Java spawn error: " + data; + + if (timeoutId) { + clearTimeout(timeoutId); + } + resolve(); }); @@ -181,8 +165,8 @@ class JavaCaller { clearTimeout(timeoutId); } - if (wasKilledByTimeout(code, signal)) { - // Process was terminated because of the timeout, either via our fallback timer or the built-in spawn timeout + if (killedByTimeout) { + // Process was terminated because of the timeout this.status = 666; stderr += `Process timed out with ${runOptions.killSignal} after ${runOptions.timeout}ms.`; } else { diff --git a/test/java-caller.test.js b/test/java-caller.test.js index 6d2b3fd..a1229a0 100644 --- a/test/java-caller.test.js +++ b/test/java-caller.test.js @@ -200,7 +200,7 @@ describe("Call with classes", () => { checkStdOutIncludes(`JavaCallerTester is called !`, stdout, stderr); }); - it("should terminate once timeout is reached", async () => { + it("should terminate once timeout is reached", async () => { const java = new JavaCaller({ classPath: 'test/java/dist', mainClass: 'com.nvuillam.javacaller.JavaCallerTester' From 61cc107d5e6dbb5ab98196c0da4cdc8b8dc453a8 Mon Sep 17 00:00:00 2001 From: BrendanC23 <4711227+BrendanC23@users.noreply.github.com> Date: Thu, 5 Feb 2026 20:27:23 -0600 Subject: [PATCH 11/11] Remove unused variable --- lib/java-caller.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/java-caller.js b/lib/java-caller.js index fb26464..1545ee6 100644 --- a/lib/java-caller.js +++ b/lib/java-caller.js @@ -160,7 +160,7 @@ class JavaCaller { }); // Catch status code - child.on("close", (code, signal) => { + child.on("close", (code) => { if (timeoutId) { clearTimeout(timeoutId); }