diff --git a/core/src/main/java/io/roastedroot/quickjs4j/core/Runner.java b/core/src/main/java/io/roastedroot/quickjs4j/core/Runner.java index 8afa242..81f0605 100644 --- a/core/src/main/java/io/roastedroot/quickjs4j/core/Runner.java +++ b/core/src/main/java/io/roastedroot/quickjs4j/core/Runner.java @@ -60,7 +60,10 @@ public void compileAndExec(String code) { public Object invokeGuestFunction( String moduleName, String name, List args, String libraryCode) { - return engine.invokeGuestFunction(moduleName, name, args, libraryCode); + return submitWithTimeout( + () -> engine.invokeGuestFunction(moduleName, name, args, libraryCode), + this.timeoutMs, + "Timeout while invoking guest function"); } public String stdout() { @@ -98,17 +101,17 @@ private T submitWithTimeout(Callable task, int timeout, String timeoutMes throw new RuntimeException("Thread interrupted", e); } catch (ExecutionException e) { if (e.getCause() != null) { - if (e.getCause() instanceof RuntimeException) { - throw (RuntimeException) e.getCause(); - } else { - throw new RuntimeException(e.getCause()); - } - } else { - throw new RuntimeException(e); + sneakyThrow(e.getCause()); } + throw new RuntimeException(e); } } + @SuppressWarnings("unchecked") + private static void sneakyThrow(Throwable e) throws E { + throw (E) e; + } + public static Builder builder() { return new Builder(); } diff --git a/core/src/test/java/io/roastedroot/quickjs4j/core/RunnerTest.java b/core/src/test/java/io/roastedroot/quickjs4j/core/RunnerTest.java index 7d11aba..2a4adaa 100644 --- a/core/src/test/java/io/roastedroot/quickjs4j/core/RunnerTest.java +++ b/core/src/test/java/io/roastedroot/quickjs4j/core/RunnerTest.java @@ -339,6 +339,43 @@ public void compileTimeout() throws Exception { runner.close(); } + @Test + public void invokeGuestFunctionTimeout() throws Exception { + var invokables = + Invokables.builder("from_js") + .add(new GuestFunction("hang", List.of(), Integer.class)) + .build(); + + var libraryCode = "function hang() { while(true) {} return 1; };"; + + var es = Executors.newSingleThreadExecutor(); + var jsEngine = Engine.builder().addInvokables(invokables).build(); + var runner = + Runner.builder() + .withEngine(jsEngine) + .withTimeoutMs(500) + .withExecutorService(es) + .build(); + + var ex = + assertThrows( + RuntimeException.class, + () -> + runner.invokeGuestFunction( + "from_js", "hang", List.of(), libraryCode)); + + assertTrue(ex.getCause() instanceof TimeoutException); + assertTrue( + ex.getMessage().contains("Timeout while invoking guest function"), + "Expected execution timeout, got: " + ex.getMessage()); + + // Verify the executor thread is free after timeout + var probe = es.submit(() -> "ok"); + assertEquals("ok", probe.get(5, java.util.concurrent.TimeUnit.SECONDS)); + + runner.close(); + } + @Test public void handleExceptionsThrownInJava() { var builtins =