From 451d7cb4521cee239b3011640b50f4afa6740c19 Mon Sep 17 00:00:00 2001 From: Pavel Marek Date: Thu, 18 Jul 2024 20:10:36 +0200 Subject: [PATCH] System.exit does proper context hard exit. (#10363) The `System.exit 42` component is treated the same way as any other Panic error - it does not interfere with other component evaluation: ![image](https://github.com/user-attachments/assets/516490b5-755f-453e-8dc9-744437dc51bd) After removing the `System.exit 42` component, the workflow works as expected. I have also tried opening the project with the component and then removing it. --- .../src/main/java/org/enso/runner/Main.java | 8 ++- .../interpreter/service/ExecutionService.java | 14 ++++ .../job/ProgramExecutionSupport.scala | 15 +++- .../test/instrument/RuntimeServerTest.scala | 56 +++++++++++++++ .../runtime/system/ExitException.java | 70 +++++++++++++++++++ .../interpreter/runtime/system/System.java | 10 +-- .../Standard/Base/0.0.0-dev/src/System.enso | 2 + 7 files changed, 166 insertions(+), 9 deletions(-) create mode 100644 engine/runtime/src/main/java/org/enso/interpreter/runtime/system/ExitException.java diff --git a/engine/runner/src/main/java/org/enso/runner/Main.java b/engine/runner/src/main/java/org/enso/runner/Main.java index 47895e691c67..947cd5e49562 100644 --- a/engine/runner/src/main/java/org/enso/runner/Main.java +++ b/engine/runner/src/main/java/org/enso/runner/Main.java @@ -873,8 +873,12 @@ private void runMain( } } } catch (PolyglotException e) { - printPolyglotException(e, rootPkgPath); - throw exitFail(); + if (e.isExit()) { + doExit(e.getExitStatus()); + } else { + printPolyglotException(e, rootPkgPath); + throw exitFail(); + } } } diff --git a/engine/runtime-instrument-common/src/main/java/org/enso/interpreter/service/ExecutionService.java b/engine/runtime-instrument-common/src/main/java/org/enso/interpreter/service/ExecutionService.java index b144d550759c..fb810b90421f 100644 --- a/engine/runtime-instrument-common/src/main/java/org/enso/interpreter/service/ExecutionService.java +++ b/engine/runtime-instrument-common/src/main/java/org/enso/interpreter/service/ExecutionService.java @@ -3,10 +3,12 @@ import com.oracle.truffle.api.CallTarget; import com.oracle.truffle.api.CompilerDirectives; import com.oracle.truffle.api.TruffleLogger; +import com.oracle.truffle.api.exception.AbstractTruffleException; import com.oracle.truffle.api.frame.VirtualFrame; import com.oracle.truffle.api.instrumentation.EventBinding; import com.oracle.truffle.api.instrumentation.ExecutionEventNodeFactory; import com.oracle.truffle.api.interop.ArityException; +import com.oracle.truffle.api.interop.ExceptionType; import com.oracle.truffle.api.interop.InteropLibrary; import com.oracle.truffle.api.interop.UnknownIdentifierException; import com.oracle.truffle.api.interop.UnsupportedMessageException; @@ -479,6 +481,18 @@ public SourceSection getSourceLocation(Object o) { return null; } + public boolean isExitException(AbstractTruffleException ex) { + var interop = InteropLibrary.getUncached(); + if (interop.isException(ex)) { + try { + return interop.getExceptionType(ex) == ExceptionType.EXIT; + } catch (UnsupportedMessageException e) { + throw new IllegalStateException(e); + } + } + return false; + } + /** * Returns a human-readable message for a panic exception. * diff --git a/engine/runtime-instrument-common/src/main/scala/org/enso/interpreter/instrument/job/ProgramExecutionSupport.scala b/engine/runtime-instrument-common/src/main/scala/org/enso/interpreter/instrument/job/ProgramExecutionSupport.scala index ade064a18310..d555e336cbaa 100644 --- a/engine/runtime-instrument-common/src/main/scala/org/enso/interpreter/instrument/job/ProgramExecutionSupport.scala +++ b/engine/runtime-instrument-common/src/main/scala/org/enso/interpreter/instrument/job/ProgramExecutionSupport.scala @@ -49,7 +49,6 @@ import java.io.File import java.util.UUID import java.util.function.Consumer import java.util.logging.Level - import scala.jdk.OptionConverters.RichOptional import scala.util.Try @@ -324,9 +323,11 @@ object ProgramExecutionSupport { ctx: RuntimeContext ): PartialFunction[Throwable, Api.ExecutionResult.Diagnostic] = { case ex: AbstractTruffleException + // exit exception is special, and handled as failure rather than Diagnostics. + if !ctx.executionService.isExitException(ex) && // The empty language is allowed because `getLanguage` returns null when // the error originates in builtin node. - if Option(ctx.executionService.getLanguage(ex)) + Option(ctx.executionService.getLanguage(ex)) .forall(_ == LanguageInfo.ID) => val section = Option(ctx.executionService.getSourceLocation(ex)) val source = section.flatMap(sec => Option(sec.getSource)) @@ -357,6 +358,16 @@ object ProgramExecutionSupport { findFileByModuleName(ex.getModule) ) + case exitEx: AbstractTruffleException + if ctx.executionService.isExitException(exitEx) => + val section = Option(ctx.executionService.getSourceLocation(exitEx)) + val source = section.flatMap(sec => Option(sec.getSource)) + val file = source.flatMap(src => findFileByModuleName(src.getName)) + Api.ExecutionResult.Failure( + exitEx.getMessage, + file + ) + case ex: ServiceException => Api.ExecutionResult.Failure(ex.getMessage, None) } diff --git a/engine/runtime-integration-tests/src/test/scala/org/enso/interpreter/test/instrument/RuntimeServerTest.scala b/engine/runtime-integration-tests/src/test/scala/org/enso/interpreter/test/instrument/RuntimeServerTest.scala index 3963f077e63e..d1ad515f5d5c 100644 --- a/engine/runtime-integration-tests/src/test/scala/org/enso/interpreter/test/instrument/RuntimeServerTest.scala +++ b/engine/runtime-integration-tests/src/test/scala/org/enso/interpreter/test/instrument/RuntimeServerTest.scala @@ -5773,6 +5773,62 @@ class RuntimeServerTest ) } + it should "return error when invoking System.exit" in { + val contextId = UUID.randomUUID() + val requestId = UUID.randomUUID() + val moduleName = "Enso_Test.Test.Main" + val metadata = new Metadata + val code = + """import Standard.Base.System + | + |main = + | System.exit 42 + |""".stripMargin.linesIterator.mkString("\n") + val contents = metadata.appendToCode(code) + val mainFile = context.writeMain(contents) + + // create context + context.send(Api.Request(requestId, Api.CreateContextRequest(contextId))) + context.receive shouldEqual Some( + Api.Response(requestId, Api.CreateContextResponse(contextId)) + ) + + // Set sources for the module + context.send( + Api.Request(requestId, Api.OpenFileRequest(mainFile, contents)) + ) + context.receive shouldEqual Some( + Api.Response(Some(requestId), Api.OpenFileResponse) + ) + + // push main + context.send( + Api.Request( + requestId, + Api.PushContextRequest( + contextId, + Api.StackItem.ExplicitCall( + Api.MethodPointer(moduleName, moduleName, "main"), + None, + Vector() + ) + ) + ) + ) + context.receiveNIgnoreStdLib(2) should contain theSameElementsAs Seq( + Api.Response(requestId, Api.PushContextResponse(contextId)), + Api.Response( + Api.ExecutionFailed( + contextId, + Api.ExecutionResult.Failure( + "Exit was called with exit code 42.", + Some(mainFile) + ) + ) + ) + ) + } + it should "return compiler warning unused variable" in { val contextId = UUID.randomUUID() val requestId = UUID.randomUUID() diff --git a/engine/runtime/src/main/java/org/enso/interpreter/runtime/system/ExitException.java b/engine/runtime/src/main/java/org/enso/interpreter/runtime/system/ExitException.java new file mode 100644 index 000000000000..40cc6272a483 --- /dev/null +++ b/engine/runtime/src/main/java/org/enso/interpreter/runtime/system/ExitException.java @@ -0,0 +1,70 @@ +package org.enso.interpreter.runtime.system; + +import com.oracle.truffle.api.CompilerDirectives.TruffleBoundary; +import com.oracle.truffle.api.TruffleStackTraceElement; +import com.oracle.truffle.api.exception.AbstractTruffleException; +import com.oracle.truffle.api.interop.ExceptionType; +import com.oracle.truffle.api.interop.InteropLibrary; +import com.oracle.truffle.api.library.ExportLibrary; +import com.oracle.truffle.api.library.ExportMessage; +import com.oracle.truffle.api.nodes.Node; +import org.enso.interpreter.runtime.data.vector.ArrayLikeHelpers; + +/** Thrown when {@code System.exit} call was called during execution. */ +@ExportLibrary(InteropLibrary.class) +final class ExitException extends AbstractTruffleException { + private final int exitCode; + + ExitException(int code, Node location) { + super(location); + this.exitCode = code; + } + + @ExportMessage + boolean isException() { + return true; + } + + @ExportMessage + int getExceptionExitStatus() { + return exitCode; + } + + @ExportMessage + ExceptionType getExceptionType() { + return ExceptionType.EXIT; + } + + @ExportMessage + boolean hasExceptionMessage() { + return true; + } + + @ExportMessage + String getExceptionMessage() { + return getMessage(); + } + + @TruffleBoundary + @Override + public String getMessage() { + return "Exit was called with exit code " + exitCode + "."; + } + + @ExportMessage + RuntimeException throwException() { + return this; + } + + @ExportMessage + boolean hasExceptionStackTrace() { + return true; + } + + @ExportMessage + Object getExceptionStackTrace() { + var node = this.getLocation(); + var frame = TruffleStackTraceElement.create(node, node.getRootNode().getCallTarget(), null); + return ArrayLikeHelpers.asVectorWithCheckAt(frame.getGuestObject()); + } +} diff --git a/engine/runtime/src/main/java/org/enso/interpreter/runtime/system/System.java b/engine/runtime/src/main/java/org/enso/interpreter/runtime/system/System.java index 0bf245100f0b..a3317c98ed53 100644 --- a/engine/runtime/src/main/java/org/enso/interpreter/runtime/system/System.java +++ b/engine/runtime/src/main/java/org/enso/interpreter/runtime/system/System.java @@ -10,7 +10,6 @@ import java.io.InputStream; import java.io.OutputStream; import org.apache.commons.lang3.SystemUtils; -import org.enso.interpreter.EnsoLanguage; import org.enso.interpreter.dsl.Builtin; import org.enso.interpreter.node.expression.builtin.text.util.ExpectStringNode; import org.enso.interpreter.runtime.EnsoContext; @@ -46,14 +45,15 @@ public static long nanoTime() { return java.lang.System.nanoTime(); } + @Builtin.Specialize @Builtin.Method( description = "Exits the process, returning the provided code.", autoRegister = false) @CompilerDirectives.TruffleBoundary - public static void exit(long code) { - var ctx = EnsoContext.get(null); - EnsoLanguage.get(null).disposeContext(ctx); - java.lang.System.exit((int) code); + public static void exit(long code, @Cached ExpectStringNode expectStringNode) { + // expectStringNode is an artificial Node just to provide a location for + // the exception + throw new ExitException((int) code, expectStringNode); } @Builtin.Specialize diff --git a/test/micro-distribution/lib/Standard/Base/0.0.0-dev/src/System.enso b/test/micro-distribution/lib/Standard/Base/0.0.0-dev/src/System.enso index e8b6f14557a7..289e819b48b1 100644 --- a/test/micro-distribution/lib/Standard/Base/0.0.0-dev/src/System.enso +++ b/test/micro-distribution/lib/Standard/Base/0.0.0-dev/src/System.enso @@ -3,3 +3,5 @@ create_process command arguments input redirect_in redirect_out redirect_err = @ @Builtin_Type type System_Process_Result Result exit_code stdout stderr + +exit code = @Builtin_Method "System.exit"