diff --git a/docs/guides/java-integration.md b/docs/guides/java-integration.md index 0ef22b205..5e589b2d3 100644 --- a/docs/guides/java-integration.md +++ b/docs/guides/java-integration.md @@ -77,6 +77,52 @@ try { } ``` +### Handling Script Exit + +When a Perl script calls `exit()`, PerlOnJava throws a `PerlExitException` instead of +terminating the JVM. This allows your Java application to handle script completion +gracefully and continue execution. + +Note: Like in standard Perl, `exit()` is not caught by Perl's `eval{}` blocks - it +always propagates to the Java caller. + +```java +import org.perlonjava.runtime.runtimetypes.PerlExitException; + +ScriptEngine engine = manager.getEngineByName("perl"); + +try { + engine.eval("print 'Processing...'; exit 0;"); +} catch (ScriptException e) { + if (e.getCause() instanceof PerlExitException exitEx) { + System.out.println("Script exited with code: " + exitEx.getExitCode()); + // Continue with other work... + } else { + throw e; + } +} +``` + +This is particularly important when running scripts like ExifTool that call `exit()` +after completing their work: + +```java +import org.perlonjava.app.cli.ArgumentParser; +import org.perlonjava.app.cli.CompilerOptions; +import org.perlonjava.app.scriptengine.PerlLanguageProvider; +import org.perlonjava.runtime.runtimetypes.PerlExitException; + +String[] scriptArgs = new String[]{"path/to/exiftool", "-ver"}; +CompilerOptions options = ArgumentParser.parseArguments(scriptArgs); + +try { + PerlLanguageProvider.executePerlCode(options, true); +} catch (PerlExitException e) { + System.out.println("ExifTool exited with code: " + e.getExitCode()); + // Script completed successfully, continue processing... +} +``` + ## Using PerlOnJava Directly For more control, you can use PerlOnJava's internal API directly. diff --git a/src/main/java/org/perlonjava/app/cli/Main.java b/src/main/java/org/perlonjava/app/cli/Main.java index 79da90829..9600e299f 100644 --- a/src/main/java/org/perlonjava/app/cli/Main.java +++ b/src/main/java/org/perlonjava/app/cli/Main.java @@ -3,6 +3,7 @@ import org.perlonjava.app.scriptengine.PerlLanguageProvider; import org.perlonjava.runtime.runtimetypes.ErrorMessageUtil; import org.perlonjava.runtime.runtimetypes.GlobalVariable; +import org.perlonjava.runtime.runtimetypes.PerlExitException; import org.perlonjava.runtime.runtimetypes.RuntimeScalar; import java.util.Locale; @@ -49,6 +50,9 @@ public static void main(String[] args) { if (rawChildStatus > 0 && rawChildStatus <= 255) { System.exit(rawChildStatus); } + } catch (PerlExitException e) { + // Perl's exit() throws PerlExitException - convert to real System.exit() for CLI + System.exit(e.getExitCode()); } catch (Throwable t) { if (parsedArgs.debugEnabled) { // Print full JVM stack diff --git a/src/main/java/org/perlonjava/app/scriptengine/PerlLanguageProvider.java b/src/main/java/org/perlonjava/app/scriptengine/PerlLanguageProvider.java index 37dc7d400..bd7119461 100644 --- a/src/main/java/org/perlonjava/app/scriptengine/PerlLanguageProvider.java +++ b/src/main/java/org/perlonjava/app/scriptengine/PerlLanguageProvider.java @@ -286,6 +286,10 @@ private static RuntimeList executeCode(RuntimeCode runtimeCode, EmitterContext c System.out.println(errorMessage); System.out.println("END failed--call queue aborted."); } + } catch (PerlExitException e) { + // PerlExitException already ran END blocks and closed handles in WarnDie.exit() + // Just re-throw for the caller to handle + throw e; } catch (Throwable t) { if (isMainProgram) { runEndBlocks(); diff --git a/src/main/java/org/perlonjava/backend/bytecode/BytecodeInterpreter.java b/src/main/java/org/perlonjava/backend/bytecode/BytecodeInterpreter.java index 1ebfc94a2..ab0f7e853 100644 --- a/src/main/java/org/perlonjava/backend/bytecode/BytecodeInterpreter.java +++ b/src/main/java/org/perlonjava/backend/bytecode/BytecodeInterpreter.java @@ -1590,6 +1590,9 @@ public static RuntimeList execute(InterpretedCode code, RuntimeArray args, int c String javaLine = (st.length > 0) ? " [java:" + st[0].getFileName() + ":" + st[0].getLineNumber() + "]" : ""; String errorMessage = "ClassCastException" + bcContext + ": " + e.getMessage() + javaLine; throw new RuntimeException(formatInterpreterError(code, errorPc, new Exception(errorMessage)), e); + } catch (PerlExitException e) { + // exit() should NEVER be caught by eval{} - always propagate + throw e; } catch (Throwable e) { // Check if we're inside an eval block if (!evalCatchStack.isEmpty()) { @@ -1604,9 +1607,9 @@ public static RuntimeList execute(InterpretedCode code, RuntimeArray args, int c } // Not in eval block - propagate exception - // If it's already a PerlDieException, re-throw as-is for proper formatting - if (e instanceof PerlDieException) { - throw (PerlDieException) e; + // Re-throw RuntimeExceptions as-is (includes PerlDieException) + if (e instanceof RuntimeException re) { + throw re; } // Check if we're running inside an eval STRING context diff --git a/src/main/java/org/perlonjava/runtime/operators/WarnDie.java b/src/main/java/org/perlonjava/runtime/operators/WarnDie.java index 2f45ba7f5..026ff618c 100644 --- a/src/main/java/org/perlonjava/runtime/operators/WarnDie.java +++ b/src/main/java/org/perlonjava/runtime/operators/WarnDie.java @@ -34,10 +34,17 @@ private static Throwable unwrapException(Throwable throwable) { } /** - * Catches the exception in an eval-block + * Catches the exception in an eval-block. + * Note: PerlExitException should NEVER be caught by eval{} - it always propagates. */ public static RuntimeScalar catchEval(Throwable e) { e = unwrapException(e); + + // exit() should never be caught by eval{} - re-throw it + if (e instanceof PerlExitException) { + throw (PerlExitException) e; + } + RuntimeScalar err = getGlobalVariable("main::@"); if (e instanceof PerlDieException pde) { @@ -276,23 +283,28 @@ private static RuntimeBase dieEmptyMessage(RuntimeScalar oldErr, String fileName } /** - * Terminates the program + * Terminates the program by throwing PerlExitException. + *
+ * This allows embedded/library use where the calling Java application + * can catch the exception and continue execution. The CLI (Main.main()) + * catches this and converts it to a real System.exit() call. * * @param runtimeScalar with exit status - * @return nothing + * @return nothing (always throws) + * @throws PerlExitException always thrown with the exit code */ public static RuntimeScalar exit(RuntimeScalar runtimeScalar) { + int exitCode = runtimeScalar.getInt(); try { runEndBlocks(); - RuntimeIO.closeAllHandles(); } catch (Throwable t) { RuntimeIO.closeAllHandles(); String errorMessage = ErrorMessageUtil.stringifyException(t); System.err.println(errorMessage); - System.exit(1); + throw new PerlExitException(1); } - System.exit(runtimeScalar.getInt()); - return new RuntimeScalar(); // This line will never be reached + RuntimeIO.closeAllHandles(); + throw new PerlExitException(exitCode); } /** diff --git a/src/main/java/org/perlonjava/runtime/perlmodule/JavaSystem.java b/src/main/java/org/perlonjava/runtime/perlmodule/JavaSystem.java index dd47b5862..91b780388 100644 --- a/src/main/java/org/perlonjava/runtime/perlmodule/JavaSystem.java +++ b/src/main/java/org/perlonjava/runtime/perlmodule/JavaSystem.java @@ -167,13 +167,17 @@ public static RuntimeList gc(RuntimeArray args, int ctx) { } /** - * Exit the JVM + * Exit the JVM by throwing PerlExitException. + *
+ * This allows embedded/library use where the calling Java application + * can catch the exception and continue execution. * Usage: exit(0); + * + * @throws PerlExitException always thrown with the exit code */ public static RuntimeList exit(RuntimeArray args, int ctx) { int status = args.size() > 0 ? (int) args.get(0).getLong() : 0; - System.exit(status); - return scalarUndef.getList(); // Never reached + throw new PerlExitException(status); } /** diff --git a/src/main/java/org/perlonjava/runtime/runtimetypes/PerlExitException.java b/src/main/java/org/perlonjava/runtime/runtimetypes/PerlExitException.java new file mode 100644 index 000000000..0c7bd417e --- /dev/null +++ b/src/main/java/org/perlonjava/runtime/runtimetypes/PerlExitException.java @@ -0,0 +1,30 @@ +package org.perlonjava.runtime.runtimetypes; + +import java.io.Serial; + +/** + * Exception used to implement Perl's exit() semantics for embedded/library use. + *
+ * When Perl code calls exit(), this exception is thrown instead of calling + * System.exit(), allowing the calling Java application to handle the exit + * gracefully and continue execution. + *
+ * The CLI (Main.main()) catches this exception and converts it to a real
+ * System.exit() call, while library users can catch it and handle the exit
+ * code as needed.
+ */
+public class PerlExitException extends RuntimeException {
+ @Serial
+ private static final long serialVersionUID = 1L;
+
+ private final int exitCode;
+
+ public PerlExitException(int exitCode) {
+ super("exit " + exitCode);
+ this.exitCode = exitCode;
+ }
+
+ public int getExitCode() {
+ return exitCode;
+ }
+}
diff --git a/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeCode.java b/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeCode.java
index f20c8edbb..87c9d4ed4 100644
--- a/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeCode.java
+++ b/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeCode.java
@@ -1965,10 +1965,12 @@ public RuntimeList apply(RuntimeArray a, int callContext) {
}
} catch (InvocationTargetException e) {
Throwable targetException = e.getTargetException();
- if (!(targetException instanceof RuntimeException)) {
- throw new RuntimeException(targetException);
+ if (targetException instanceof RuntimeException re) {
+ throw re;
}
- throw (RuntimeException) targetException;
+ throw new RuntimeException(targetException);
+ } catch (RuntimeException e) {
+ throw e;
} catch (Throwable e) {
throw new RuntimeException(e);
}
@@ -2037,10 +2039,12 @@ public RuntimeList apply(String subroutineName, RuntimeArray a, int callContext)
}
} catch (InvocationTargetException e) {
Throwable targetException = e.getTargetException();
- if (!(targetException instanceof RuntimeException)) {
- throw new RuntimeException(targetException);
+ if (targetException instanceof RuntimeException re) {
+ throw re;
}
- throw (RuntimeException) targetException;
+ throw new RuntimeException(targetException);
+ } catch (RuntimeException e) {
+ throw e;
} catch (Throwable e) {
throw new RuntimeException(e);
}
diff --git a/src/test/java/org/perlonjava/PerlExitExceptionTest.java b/src/test/java/org/perlonjava/PerlExitExceptionTest.java
new file mode 100644
index 000000000..0dd294e6d
--- /dev/null
+++ b/src/test/java/org/perlonjava/PerlExitExceptionTest.java
@@ -0,0 +1,104 @@
+package org.perlonjava;
+
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Tag;
+import org.junit.jupiter.api.Test;
+import org.perlonjava.app.cli.CompilerOptions;
+import org.perlonjava.app.scriptengine.PerlLanguageProvider;
+import org.perlonjava.runtime.runtimetypes.PerlExitException;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+/**
+ * Test that PerlExitException is thrown when Perl code calls exit(),
+ * allowing library users to catch it and continue execution.
+ *
+ * This addresses GitHub issue #291 where executePerlCode() was
+ * terminating the JVM instead of returning.
+ */
+@Tag("unit")
+public class PerlExitExceptionTest {
+
+ @BeforeEach
+ void setUp() {
+ PerlLanguageProvider.resetAll();
+ }
+
+ @Test
+ void testExitZeroThrowsException() {
+ CompilerOptions options = new CompilerOptions();
+ options.fileName = "