diff --git a/src/java.base/share/classes/java/lang/Runtime.java b/src/java.base/share/classes/java/lang/Runtime.java index 25d2351a24e..2a5c37b2ae9 100644 --- a/src/java.base/share/classes/java/lang/Runtime.java +++ b/src/java.base/share/classes/java/lang/Runtime.java @@ -156,6 +156,11 @@ public class Runtime { *

The {@link System#exit(int) System.exit} method is the * conventional and convenient means of invoking this method. * + * @implNote + * If the {@linkplain System#getLogger(String) system logger} for {@code java.lang.Runtime} + * is enabled with logging level {@link System.Logger.Level#DEBUG Level.DEBUG} the stack trace + * of the call to {@code Runtime.exit()} is logged. + * * @param status * Termination status. By convention, a nonzero status code * indicates abnormal termination. diff --git a/src/java.base/share/classes/java/lang/Shutdown.java b/src/java.base/share/classes/java/lang/Shutdown.java index aa37689e505..4dc63a46714 100644 --- a/src/java.base/share/classes/java/lang/Shutdown.java +++ b/src/java.base/share/classes/java/lang/Shutdown.java @@ -157,16 +157,36 @@ class Shutdown { * which should pass a nonzero status code. */ static void exit(int status) { + System.Logger log = getRuntimeExitLogger(); // Locate the logger without holding the lock; synchronized (Shutdown.class) { /* Synchronize on the class object, causing any other thread * that attempts to initiate shutdown to stall indefinitely */ + if (log != null) { + Throwable throwable = new Throwable("Runtime.exit(" + status + ")"); + log.log(System.Logger.Level.DEBUG, "Runtime.exit() called with status: " + status, + throwable); + } beforeHalt(); runHooks(); halt(status); } } + /* Locate and return the logger for Shutdown.exit, if it is functional and DEBUG enabled. + * Exceptions should not prevent System.exit; the exception is printed and otherwise ignored. + */ + private static System.Logger getRuntimeExitLogger() { + try { + System.Logger log = System.getLogger("java.lang.Runtime"); + return (log.isLoggable(System.Logger.Level.DEBUG)) ? log : null; + } catch (Throwable throwable) { + // Exceptions from locating the Logger are printed but do not prevent exit + System.err.println("Runtime.exit() log finder failed with: " + throwable.getMessage()); + } + return null; + } + /* Invoked by the JNI DestroyJavaVM procedure when the last non-daemon * thread has finished. Unlike the exit method, this method does not diff --git a/src/java.base/share/classes/java/lang/System.java b/src/java.base/share/classes/java/lang/System.java index 51d688e0d4a..501ed47fcad 100644 --- a/src/java.base/share/classes/java/lang/System.java +++ b/src/java.base/share/classes/java/lang/System.java @@ -1902,6 +1902,9 @@ public final class System { * Runtime.getRuntime().exit(n) * * + * @implNote + * The initiation of the shutdown sequence is logged by {@link Runtime#exit(int)}. + * * @param status exit status. * @throws SecurityException * if a security manager exists and its {@code checkExit} method diff --git a/test/jdk/java/lang/RuntimeTests/ExitLogging-FINE.properties b/test/jdk/java/lang/RuntimeTests/ExitLogging-FINE.properties new file mode 100644 index 00000000000..21b19fa5ac8 --- /dev/null +++ b/test/jdk/java/lang/RuntimeTests/ExitLogging-FINE.properties @@ -0,0 +1,8 @@ +############################################################ +# Enable logging java.lang.Runtime to the console +############################################################ + +handlers= java.util.logging.ConsoleHandler + +java.util.logging.ConsoleHandler.level = ALL +java.lang.Runtime.level = FINE diff --git a/test/jdk/java/lang/RuntimeTests/ExitLogging-INFO.properties b/test/jdk/java/lang/RuntimeTests/ExitLogging-INFO.properties new file mode 100644 index 00000000000..212d9bd86d2 --- /dev/null +++ b/test/jdk/java/lang/RuntimeTests/ExitLogging-INFO.properties @@ -0,0 +1,8 @@ +############################################################ +# Enable logging java.lang.Runtime to the console +############################################################ + +handlers= java.util.logging.ConsoleHandler + +java.util.logging.ConsoleHandler.level = ALL +java.lang.Runtime.level = INFO diff --git a/test/jdk/java/lang/RuntimeTests/RuntimeExitLogTest.java b/test/jdk/java/lang/RuntimeTests/RuntimeExitLogTest.java new file mode 100644 index 00000000000..7e39ae5cec1 --- /dev/null +++ b/test/jdk/java/lang/RuntimeTests/RuntimeExitLogTest.java @@ -0,0 +1,116 @@ +/* + * Copyright (c) 2023, 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. + */ + + +import java.io.BufferedReader; +import java.io.IOException; +import java.nio.file.Path; +import java.util.List; +import java.util.Optional; +import java.util.stream.Stream; + + +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; +import org.junit.jupiter.params.ParameterizedTest; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.fail; + +/* + * @test + * @summary verify logging of call to System.exit or Runtime.exit. + * @run junit/othervm RuntimeExitLogTest + */ + +public class RuntimeExitLogTest { + + private static final String TEST_JDK = System.getProperty("test.jdk"); + private static final String TEST_SRC = System.getProperty("test.src"); + + /** + * Call System.exit() with the parameter (or zero if not supplied). + * @param args zero or 1 argument, an exit status + */ + public static void main(String[] args) throws InterruptedException { + int status = args.length > 0 ? Integer.parseInt(args[0]) : 0; + System.exit(status); + } + + /** + * Test various log level settings, and none. + * @return a stream of arguments for parameterized test + */ + private static Stream logParamProvider() { + return Stream.of( + // Logging enabled with level DEBUG + Arguments.of(List.of("-Djava.util.logging.config.file=" + + Path.of(TEST_SRC, "ExitLogging-FINE.properties").toString()), 1, true), + // Logging disabled due to level + Arguments.of(List.of("-Djava.util.logging.config.file=" + + Path.of(TEST_SRC, "ExitLogging-INFO.properties").toString()), 2, false), + // Console logger + Arguments.of(List.of("--limit-modules", "java.base", + "-Djdk.system.logger.level=DEBUG"), 3, true), + // Console logger + Arguments.of(List.of(), 4, false) + ); + } + + /** + * Check that the logger output of a launched process contains the expected message. + * @param logProps The name of the log.properties file to set on the command line + * @param status the expected exit status of the process + * @param shouldLog true if the log should contain the message expected from Runtime.exit(status) + */ + @ParameterizedTest + @MethodSource("logParamProvider") + public void checkLogger(List logProps, int status, boolean shouldLog) { + ProcessBuilder pb = new ProcessBuilder(); + pb.redirectErrorStream(true); + + List cmd = pb.command(); + cmd.add(Path.of(TEST_JDK,"bin", "java").toString()); + cmd.addAll(logProps); + cmd.add(this.getClass().getName()); + cmd.add(Integer.toString(status)); + + try { + Process process = pb.start(); + try (BufferedReader reader = process.inputReader()) { + List lines = reader.lines().toList(); + final String expected = "Runtime.exit() called with status: " + status; + Optional found = lines.stream().filter(s -> s.contains(expected)).findFirst(); + if (found.isPresent() != shouldLog) { + System.err.println("---- Process output begin"); + lines.forEach(l -> System.err.println(l)); + System.err.println("---- Process output end"); + fail("Unexpected log contents"); + } + } + int result = process.waitFor(); + assertEquals(status, result, "Exit status"); + } catch (IOException | InterruptedException ex) { + fail(ex); + } + } +}