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"
}
}
],
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 1e29773..d215aa2 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,6 +2,8 @@
## Unreleased
+- Add `timeout` and `killSignal` run options
+
## [4.3.3] 2025-02-03
- Add types definition to make library compliant with typescript usage
diff --git a/README.md b/README.md
index 261e383..9aa8c8c 100644
--- a/README.md
+++ b/README.md
@@ -65,13 +65,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/index.d.ts b/lib/index.d.ts
index a007ab4..5d7714c 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 "SIGTERM"
+ */
+ killSignal?: number | NodeJS.Signals;
}
/**
diff --git a/lib/java-caller.js b/lib/java-caller.js
index b15818e..1545ee6 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;
@@ -97,10 +100,13 @@ 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(" ")}`);
@@ -117,6 +123,19 @@ class JavaCaller {
}
child = spawn(javaExeToUse, javaArgs, spawnOptions);
+ if (runOptions.timeout) {
+ timeoutId = setTimeout(() => {
+ if (!child.killed) {
+ try {
+ child.kill(runOptions.killSignal);
+ killedByTimeout = true;
+ } 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}`);
@@ -132,12 +151,28 @@ class JavaCaller {
child.on("error", (data) => {
this.status = 666;
stderr += "Java spawn error: " + data;
+
+ if (timeoutId) {
+ clearTimeout(timeoutId);
+ }
+
resolve();
});
// Catch status code
child.on("close", (code) => {
- this.status = code;
+ if (timeoutId) {
+ clearTimeout(timeoutId);
+ }
+
+ if (killedByTimeout) {
+ // Process was terminated because of the timeout
+ this.status = 666;
+ stderr += `Process timed out with ${runOptions.killSignal} after ${runOptions.timeout}ms.`;
+ } else {
+ this.status = code;
+ }
+
resolve();
});
diff --git a/package.json b/package.json
index 549880d..8045c6c 100644
--- a/package.json
+++ b/package.json
@@ -9,7 +9,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",
@@ -78,4 +78,4 @@
],
"all": true
}
-}
+}
\ No newline at end of file
diff --git a/test/java-caller.test.js b/test/java-caller.test.js
index 2fafb2f..a1229a0 100644
--- a/test/java-caller.test.js
+++ b/test/java-caller.test.js
@@ -32,9 +32,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 () => {
@@ -187,4 +199,38 @@ describe("Call with classes", () => {
checkStatus(0, status, stdout, stderr);
checkStdOutIncludes(`JavaCallerTester is called !`, stdout, stderr);
});
+
+ 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 0f905f7..ea7e8ac 100644
Binary files a/test/java/dist/com/nvuillam/javacaller/JavaCallerTester.class and b/test/java/dist/com/nvuillam/javacaller/JavaCallerTester.class differ
diff --git a/test/java/jar/JavaCallerTester.jar b/test/java/jar/JavaCallerTester.jar
index 8482fad..66abda7 100644
Binary files a/test/java/jar/JavaCallerTester.jar and b/test/java/jar/JavaCallerTester.jar differ
diff --git a/test/java/jar/JavaCallerTesterRunnable.jar b/test/java/jar/JavaCallerTesterRunnable.jar
index b2d6277..e12ebf2 100644
Binary files a/test/java/jar/JavaCallerTesterRunnable.jar and b/test/java/jar/JavaCallerTesterRunnable.jar differ
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() {