Conversation
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 nvuillam#144
|
@BrendanC23 looks great :) |
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.
|
@BrendanC23 it seems to work great on winfows, but not on mac / ubuntu, please can you check CI job logs ? |
|
It looks like on Linux,
Instead, the code is |
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`.
|
I'm still working on a full fix for the failing tests. Apparently this line still isn't executing. I turned off the I noticed that ESLint isn't running locally. A previous PR updated it to version 9, which requires a flat config file. I was also getting an error that |
|
I added some logging. On Linux, for the failing tests, |
|
@copilot can you help make the timeout feature work on mac and linux ? |
|
@BrendanC23 Copilot can not act on forked repos pull requests, do you have it ? (it should work even with the free tier) |
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## main #147 +/- ##
==========================================
- Coverage 89.95% 89.38% -0.57%
==========================================
Files 3 3
Lines 229 245 +16
==========================================
+ Hits 206 219 +13
- Misses 23 26 +3
Flags with carried forward coverage won't be shown. Click here to find out more. ☔ View full report in Codecov by Sentry. 🚀 New features to boost your workflow:
|
There was a problem hiding this comment.
Pull request overview
This PR adds support for Node.js spawn's timeout and killSignal options to the JavaCaller library, addressing issue #144. The implementation allows users to specify a maximum execution time for Java processes and control which signal is used to terminate them when the timeout is reached.
Changes:
- Added
timeoutandkillSignaloptions toJavaCallerRunOptions - Fixed a bug in
JavaCallerTester.javawhere string comparison used==instead of.equals() - Updated Java compilation command to use
--release 8instead of deprecated-source 8 -target 1.8flags - Modified detached mode test to properly demonstrate and verify detached process behavior
Reviewed changes
Copilot reviewed 7 out of 11 changed files in this pull request and generated 6 comments.
Show a summary per file
| File | Description |
|---|---|
| lib/java-caller.js | Implements timeout functionality with dual mechanism (built-in spawn timeout and custom setTimeout), adds signal detection logic, and handles timeout status codes |
| lib/index.d.ts | Adds TypeScript type definitions for new timeout and killSignal options |
| test/java-caller.test.js | Updates detached test to verify proper behavior and adds three new tests for timeout functionality |
| test/java/src/com/nvuillam/javacaller/JavaCallerTester.java | Fixes string comparison bug and changes sleep duration from minutes to milliseconds |
| test/java/jar/*.jar | Recompiled JAR files with bug fixes |
| test/java/dist/*.class | Recompiled class files with bug fixes |
| package.json | Updates Java compilation command to use modern --release 8 flag |
| .eslintrc.js | Disables no-undef rule for TypeScript definition files |
| README.md | Adds documentation for new timeout and killSignal options |
| CHANGELOG.md | Documents the new feature |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
test/java-caller.test.js
Outdated
| checkStdOutIncludes(`JavaCallerTester is called !`, stdout, stderr); | ||
| }); | ||
|
|
||
| it("should terminate once timeout is reached", async () => { |
There was a problem hiding this comment.
Inconsistent indentation: this test block has incorrect indentation (extra leading spaces) compared to other test blocks in the file. It should start at the same indentation level as the other it blocks.
| it("should terminate once timeout is reached", async () => { | |
| it("should terminate once timeout is reached", async () => { |
lib/java-caller.js
Outdated
| 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; |
There was a problem hiding this comment.
The wasKilledByTimeout function may incorrectly identify a process as timed out if it naturally exits with a signal that matches killSignal without actually being killed by the timeout. For example, if a Java process catches and handles SIGTERM gracefully and exits with that signal code, it would be incorrectly identified as timed out. Consider only relying on the killedByTimeout flag when it's set, and for other cases, only check the signal if you're certain the process was actually killed (e.g., by checking if the signal matches AND the timeout has actually elapsed).
| 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; |
| child.on("error", (data) => { | ||
| this.status = 666; | ||
| stderr += "Java spawn error: " + data; | ||
| resolve(); | ||
| }); |
There was a problem hiding this comment.
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.
@copilot open a new pull request to apply changes based on this feedback
| 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.`; |
There was a problem hiding this comment.
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.`; |
| 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); | ||
| } |
There was a problem hiding this comment.
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.
lib/java-caller.js
Outdated
| 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; | ||
| } |
There was a problem hiding this comment.
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 killedByTimeout flag and signal parameter for detection.
There was a problem hiding this comment.
@copilot open a new pull request to apply changes based on this feedback
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.
|
@nvuillam Can you review this? Your plan to manually handle the timeout instead of passing it to |
nvuillam
left a comment
There was a problem hiding this comment.
Looks fine to me :)
Thanks for the PR ,I'll merge and release later today or this week-end :)
Fixes #144
Proposed Changes
timeoutandkillSignaloptionsThis commit adds new
runOptions.timeoutandrunOptions.killSignaloptions that allow a process to be killed after a specified number of milliseconds have elapsed. See the Node ChildProcess docs.This commit includes several additional changes that were necessary as part of the implementation of
timeout:JavaCallerTester.javathat replaces an==string comparison with aargs[0].equals("--sleep"). This fixes thesleepcommand line argument, which previously never triggered because the comparison was always false.JavaCallerTester.classandJavaCallerTester.jarshould call JavaCallerTester.class detachedtest. This began failing after the call fix for--sleep. The test has been rewritten to demonstrate the expected behavior fordetached. Now, the test will check that the Java process is initially still running and then check the return status code once it exits.java:compileto use--release 8instead of-source 8 -target 1.8. This fixes an error that occurred when running the previous script:See this StackOverflow post for more details.
Note that a warning is still generated:
Readiness Checklist
Author/Contributor
Reviewing Maintainer
breakingif this is a large fundamental changeautomation,bug,documentation,enhancement,infrastructure, orperformance