From f21402d4c37d1326c37859bbb8dab3e44d8232c1 Mon Sep 17 00:00:00 2001 From: Phillip Webb Date: Fri, 6 Dec 2024 17:15:10 -0800 Subject: [PATCH] Consistently return non-zero exit codes for jarmode failures Update jar mode launchers to catch all exceptions and return a non-zero exit code. This refinement also allows us to consolidate the existing error reporting logic to a central locations. Modes that wish to report a simple error rather than a full stacktrace can throw the newly introduced `JarModeErrorException`. Fixes gh-43435 --- .../boot/jarmode/tools/ExtractCommand.java | 27 +++----------- .../boot/jarmode/tools/Layers.java | 13 ++----- .../boot/jarmode/tools/ListLayersCommand.java | 16 ++------- .../jarmode/tools/ExtractCommandTests.java | 25 +++++++------ .../tools/ExtractLayersCommandTests.java | 7 ++-- .../jarmode/tools/ListLayersCommandTests.java | 9 +++-- ...ommand-printErrorIfLayersAreNotEnabled.txt | 2 -- .../list-layers-output-layers-disabled.txt | 2 -- .../boot/loader/jarmode/JarMode.java | 5 +-- .../loader/jarmode/JarModeErrorException.java | 36 +++++++++++++++++++ .../boot/loader/jarmode/JarModeLauncher.java | 35 +++++++++++++++--- .../boot/loader/jarmode/TestJarMode.java | 8 ++++- .../loader/jarmode/LauncherJarModeTests.java | 20 +++++++++++ .../boot/loader/jarmode/JarMode.java | 5 +-- .../loader/jarmode/JarModeErrorException.java | 36 +++++++++++++++++++ .../boot/loader/launch/JarModeRunner.java | 36 ++++++++++++++++--- .../boot/loader/jarmode/TestJarMode.java | 8 ++++- .../boot/loader/launch/LauncherTests.java | 20 +++++++++++ 18 files changed, 230 insertions(+), 80 deletions(-) delete mode 100644 spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/test/resources/org/springframework/boot/jarmode/tools/ExtractCommand-printErrorIfLayersAreNotEnabled.txt delete mode 100644 spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/test/resources/org/springframework/boot/jarmode/tools/list-layers-output-layers-disabled.txt create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-loader-classic/src/main/java/org/springframework/boot/loader/jarmode/JarModeErrorException.java create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jarmode/JarModeErrorException.java diff --git a/spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/main/java/org/springframework/boot/jarmode/tools/ExtractCommand.java b/spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/main/java/org/springframework/boot/jarmode/tools/ExtractCommand.java index 200080138bd..c1faf1450a8 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/main/java/org/springframework/boot/jarmode/tools/ExtractCommand.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/main/java/org/springframework/boot/jarmode/tools/ExtractCommand.java @@ -43,7 +43,7 @@ import java.util.zip.ZipInputStream; import org.springframework.boot.jarmode.tools.JarStructure.Entry; import org.springframework.boot.jarmode.tools.JarStructure.Entry.Type; -import org.springframework.boot.jarmode.tools.Layers.LayersNotEnabledException; +import org.springframework.boot.loader.jarmode.JarModeErrorException; import org.springframework.util.Assert; import org.springframework.util.StreamUtils; import org.springframework.util.StringUtils; @@ -118,12 +118,6 @@ class ExtractCommand extends Command { catch (IOException ex) { throw new UncheckedIOException(ex); } - catch (LayersNotEnabledException ex) { - printError(out, "Layers are not enabled"); - } - catch (AbortException ex) { - printError(out, ex.getMessage()); - } } private static void checkDirectoryIsEmpty(Map options, File destination) { @@ -134,11 +128,11 @@ class ExtractCommand extends Command { return; } if (!destination.isDirectory()) { - throw new AbortException(destination.getAbsoluteFile() + " already exists and is not a directory"); + throw new JarModeErrorException(destination.getAbsoluteFile() + " already exists and is not a directory"); } File[] files = destination.listFiles(); if (files != null && files.length > 0) { - throw new AbortException(destination.getAbsoluteFile() + " already exists and is not empty"); + throw new JarModeErrorException(destination.getAbsoluteFile() + " already exists and is not empty"); } } @@ -147,18 +141,13 @@ class ExtractCommand extends Command { try (ZipInputStream stream = new ZipInputStream(new FileInputStream(file))) { ZipEntry entry = stream.getNextEntry(); if (entry == null) { - throw new AbortException( + throw new JarModeErrorException( "File '%s' is not compatible; ensure jar file is valid and launch script is not enabled" .formatted(file)); } } } - private void printError(PrintStream out, String message) { - out.println("Error: " + message); - out.println(); - } - private void extractLibraries(FileResolver fileResolver, JarStructure jarStructure, Map options) throws IOException { String librariesDirectory = getLibrariesDirectory(options); @@ -494,12 +483,4 @@ class ExtractCommand extends Command { } - private static final class AbortException extends RuntimeException { - - AbortException(String message) { - super(message); - } - - } - } diff --git a/spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/main/java/org/springframework/boot/jarmode/tools/Layers.java b/spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/main/java/org/springframework/boot/jarmode/tools/Layers.java index 05eb07ebdc9..89d2f67c479 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/main/java/org/springframework/boot/jarmode/tools/Layers.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/main/java/org/springframework/boot/jarmode/tools/Layers.java @@ -19,6 +19,8 @@ package org.springframework.boot.jarmode.tools; import java.util.Iterator; import java.util.zip.ZipEntry; +import org.springframework.boot.loader.jarmode.JarModeErrorException; + /** * Provides information about the jar layers. * @@ -62,22 +64,13 @@ interface Layers extends Iterable { * Return a {@link Layers} instance for the currently running application. * @param context the command context * @return a new layers instance - * @throws LayersNotEnabledException if layers are not enabled */ static Layers get(Context context) { IndexedLayers indexedLayers = IndexedLayers.get(context); if (indexedLayers == null) { - throw new LayersNotEnabledException(); + throw new JarModeErrorException("Layers are not enabled"); } return indexedLayers; } - final class LayersNotEnabledException extends RuntimeException { - - LayersNotEnabledException() { - super("Layers not enabled: Failed to load layer index file"); - } - - } - } diff --git a/spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/main/java/org/springframework/boot/jarmode/tools/ListLayersCommand.java b/spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/main/java/org/springframework/boot/jarmode/tools/ListLayersCommand.java index a2b122ec21e..8561cfb2ce3 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/main/java/org/springframework/boot/jarmode/tools/ListLayersCommand.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/main/java/org/springframework/boot/jarmode/tools/ListLayersCommand.java @@ -20,8 +20,6 @@ import java.io.PrintStream; import java.util.List; import java.util.Map; -import org.springframework.boot.jarmode.tools.Layers.LayersNotEnabledException; - /** * The {@code 'list-layers'} tools command. * @@ -38,22 +36,12 @@ class ListLayersCommand extends Command { @Override void run(PrintStream out, Map options, List parameters) { - try { - Layers layers = Layers.get(this.context); - printLayers(out, layers); - } - catch (LayersNotEnabledException ex) { - printError(out, "Layers are not enabled"); - } + Layers layers = Layers.get(this.context); + printLayers(out, layers); } void printLayers(PrintStream out, Layers layers) { layers.forEach(out::println); } - private void printError(PrintStream out, String message) { - out.println("Error: " + message); - out.println(); - } - } diff --git a/spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/test/java/org/springframework/boot/jarmode/tools/ExtractCommandTests.java b/spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/test/java/org/springframework/boot/jarmode/tools/ExtractCommandTests.java index 0212d749c1e..b462536fa14 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/test/java/org/springframework/boot/jarmode/tools/ExtractCommandTests.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/test/java/org/springframework/boot/jarmode/tools/ExtractCommandTests.java @@ -32,7 +32,10 @@ import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; +import org.springframework.boot.loader.jarmode.JarModeErrorException; + import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; import static org.assertj.core.api.Assertions.assertThatIllegalStateException; /** @@ -172,8 +175,8 @@ class ExtractCommandTests extends AbstractJarModeTests { try (FileWriter writer = new FileWriter(file)) { writer.write("text"); } - TestPrintStream out = run(file); - assertThat(out).contains("is not compatible; ensure jar file is valid and launch script is not enabled"); + assertThatExceptionOfType(JarModeErrorException.class).isThrownBy(() -> run(file)) + .withMessageContaining("is not compatible; ensure jar file is valid and launch script is not enabled"); } @Test @@ -181,8 +184,9 @@ class ExtractCommandTests extends AbstractJarModeTests { File destination = file("out"); Files.createDirectories(destination.toPath()); Files.createFile(new File(destination, "file.txt").toPath()); - TestPrintStream out = run(ExtractCommandTests.this.archive, "--destination", destination.getAbsolutePath()); - assertThat(out).contains("already exists and is not empty"); + assertThatExceptionOfType(JarModeErrorException.class) + .isThrownBy(() -> run(ExtractCommandTests.this.archive, "--destination", destination.getAbsolutePath())) + .withMessageContaining("already exists and is not empty"); } @Test @@ -266,10 +270,10 @@ class ExtractCommandTests extends AbstractJarModeTests { } @Test - void printErrorIfLayersAreNotEnabled() throws IOException { + void failsIfLayersAreNotEnabled() throws IOException { File archive = createArchive(); - TestPrintStream out = run(archive, "--layers"); - assertThat(out).hasSameContentAsResource("ExtractCommand-printErrorIfLayersAreNotEnabled.txt"); + assertThatExceptionOfType(JarModeErrorException.class).isThrownBy(() -> run(archive, "--layers")) + .withMessage("Layers are not enabled"); } } @@ -318,10 +322,11 @@ class ExtractCommandTests extends AbstractJarModeTests { } @Test - void printErrorIfLayersAreNotEnabled() throws IOException { + void failsIfLayersAreNotEnabled() throws IOException { File archive = createArchive(); - TestPrintStream out = run(archive, "--launcher", "--layers"); - assertThat(out).hasSameContentAsResource("ExtractCommand-printErrorIfLayersAreNotEnabled.txt"); + assertThatExceptionOfType(JarModeErrorException.class) + .isThrownBy(() -> run(archive, "--launcher", "--layers")) + .withMessage("Layers are not enabled"); } @Test diff --git a/spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/test/java/org/springframework/boot/jarmode/tools/ExtractLayersCommandTests.java b/spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/test/java/org/springframework/boot/jarmode/tools/ExtractLayersCommandTests.java index d3baba1630b..4d3df2e97ca 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/test/java/org/springframework/boot/jarmode/tools/ExtractLayersCommandTests.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/test/java/org/springframework/boot/jarmode/tools/ExtractLayersCommandTests.java @@ -42,10 +42,12 @@ import org.junit.jupiter.api.io.TempDir; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.boot.loader.jarmode.JarModeErrorException; import org.springframework.core.io.ClassPathResource; import org.springframework.util.FileCopyUtils; import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; import static org.assertj.core.api.Assertions.assertThatIllegalStateException; import static org.mockito.BDDMockito.given; @@ -146,8 +148,9 @@ class ExtractLayersCommandTests { } given(this.context.getArchiveFile()).willReturn(file); try (TestPrintStream out = new TestPrintStream(this)) { - this.command.run(out, Collections.emptyMap(), Collections.emptyList()); - assertThat(out).contains("is not compatible"); + assertThatExceptionOfType(JarModeErrorException.class) + .isThrownBy(() -> this.command.run(out, Collections.emptyMap(), Collections.emptyList())) + .withMessageContaining("is not compatible"); } } diff --git a/spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/test/java/org/springframework/boot/jarmode/tools/ListLayersCommandTests.java b/spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/test/java/org/springframework/boot/jarmode/tools/ListLayersCommandTests.java index 676a105617c..ed014b1f337 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/test/java/org/springframework/boot/jarmode/tools/ListLayersCommandTests.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/test/java/org/springframework/boot/jarmode/tools/ListLayersCommandTests.java @@ -22,7 +22,10 @@ import java.util.jar.Manifest; import org.junit.jupiter.api.Test; +import org.springframework.boot.loader.jarmode.JarModeErrorException; + import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; /** * Tests for {@link ListLayersCommand}. @@ -39,9 +42,9 @@ class ListLayersCommandTests extends AbstractJarModeTests { } @Test - void shouldPrintErrorWhenLayersAreNotEnabled() throws IOException { - TestPrintStream out = run(createArchive()); - assertThat(out).hasSameContentAsResource("list-layers-output-layers-disabled.txt"); + void shouldFailWhenLayersAreNotEnabled() { + assertThatExceptionOfType(JarModeErrorException.class).isThrownBy(() -> run(createArchive())) + .withMessage("Layers are not enabled"); } private TestPrintStream run(File archive) { diff --git a/spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/test/resources/org/springframework/boot/jarmode/tools/ExtractCommand-printErrorIfLayersAreNotEnabled.txt b/spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/test/resources/org/springframework/boot/jarmode/tools/ExtractCommand-printErrorIfLayersAreNotEnabled.txt deleted file mode 100644 index 994354551a6..00000000000 --- a/spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/test/resources/org/springframework/boot/jarmode/tools/ExtractCommand-printErrorIfLayersAreNotEnabled.txt +++ /dev/null @@ -1,2 +0,0 @@ -Error: Layers are not enabled - diff --git a/spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/test/resources/org/springframework/boot/jarmode/tools/list-layers-output-layers-disabled.txt b/spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/test/resources/org/springframework/boot/jarmode/tools/list-layers-output-layers-disabled.txt deleted file mode 100644 index 994354551a6..00000000000 --- a/spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/test/resources/org/springframework/boot/jarmode/tools/list-layers-output-layers-disabled.txt +++ /dev/null @@ -1,2 +0,0 @@ -Error: Layers are not enabled - diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader-classic/src/main/java/org/springframework/boot/loader/jarmode/JarMode.java b/spring-boot-project/spring-boot-tools/spring-boot-loader-classic/src/main/java/org/springframework/boot/loader/jarmode/JarMode.java index 162e4a6a739..b91fdf1f546 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-loader-classic/src/main/java/org/springframework/boot/loader/jarmode/JarMode.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader-classic/src/main/java/org/springframework/boot/loader/jarmode/JarMode.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2023 the original author or authors. + * Copyright 2012-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -36,7 +36,8 @@ public interface JarMode { * Run the jar in the given mode. * @param mode the mode to use * @param args any program arguments + * @throws JarModeErrorException on an error that should print a simple error message */ - void run(String mode, String[] args); + void run(String mode, String[] args) throws JarModeErrorException; } diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader-classic/src/main/java/org/springframework/boot/loader/jarmode/JarModeErrorException.java b/spring-boot-project/spring-boot-tools/spring-boot-loader-classic/src/main/java/org/springframework/boot/loader/jarmode/JarModeErrorException.java new file mode 100644 index 00000000000..92f1720bdf2 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader-classic/src/main/java/org/springframework/boot/loader/jarmode/JarModeErrorException.java @@ -0,0 +1,36 @@ +/* + * Copyright 2012-2024 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.loader.jarmode; + +/** + * Simple {@link RuntimeException} used to fail the jar mode with a simple printed error + * message. + * + * @author Phillip Webb + * @since 3.3.7 + */ +public class JarModeErrorException extends RuntimeException { + + public JarModeErrorException(String message) { + super(message); + } + + public JarModeErrorException(String message, Throwable cause) { + super(message, cause); + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader-classic/src/main/java/org/springframework/boot/loader/jarmode/JarModeLauncher.java b/spring-boot-project/spring-boot-tools/spring-boot-loader-classic/src/main/java/org/springframework/boot/loader/jarmode/JarModeLauncher.java index 600266a241b..1a774e513f5 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-loader-classic/src/main/java/org/springframework/boot/loader/jarmode/JarModeLauncher.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader-classic/src/main/java/org/springframework/boot/loader/jarmode/JarModeLauncher.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2023 the original author or authors. + * Copyright 2012-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -31,11 +31,31 @@ public final class JarModeLauncher { static final String DISABLE_SYSTEM_EXIT = JarModeLauncher.class.getName() + ".DISABLE_SYSTEM_EXIT"; + static final String SUPPRESSED_SYSTEM_EXIT_CODE = JarModeLauncher.class.getName() + ".SUPPRESSED_SYSTEM_EXIT_CODE"; + private JarModeLauncher() { } public static void main(String[] args) { String mode = System.getProperty("jarmode"); + boolean disableSystemExit = Boolean.getBoolean(DISABLE_SYSTEM_EXIT); + try { + runJarMode(mode, args); + if (disableSystemExit) { + System.setProperty(SUPPRESSED_SYSTEM_EXIT_CODE, "0"); + } + } + catch (Throwable ex) { + printError(ex); + if (disableSystemExit) { + System.setProperty(SUPPRESSED_SYSTEM_EXIT_CODE, "1"); + return; + } + System.exit(1); + } + } + + private static void runJarMode(String mode, String[] args) { List candidates = SpringFactoriesLoader.loadFactories(JarMode.class, ClassUtils.getDefaultClassLoader()); for (JarMode candidate : candidates) { @@ -44,10 +64,17 @@ public final class JarModeLauncher { return; } } - System.err.println("Unsupported jarmode '" + mode + "'"); - if (!Boolean.getBoolean(DISABLE_SYSTEM_EXIT)) { - System.exit(1); + throw new JarModeErrorException("Unsupported jarmode '" + mode + "'"); + } + + private static void printError(Throwable ex) { + if (ex instanceof JarModeErrorException) { + String message = ex.getMessage(); + System.err.println("Error: " + message); + System.err.println(); + return; } + ex.printStackTrace(); } } diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader-classic/src/main/java/org/springframework/boot/loader/jarmode/TestJarMode.java b/spring-boot-project/spring-boot-tools/spring-boot-loader-classic/src/main/java/org/springframework/boot/loader/jarmode/TestJarMode.java index 2e17175690a..87ec48ed8c6 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-loader-classic/src/main/java/org/springframework/boot/loader/jarmode/TestJarMode.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader-classic/src/main/java/org/springframework/boot/loader/jarmode/TestJarMode.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2023 the original author or authors. + * Copyright 2012-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -33,6 +33,12 @@ class TestJarMode implements JarMode { @Override public void run(String mode, String[] args) { System.out.println("running in " + mode + " jar mode " + Arrays.asList(args)); + if (args.length > 0 && "error".equals(args[0])) { + throw new JarModeErrorException("error message"); + } + if (args.length > 0 && "fail".equals(args[0])) { + throw new IllegalStateException("bad"); + } } } diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader-classic/src/test/java/org/springframework/boot/loader/jarmode/LauncherJarModeTests.java b/spring-boot-project/spring-boot-tools/spring-boot-loader-classic/src/test/java/org/springframework/boot/loader/jarmode/LauncherJarModeTests.java index dfd4d671e01..71f4f3c2000 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-loader-classic/src/test/java/org/springframework/boot/loader/jarmode/LauncherJarModeTests.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader-classic/src/test/java/org/springframework/boot/loader/jarmode/LauncherJarModeTests.java @@ -55,6 +55,7 @@ class LauncherJarModeTests { System.setProperty("jarmode", "test"); new TestLauncher().launch(new String[] { "boot" }); assertThat(out).contains("running in test jar mode [boot]"); + assertThat(System.getProperty(JarModeLauncher.SUPPRESSED_SYSTEM_EXIT_CODE)).isEqualTo("0"); } @Test @@ -62,6 +63,25 @@ class LauncherJarModeTests { System.setProperty("jarmode", "idontexist"); new TestLauncher().launch(new String[] { "boot" }); assertThat(out).contains("Unsupported jarmode 'idontexist'"); + assertThat(System.getProperty(JarModeLauncher.SUPPRESSED_SYSTEM_EXIT_CODE)).isEqualTo("1"); + } + + @Test + void launchWhenJarModeRunFailsWithErrorExceptionPrintsSimpleMessage(CapturedOutput out) throws Exception { + System.setProperty("jarmode", "test"); + new TestLauncher().launch(new String[] { "error" }); + assertThat(out).contains("running in test jar mode [error]"); + assertThat(out).contains("Error: error message"); + assertThat(System.getProperty(JarModeLauncher.SUPPRESSED_SYSTEM_EXIT_CODE)).isEqualTo("1"); + } + + @Test + void launchWhenJarModeRunFailsWithErrorExceptionPrintsStackTrace(CapturedOutput out) throws Exception { + System.setProperty("jarmode", "test"); + new TestLauncher().launch(new String[] { "fail" }); + assertThat(out).contains("running in test jar mode [fail]"); + assertThat(out).contains("java.lang.IllegalStateException: bad"); + assertThat(System.getProperty(JarModeLauncher.SUPPRESSED_SYSTEM_EXIT_CODE)).isEqualTo("1"); } private static final class TestLauncher extends Launcher { diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jarmode/JarMode.java b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jarmode/JarMode.java index 162e4a6a739..b91fdf1f546 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jarmode/JarMode.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jarmode/JarMode.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2023 the original author or authors. + * Copyright 2012-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -36,7 +36,8 @@ public interface JarMode { * Run the jar in the given mode. * @param mode the mode to use * @param args any program arguments + * @throws JarModeErrorException on an error that should print a simple error message */ - void run(String mode, String[] args); + void run(String mode, String[] args) throws JarModeErrorException; } diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jarmode/JarModeErrorException.java b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jarmode/JarModeErrorException.java new file mode 100644 index 00000000000..92f1720bdf2 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jarmode/JarModeErrorException.java @@ -0,0 +1,36 @@ +/* + * Copyright 2012-2024 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.loader.jarmode; + +/** + * Simple {@link RuntimeException} used to fail the jar mode with a simple printed error + * message. + * + * @author Phillip Webb + * @since 3.3.7 + */ +public class JarModeErrorException extends RuntimeException { + + public JarModeErrorException(String message) { + super(message); + } + + public JarModeErrorException(String message, Throwable cause) { + super(message, cause); + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/launch/JarModeRunner.java b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/launch/JarModeRunner.java index 4805a633d48..a4ac27f1314 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/launch/JarModeRunner.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/launch/JarModeRunner.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2023 the original author or authors. + * Copyright 2012-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -19,6 +19,7 @@ package org.springframework.boot.loader.launch; import java.util.List; import org.springframework.boot.loader.jarmode.JarMode; +import org.springframework.boot.loader.jarmode.JarModeErrorException; import org.springframework.core.io.support.SpringFactoriesLoader; import org.springframework.util.ClassUtils; @@ -31,11 +32,31 @@ final class JarModeRunner { static final String DISABLE_SYSTEM_EXIT = JarModeRunner.class.getName() + ".DISABLE_SYSTEM_EXIT"; + static final String SUPPRESSED_SYSTEM_EXIT_CODE = JarModeRunner.class.getName() + ".SUPPRESSED_SYSTEM_EXIT_CODE"; + private JarModeRunner() { } static void main(String[] args) { String mode = System.getProperty("jarmode"); + boolean disableSystemExit = Boolean.getBoolean(DISABLE_SYSTEM_EXIT); + try { + runJarMode(mode, args); + if (disableSystemExit) { + System.setProperty(SUPPRESSED_SYSTEM_EXIT_CODE, "0"); + } + } + catch (Throwable ex) { + printError(ex); + if (disableSystemExit) { + System.setProperty(SUPPRESSED_SYSTEM_EXIT_CODE, "1"); + return; + } + System.exit(1); + } + } + + private static void runJarMode(String mode, String[] args) { List candidates = SpringFactoriesLoader.loadFactories(JarMode.class, ClassUtils.getDefaultClassLoader()); for (JarMode candidate : candidates) { @@ -44,10 +65,17 @@ final class JarModeRunner { return; } } - System.err.println("Unsupported jarmode '" + mode + "'"); - if (!Boolean.getBoolean(DISABLE_SYSTEM_EXIT)) { - System.exit(1); + throw new JarModeErrorException("Unsupported jarmode '" + mode + "'"); + } + + private static void printError(Throwable ex) { + if (ex instanceof JarModeErrorException) { + String message = ex.getMessage(); + System.err.println("Error: " + message); + System.err.println(); + return; } + ex.printStackTrace(); } } diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/jarmode/TestJarMode.java b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/jarmode/TestJarMode.java index 2e17175690a..87ec48ed8c6 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/jarmode/TestJarMode.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/jarmode/TestJarMode.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2023 the original author or authors. + * Copyright 2012-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -33,6 +33,12 @@ class TestJarMode implements JarMode { @Override public void run(String mode, String[] args) { System.out.println("running in " + mode + " jar mode " + Arrays.asList(args)); + if (args.length > 0 && "error".equals(args[0])) { + throw new JarModeErrorException("error message"); + } + if (args.length > 0 && "fail".equals(args[0])) { + throw new IllegalStateException("bad"); + } } } diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/launch/LauncherTests.java b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/launch/LauncherTests.java index 2937d9e6752..5c73dc68b8a 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/launch/LauncherTests.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/launch/LauncherTests.java @@ -63,6 +63,7 @@ class LauncherTests { System.setProperty("jarmode", "test"); new TestLauncher().launch(new String[] { "boot" }); assertThat(out).contains("running in test jar mode [boot]"); + assertThat(System.getProperty(JarModeRunner.SUPPRESSED_SYSTEM_EXIT_CODE)).isEqualTo("0"); } @Test @@ -70,6 +71,25 @@ class LauncherTests { System.setProperty("jarmode", "idontexist"); new TestLauncher().launch(new String[] { "boot" }); assertThat(out).contains("Unsupported jarmode 'idontexist'"); + assertThat(System.getProperty(JarModeRunner.SUPPRESSED_SYSTEM_EXIT_CODE)).isEqualTo("1"); + } + + @Test + void launchWhenJarModeRunFailsWithErrorExceptionPrintsSimpleMessage(CapturedOutput out) throws Exception { + System.setProperty("jarmode", "test"); + new TestLauncher().launch(new String[] { "error" }); + assertThat(out).contains("running in test jar mode [error]"); + assertThat(out).contains("Error: error message"); + assertThat(System.getProperty(JarModeRunner.SUPPRESSED_SYSTEM_EXIT_CODE)).isEqualTo("1"); + } + + @Test + void launchWhenJarModeRunFailsWithErrorExceptionPrintsStackTrace(CapturedOutput out) throws Exception { + System.setProperty("jarmode", "test"); + new TestLauncher().launch(new String[] { "fail" }); + assertThat(out).contains("running in test jar mode [fail]"); + assertThat(out).contains("java.lang.IllegalStateException: bad"); + assertThat(System.getProperty(JarModeRunner.SUPPRESSED_SYSTEM_EXIT_CODE)).isEqualTo("1"); } }