-
-
Notifications
You must be signed in to change notification settings - Fork 11
Add timeout #147
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Add timeout #147
Changes from 10 commits
c13754d
456a2f1
2040f45
115c25e
ad39b01
3b79cf1
9d1d97e
ea5297e
0b98883
24fc64e
962b23e
61cc107
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -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,32 @@ 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 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; | ||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||
| 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; | |
| return killedByTimeout; |
Copilot
AI
Feb 4, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The implementation creates both a fallback setTimeout timer and passes timeout to the spawn options, which means Node.js's built-in timeout mechanism will also be active. This could result in redundant timeout handling. The built-in spawn timeout should be sufficient in most cases. Consider removing the custom setTimeout implementation (lines 147-158) since spawn already handles timeout internally, or document why both mechanisms are necessary.
Copilot
AI
Feb 4, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If a spawn error occurs (line 172), the timeout timer is not cleared before resolving. This means the timer will continue to run and attempt to kill the process even after the error handler has completed. The timeout should be cleared in the error handler as well to prevent this leak.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@copilot open a new pull request to apply changes based on this feedback
Copilot
AI
Feb 4, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The error message uses template literal syntax but doesn't include a newline or proper formatting. When concatenating to existing stderr content, this could result in the timeout message being appended directly to previous error output without separation. Consider adding a newline at the beginning of the message for better readability, similar to how other error messages are formatted in this codebase.
| stderr += `Process timed out with ${runOptions.killSignal} after ${runOptions.timeout}ms.`; | |
| stderr += `\nProcess timed out with ${runOptions.killSignal} after ${runOptions.timeout}ms.`; |
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
|
|
@@ -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 () => { | ||||||
|
||||||
| it("should terminate once timeout is reached", async () => { | |
| it("should terminate once timeout is reached", async () => { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The logic for detecting timeout based on exit code (128 + signal number) is Unix-specific and may not work correctly on Windows. On Windows, processes terminated by signals typically exit with different status codes. Consider testing this behavior on Windows and handling platform-specific differences, or rely solely on the
killedByTimeoutflag and signal parameter for detection.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@copilot open a new pull request to apply changes based on this feedback