From f1e06c3c050b3440555d63b6de80fe9f60b35dfa Mon Sep 17 00:00:00 2001 From: Jan Lahoda Date: Mon, 5 May 2025 09:00:59 +0000 Subject: [PATCH] 8299934: LocalExecutionControl replaces default uncaught exception handler Reviewed-by: liach --- .../execution/LocalExecutionControl.java | 29 ++-- .../LocalExecutionControlExceptionTest.java | 154 ++++++++++++++++++ 2 files changed, 169 insertions(+), 14 deletions(-) create mode 100644 test/langtools/jdk/jshell/LocalExecutionControlExceptionTest.java diff --git a/src/jdk.jshell/share/classes/jdk/jshell/execution/LocalExecutionControl.java b/src/jdk.jshell/share/classes/jdk/jshell/execution/LocalExecutionControl.java index 5f547b25178..d3aa891b8c4 100644 --- a/src/jdk.jshell/share/classes/jdk/jshell/execution/LocalExecutionControl.java +++ b/src/jdk.jshell/share/classes/jdk/jshell/execution/LocalExecutionControl.java @@ -132,28 +132,29 @@ public class LocalExecutionControl extends DirectExecutionControl { } allStop.set(null, false); - execThreadGroup = new ThreadGroup("JShell process local execution"); - AtomicReference iteEx = new AtomicReference<>(); AtomicReference iaeEx = new AtomicReference<>(); AtomicReference nmeEx = new AtomicReference<>(); AtomicReference stopped = new AtomicReference<>(false); - Thread.setDefaultUncaughtExceptionHandler((t, e) -> { - if (e instanceof InvocationTargetException) { - if (e.getCause() instanceof ThreadDeath) { + execThreadGroup = new ThreadGroup("JShell process local execution") { + @Override + public void uncaughtException(Thread t, Throwable e) { + if (e instanceof InvocationTargetException) { + if (e.getCause() instanceof ThreadDeath) { + stopped.set(true); + } else { + iteEx.set((InvocationTargetException) e); + } + } else if (e instanceof IllegalAccessException) { + iaeEx.set((IllegalAccessException) e); + } else if (e instanceof NoSuchMethodException) { + nmeEx.set((NoSuchMethodException) e); + } else if (e instanceof ThreadDeath) { stopped.set(true); - } else { - iteEx.set((InvocationTargetException) e); } - } else if (e instanceof IllegalAccessException) { - iaeEx.set((IllegalAccessException) e); - } else if (e instanceof NoSuchMethodException) { - nmeEx.set((NoSuchMethodException) e); - } else if (e instanceof ThreadDeath) { - stopped.set(true); } - }); + }; final Object[] res = new Object[1]; Thread snippetThread = new Thread(execThreadGroup, () -> { diff --git a/test/langtools/jdk/jshell/LocalExecutionControlExceptionTest.java b/test/langtools/jdk/jshell/LocalExecutionControlExceptionTest.java new file mode 100644 index 00000000000..afdf7d12e82 --- /dev/null +++ b/test/langtools/jdk/jshell/LocalExecutionControlExceptionTest.java @@ -0,0 +1,154 @@ +/* + * Copyright (c) 2025, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +/* + * @test + * @bug 8299934 + * @summary Test LocalExecutionControl + * @run junit LocalExecutionControlExceptionTest + */ + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.PrintStream; +import java.lang.reflect.Method; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import jdk.jshell.EvalException; +import jdk.jshell.JShell; +import jdk.jshell.execution.LocalExecutionControlProvider; +import org.junit.jupiter.api.*; +import org.junit.jupiter.api.extension.*; + +public class LocalExecutionControlExceptionTest { + + @Test + @ExtendWith(NoUncaughtExceptionHandleInterceptor.class) + public void testUncaughtExceptions() throws InterruptedException { + List excs = new ArrayList<>(); + ByteArrayOutputStream out = new ByteArrayOutputStream(); + Thread.setDefaultUncaughtExceptionHandler((t, ex) -> excs.add(ex)); + try (var jshell = JShell.builder() + .err(new PrintStream(out)) + .out(new PrintStream(out)) + .executionEngine(new LocalExecutionControlProvider(), Map.of()) + .build()) { + { + var events = jshell.eval("throw new java.io.IOException();"); + Assertions.assertEquals(1, events.size()); + Assertions.assertNotNull(events.get(0).exception()); + Assertions.assertTrue(events.get(0).exception() instanceof EvalException); + Assertions.assertEquals("java.io.IOException", + ((EvalException) events.get(0).exception()).getExceptionClassName()); + Assertions.assertEquals(0, excs.size()); + Assertions.assertEquals(0, out.size()); + } + { + var events = jshell.eval("throw new IllegalAccessException();"); + Assertions.assertEquals(1, events.size()); + Assertions.assertNotNull(events.get(0).exception()); + Assertions.assertTrue(events.get(0).exception() instanceof EvalException); + Assertions.assertEquals("java.lang.IllegalAccessException", + ((EvalException) events.get(0).exception()).getExceptionClassName()); + Assertions.assertEquals(0, excs.size()); + Assertions.assertEquals(0, out.size()); + } + jshell.eval(""" + T t2(Throwable t) throws T { + throw (T) t; + } + """); + jshell.eval(""" + void t(Throwable t) { + throw t2(t); + } + """); + { + var events = jshell.eval(""" + { + var t = new Thread(() -> t(new java.io.IOException())); + t.start(); + t.join(); + } + """); + Assertions.assertEquals(1, events.size()); + Assertions.assertNull(events.get(0).exception()); + Assertions.assertEquals(0, excs.size()); + Assertions.assertEquals(0, out.size()); + } + { + var events = jshell.eval(""" + { + var t = new Thread(() -> t(new IllegalAccessException())); + t.start(); + t.join(); + } + """); + Assertions.assertEquals(1, events.size()); + Assertions.assertNull(events.get(0).exception()); + Assertions.assertEquals(0, excs.size()); + Assertions.assertEquals(0, out.size()); + } + Thread outsideOfJShell = new Thread(() -> { + t(new IOException()); + }); + outsideOfJShell.start(); + outsideOfJShell.join(); + Assertions.assertEquals(1, excs.size()); + } + } + + void t(Throwable t) { + throw t2(t); + } + + T t2(Throwable t) throws T { + throw (T) t; + } + + public static final class NoUncaughtExceptionHandleInterceptor implements InvocationInterceptor { + public void interceptTestMethod(Invocation invocation, + ReflectiveInvocationContext invocationContext, + ExtensionContext extensionContext) throws Throwable { + Throwable[] exc = new Throwable[1]; + //the tests normally run in a ThreadGroup which handles uncaught exception + //run in a ThreadGroup which does not handle the uncaught exceptions, and let them + //pass to the default uncaught handler for the test: + var thread = new Thread(Thread.currentThread().getThreadGroup().getParent(), "test-group") { + public void run() { + try { + invocation.proceed(); + } catch (Throwable ex) { + exc[0] = ex; + } + } + }; + thread.start(); + thread.join(); + if (exc[0] != null) { + throw exc[0]; + } + } + } +}