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
zcmX@Zy_1_Sz?+#xgn@&DgW*6Sci==mSp{Z>0B?2<1|VPp5kP>Z#?sY_g=@wO$rqAZ
z85YZ)h)?+;zT{cu&Wh|$MIVb8!Me}YulQmM)Mwc7f%6WdQ@p8vSk
z?uYr;FJI27$#J>(e@`q+d-wNUdHvbi&*$aWGu$~~ENR=}X|Z^Lcwt+y^|N)ESA3VJ
ztqCvNA!>9)Jm)xHzTuIem-R(LoGPj(xPC2=3D$qds+d1v+HI8!1&WhytlRkTwqFEK
zZJt>X=bY3XTYM(DZjO6i*C~AL`WuJMM|Q6AJ#EqM$hoRXaZb9#tE5)PPe)e16YyR2
zL7v}f*Se>dQYDplC_G%$6xg%3bd%(*(#L)8XIC>iE0x^7#bdm3?V*zkieFw%s~6tk
za%S^p$GOG&ot{3P7ess>8y!9(*!t=@6$f^?wa98cZlr-g!eCcDQyl|41*{uigX*s8oj{VlQXBJSZI7OwE=S;Bcn
z=vm3G{sp(TusqJ4rM!D-?)|?f*IcP&`plDDzx=?oe74)U58Qdg{g%DVySy%Zr@nG`
z1k?P0))v;Y`BsXpE8Z5_E4Z5~?qB)9xW?~qYL#61DxS@EyDDF;I_xkr>VUXVp5~5T
zeaWC{w+-gR3SB$frsxpp{qxvK4v8mH{HJr>7cD!-o&Sz2Uu>my@XzmsX&Y5`y|O#}
zGvvD3&sp`3g4bvItuVTp5_yn|i>KCQ&%@nv-w)fiW&hWXG1__Tg7N%`8yoXgYF01k
zxb$)Q^_MA@9KQBEayK4+vyMBaoVaAo1IB&H9&Bg5E;RNn`4oFd?VUT1&Z2*D2l;d((#m&*+T^yVL!5)hcc`Ssvq*=XUP{uRsQ3#@{&_3L+qo-+v8AiM8cPRI(6
zMc+Lx805PD*n0JCqwyDU=gU>wvZ`cjcfK&I?Y_VLXyT8Qv&M(mY_GCPl|AZi>@0N8
zp6Vz3CH>3$tMN^H7w|GKH-P-zf{>Am*k44{zxp}-}on1~$Vs_%5
z=iw)+a$@cTo$0Sv54gSD(_>3t(7IzMx(jdlIvp1Ixol#7xY9qBs(;5;wD3-?c+0Wt
zvp%R;2#Cv3-^9$o(8dNT78sdC7!ZXAsHk9?{E5X#9#oVd7Xcz5DLAm5?98ex4=N0h
YP1A$RgBW`zo-&-gja7i{6bpz80P!FbeEAG+*O=dVa*^D$z`#dUaINH_*MH&
zi+wWvi~E-^U;1+Rj#^k=O)5!$_j>pDI^+ERKmYPJSXbyR<`(1>7jY3x-mxY9jr6Y6
z8=dBNd?NXO7n%iFH
z<7zzK8)eql%@ZE(u{k3`?E5y0pB0&x{ih$@H%V@KA!n^n2T}^LMz$eEHJ*>Uo6^jE^uc&pcb;ef#8g@wsy?k}rdw$U(aY$;b!zp#eGWVK6~?)QCjiX((oV$&rqSQX_>PSfvO9<}t8
z$>e+ykqt*L_;#Ax9)B8FaG>CG{ZwzoZl2i7!A%G6YgIJmt~$WaqyIsE_Q7}7>#l4w
z`0sb{`;SJ&hu6L9x}P;it#fXi@>HEoOifc$qxqQP%(B+~H!A9X_3>QUx_pJ#jnD+H
z&-*stS<~frJofNs%iPW*-F~gcU!M8SUVlAXRjNHfQZuV9lRL(GZX8?NJg0;y^Vdq|
zosQG9C|!R>epb@tocZsjbU2pRbjF?h;MUk#KIxk6t=>!XFWS$l0B?2<1|VPp5kP>Z#&SN(ZjjnL4)yoL
zh22H|wRxCDicNG4a7ehQbuw(zgj?6lLRNR=D*VY1PMW%H*Ug?p!<%Ael3s*)_=#Um_K3KZIufJ
zij!`v+xYOdUj$EWo>>v+oYWm#d?vYWj(c9$DSYhu8;8wDcCPY0ZPD(?xvEKVPP)Xa
zq*ljIM^?TQ@Llymp5JNLx~G>?C6#w5JY3Wi*t55EljN<^$9?Z-S2H>*mE69?W4vQmVTTnroJ_M4w%s
zJ8fDQPjKt^RnJn4{xmkN-f`@LbfDoJPu=CGg@Sk{yT?71JvHb47pJ(`s=Pw|EwSz*
z?%^L6uJGwu!g)sMS;?;c1-G`aJkFh^ynAWx{l6#IT&ZOG%#&Qd{J^w)w%fT6+@YLxfVfbe=8j%{$)IVs4d%oOT|3*R=n&}r^Vmrai6>J0r*qvGEj!1Z|BfqPY^8PZ
z&+mn48&!6_vOD}U>gB&(B|%%MAkb<{y|65SYOE_26UKXyKp!70cuctb(%j>vx5o
zGYHrqyYE^~$O?}|-#soEL>gq{mc8S@lAUd@G>tqm5V(#@ksC$t5x-aAu}q97(CzRCpP^1
zrP!9aeE%=j;wqirQdVnU{m9=@)4;j2Rr`1SjtC2fsJ(tszXKNijgkJXzKK8nQQNOV
z?)h>`*ALAVdpGd~>&BR)r4lnb|5>KIaI2Ww>a_aBD!UZ|lOO4vZ1*T;C^oUlzhu|$
z$Q63jmglkI*+aQbUoIw0GuW)8I9b7B?@T|BIQ1o!UspRn2`?$#+WK|=#r5BhMc;_I
zdAwttT~175cH*Ar;U}tcV(tW;>91E0xV_xdV@qGqx??B03vc;49TxeyY+`=6(m$1|
zf5%p|@J_9G%dzXTJ}83+#O0`OVrF1yV*_PyMkWykL@@x$^h}clS&igDg+e)dCUYWC
pBo~N9KvHmEJ2{C}SsqjvAe*KKmj^K(OnhlL`6{ab+dCEz7XUj504o3h
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);
}