From 51387323f39b62e875e8f9a7e0995c5fc9cee116 Mon Sep 17 00:00:00 2001 From: Andrew Konchin Date: Thu, 25 Apr 2024 19:41:26 +0300 Subject: [PATCH] Set $! global variable when an at_exit hook raises an exception --- CHANGELOG.md | 1 + spec/ruby/shared/kernel/at_exit.rb | 6 ++++++ spec/tags/core/kernel/at_exit_tags.txt | 1 + spec/tags/language/END_tags.txt | 1 + src/main/java/org/truffleruby/RubyContext.java | 3 ++- .../truffleruby/core/kernel/AtExitManager.java | 15 ++++++++++++--- 6 files changed, 23 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 26b096cd0acb..56f03ad936a9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,6 +19,7 @@ Compatibility: * Fix evaluation order for multi-assignment and evaluate left-hand-side before right-hand-side (@andrykonchin). * Add `Regexp.linear_time?` method (#3039, @andrykonchin). * Allow null encoding pointer in `rb_enc_interned_str_cstr` (@thomasmarshall). +* Set `$!` when a `Kernel#at_exit` hook raises an exception (#3535, @andrykonchin). Performance: * Fix inline caching for Regexp creation from Strings (#3492, @andrykonchin, @eregon). diff --git a/spec/ruby/shared/kernel/at_exit.rb b/spec/ruby/shared/kernel/at_exit.rb index 16d41cb01c93..29db79bb3914 100644 --- a/spec/ruby/shared/kernel/at_exit.rb +++ b/spec/ruby/shared/kernel/at_exit.rb @@ -30,6 +30,12 @@ result.lines.should.include?("The exception matches: true (message=foo)\n") end + it "gives access to an exception raised in a previous handler" do + code = "#{@method} { print '$!.message = ' + $!.message }; #{@method} { raise 'foo' }" + result = ruby_exe(code, args: "2>&1", exit_status: 1) + result.lines.should.include?("$!.message = foo") + end + it "both exceptions in a handler and in the main script are printed" do code = "#{@method} { raise 'at_exit_error' }; raise 'main_script_error'" result = ruby_exe(code, args: "2>&1", exit_status: 1) diff --git a/spec/tags/core/kernel/at_exit_tags.txt b/spec/tags/core/kernel/at_exit_tags.txt index 05157d6223c9..55b303969812 100644 --- a/spec/tags/core/kernel/at_exit_tags.txt +++ b/spec/tags/core/kernel/at_exit_tags.txt @@ -7,3 +7,4 @@ slow:Kernel.at_exit decides the exit status if both at_exit and the main script slow:Kernel.at_exit runs all handlers even if some raise exceptions slow:Kernel.at_exit runs handlers even if the main script fails to parse slow:Kernel.at_exit calls the nested handler right after the outer one if a handler is nested into another handler +slow:Kernel.at_exit gives access to an exception raised in a previous handler diff --git a/spec/tags/language/END_tags.txt b/spec/tags/language/END_tags.txt index 0cec2bc81b77..7218ff04a5fd 100644 --- a/spec/tags/language/END_tags.txt +++ b/spec/tags/language/END_tags.txt @@ -11,3 +11,4 @@ slow:The END keyword runs only once for multiple calls slow:The END keyword warns when END is used in a method slow:The END keyword END blocks and at_exit callbacks are mixed runs them all in reverse order of registration slow:The END keyword is affected by the toplevel assignment +slow:The END keyword gives access to an exception raised in a previous handler diff --git a/src/main/java/org/truffleruby/RubyContext.java b/src/main/java/org/truffleruby/RubyContext.java index caa739fef09d..6e9a42cc1e36 100644 --- a/src/main/java/org/truffleruby/RubyContext.java +++ b/src/main/java/org/truffleruby/RubyContext.java @@ -118,7 +118,7 @@ public final class RubyContext { private final MarkingService markingService; private final ObjectSpaceManager objectSpaceManager = new ObjectSpaceManager(); private final SharedObjects sharedObjects = new SharedObjects(this); - private final AtExitManager atExitManager = new AtExitManager(this); + private final AtExitManager atExitManager; private final CallStackManager callStack; private final CoreExceptions coreExceptions; private final EncodingManager encodingManager; @@ -202,6 +202,7 @@ public RubyContext(RubyLanguage language, TruffleLanguage.Env env) { finalizationService = new FinalizationService(referenceProcessor); markingService = new MarkingService(); dataObjectFinalizationService = new DataObjectFinalizationService(language, referenceProcessor); + atExitManager = new AtExitManager(this, language); // We need to construct this at runtime random = createRandomInstance(); diff --git a/src/main/java/org/truffleruby/core/kernel/AtExitManager.java b/src/main/java/org/truffleruby/core/kernel/AtExitManager.java index af59f8ef1d1a..d54158a1b414 100644 --- a/src/main/java/org/truffleruby/core/kernel/AtExitManager.java +++ b/src/main/java/org/truffleruby/core/kernel/AtExitManager.java @@ -16,6 +16,8 @@ import com.oracle.truffle.api.exception.AbstractTruffleException; import org.truffleruby.RubyContext; +import org.truffleruby.RubyLanguage; +import org.truffleruby.core.exception.ExceptionOperations; import org.truffleruby.core.exception.RubyException; import org.truffleruby.core.exception.RubySystemExit; import org.truffleruby.core.proc.ProcOperations; @@ -31,12 +33,14 @@ public final class AtExitManager { private final RubyContext context; + private final RubyLanguage language; private final Deque atExitHooks = new ConcurrentLinkedDeque<>(); private final Deque systemExitHooks = new ConcurrentLinkedDeque<>(); - public AtExitManager(RubyContext context) { + public AtExitManager(RubyContext context, RubyLanguage language) { this.context = context; + this.language = language; } public void add(RubyProc block, boolean always) { @@ -74,7 +78,7 @@ private AbstractTruffleException runExitHooks(Deque stack) { try { ProcOperations.rootCall(block, NoKeywordArgumentsDescriptor.INSTANCE, RubyBaseNode.EMPTY_ARGUMENTS); } catch (AbstractTruffleException e) { - handleAtExitException(context, e); + handleAtExitException(context, language, e); lastException = e; } } @@ -99,7 +103,12 @@ public static boolean isSilentException(RubyContext context, AbstractTruffleExce rubyException.getLogicalClass() == context.getCoreLibrary().signalExceptionClass; } - private static void handleAtExitException(RubyContext context, AbstractTruffleException exception) { + private static void handleAtExitException(RubyContext context, RubyLanguage language, + AbstractTruffleException exception) { + // Set $! for the next at_exit handlers + language.getCurrentThread().threadLocalGlobals.setLastException(ExceptionOperations + .getExceptionObject(exception)); + if (!isSilentException(context, exception)) { context.getDefaultBacktraceFormatter().printRubyExceptionOnEnvStderr("", exception); }