diff --git a/core/deployment/src/main/java/io/quarkus/runner/bootstrap/StartupActionImpl.java b/core/deployment/src/main/java/io/quarkus/runner/bootstrap/StartupActionImpl.java index 4589a5d5407c7..e03976589005e 100644 --- a/core/deployment/src/main/java/io/quarkus/runner/bootstrap/StartupActionImpl.java +++ b/core/deployment/src/main/java/io/quarkus/runner/bootstrap/StartupActionImpl.java @@ -17,6 +17,7 @@ import java.util.Map; import java.util.Set; import java.util.concurrent.CountDownLatch; +import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicInteger; import java.util.function.Consumer; @@ -210,24 +211,20 @@ public int runMainClassBlocking(String... args) throws Exception { try { AtomicInteger result = new AtomicInteger(); Class lifecycleManager = Class.forName(ApplicationLifecycleManager.class.getName(), true, runtimeClassLoader); - Method getCurrentApplication = lifecycleManager.getDeclaredMethod("getCurrentApplication"); - Object oldApplication = getCurrentApplication.invoke(null); - lifecycleManager.getDeclaredMethod("setDefaultExitCodeHandler", Consumer.class).invoke(null, - new Consumer() { - @Override - public void accept(Integer integer) { - result.set(integer); - } - }); - // force init here - Class appClass = Class.forName(className, true, runtimeClassLoader); - Method start = appClass.getMethod("main", String[].class); - start.invoke(null, (Object) (args == null ? new String[0] : args)); + AtomicBoolean alreadyStarted = new AtomicBoolean(); + Method setDefaultExitCodeHandler = lifecycleManager.getDeclaredMethod("setDefaultExitCodeHandler", Consumer.class); + Method setAlreadyStartedCallback = lifecycleManager.getDeclaredMethod("setAlreadyStartedCallback", Consumer.class); - CountDownLatch latch = new CountDownLatch(1); - new Thread(new Runnable() { - @Override - public void run() { + try { + setDefaultExitCodeHandler.invoke(null, (Consumer) result::set); + setAlreadyStartedCallback.invoke(null, (Consumer) alreadyStarted::set); + // force init here + Class appClass = Class.forName(className, true, runtimeClassLoader); + Method start = appClass.getMethod("main", String[].class); + start.invoke(null, (Object) (args == null ? new String[0] : args)); + + CountDownLatch latch = new CountDownLatch(1); + new Thread(() -> { try { Class q = Class.forName(Quarkus.class.getName(), true, runtimeClassLoader); q.getMethod("blockingExit").invoke(null); @@ -236,17 +233,19 @@ public void run() { } finally { latch.countDown(); } + }).start(); + latch.await(); + + if (alreadyStarted.get()) { + //quarkus was not actually started by the main method + //just return + return 0; } - }).start(); - latch.await(); - - Object newApplication = getCurrentApplication.invoke(null); - if (oldApplication == newApplication) { - //quarkus was not actually started by the main method - //just return - return 0; + return result.get(); + } finally { + setDefaultExitCodeHandler.invoke(null, (Consumer) null); + setAlreadyStartedCallback.invoke(null, (Consumer) null); } - return result.get(); } finally { for (var closeTask : runtimeCloseTasks) { try { diff --git a/core/runtime/src/main/java/io/quarkus/runtime/ApplicationLifecycleManager.java b/core/runtime/src/main/java/io/quarkus/runtime/ApplicationLifecycleManager.java index d16e5ff7e94af..bc57782191e2e 100644 --- a/core/runtime/src/main/java/io/quarkus/runtime/ApplicationLifecycleManager.java +++ b/core/runtime/src/main/java/io/quarkus/runtime/ApplicationLifecycleManager.java @@ -2,7 +2,6 @@ import java.util.List; import java.util.Locale; -import java.util.Objects; import java.util.Set; import java.util.concurrent.locks.Condition; import java.util.concurrent.locks.Lock; @@ -51,7 +50,7 @@ public class ApplicationLifecycleManager { // used by ShutdownEvent to propagate the information about shutdown reason public static volatile ShutdownEvent.ShutdownReason shutdownReason = ShutdownEvent.ShutdownReason.STANDARD; - private static volatile BiConsumer defaultExitCodeHandler = new BiConsumer() { + private static final BiConsumer MAIN_EXIT_CODE_HANDLER = new BiConsumer<>() { @Override public void accept(Integer integer, Throwable cause) { Logger logger = Logger.getLogger(Application.class); @@ -62,6 +61,12 @@ public void accept(Integer integer, Throwable cause) { System.exit(integer); } }; + private static final Consumer NOOP_ALREADY_STARTED_CALLBACK = new Consumer<>() { + @Override + public void accept(Boolean t) { + } + }; + private static volatile BiConsumer defaultExitCodeHandler = MAIN_EXIT_CODE_HANDLER; private ApplicationLifecycleManager() { @@ -77,8 +82,9 @@ private ApplicationLifecycleManager() { private static int exitCode = -1; private static volatile boolean shutdownRequested; - private static Application currentApplication; + private static volatile Application currentApplication; private static boolean vmShuttingDown; + private static Consumer alreadyStartedCallback = NOOP_ALREADY_STARTED_CALLBACK; private static final boolean IS_WINDOWS = System.getProperty("os.name").toLowerCase(Locale.ENGLISH).contains("windows"); private static final boolean IS_MAC = System.getProperty("os.name").toLowerCase(Locale.ENGLISH).contains("mac"); @@ -89,17 +95,19 @@ public static void run(Application application, String... args) { public static void run(Application application, Class quarkusApplication, BiConsumer exitCodeHandler, String... args) { + boolean alreadyStarted; stateLock.lock(); - //in tests, we might pass this method an already started application - //in this case we don't shut it down at the end - boolean alreadyStarted = application.isStarted(); - if (shutdownHookThread == null) { - registerHooks(exitCodeHandler == null ? defaultExitCodeHandler : exitCodeHandler); - } - if (currentApplication != null && !shutdownRequested) { - throw new IllegalStateException("Quarkus already running"); - } try { + //in tests, we might pass this method an already started application + //in this case we don't shut it down at the end + alreadyStarted = application.isStarted(); + alreadyStartedCallback.accept(alreadyStarted); + if (shutdownHookThread == null) { + registerHooks(exitCodeHandler == null ? defaultExitCodeHandler : exitCodeHandler); + } + if (currentApplication != null && !shutdownRequested) { + throw new IllegalStateException("Quarkus already running"); + } exitCode = -1; shutdownRequested = false; currentApplication = application; @@ -209,6 +217,7 @@ public static void run(Application application, Class defaultExitCodeHandler) { - Objects.requireNonNull(defaultExitCodeHandler); + if (defaultExitCodeHandler == null) { + defaultExitCodeHandler = MAIN_EXIT_CODE_HANDLER; + } ApplicationLifecycleManager.defaultExitCodeHandler = defaultExitCodeHandler; } @@ -365,8 +376,18 @@ public static void setDefaultExitCodeHandler(BiConsumer defa * * @param defaultExitCodeHandler the new default exit handler */ + // Used by StartupActionImpl via reflection public static void setDefaultExitCodeHandler(Consumer defaultExitCodeHandler) { - setDefaultExitCodeHandler((exitCode, cause) -> defaultExitCodeHandler.accept(exitCode)); + BiConsumer biConsumer = defaultExitCodeHandler == null ? null + : (exitCode, cause) -> defaultExitCodeHandler.accept(exitCode); + setDefaultExitCodeHandler(biConsumer); + } + + @SuppressWarnings("unused") + // Used by StartupActionImpl via reflection + public static void setAlreadyStartedCallback(Consumer alreadyStartedCallback) { + ApplicationLifecycleManager.alreadyStartedCallback = alreadyStartedCallback != null ? alreadyStartedCallback + : NOOP_ALREADY_STARTED_CALLBACK; } /** diff --git a/integration-tests/picocli-native/src/main/java/io/quarkus/it/picocli/ExitCodeCommand.java b/integration-tests/picocli-native/src/main/java/io/quarkus/it/picocli/ExitCodeCommand.java new file mode 100644 index 0000000000000..3601951ed6ee7 --- /dev/null +++ b/integration-tests/picocli-native/src/main/java/io/quarkus/it/picocli/ExitCodeCommand.java @@ -0,0 +1,17 @@ +package io.quarkus.it.picocli; + +import java.util.concurrent.Callable; + +import picocli.CommandLine; + +@CommandLine.Command(name = "exitcode", versionProvider = DynamicVersionProvider.class) +public class ExitCodeCommand implements Callable { + + @CommandLine.Option(names = "--code") + int exitCode; + + @Override + public Integer call() { + return exitCode; + } +} diff --git a/integration-tests/picocli-native/src/main/java/io/quarkus/it/picocli/TopTestCommand.java b/integration-tests/picocli-native/src/main/java/io/quarkus/it/picocli/TopTestCommand.java index 66e98420e31bb..d51e5c4069841 100644 --- a/integration-tests/picocli-native/src/main/java/io/quarkus/it/picocli/TopTestCommand.java +++ b/integration-tests/picocli-native/src/main/java/io/quarkus/it/picocli/TopTestCommand.java @@ -5,6 +5,7 @@ @TopCommand @CommandLine.Command(name = "test", mixinStandardHelpOptions = true, commandListHeading = "%nCommands:%n", synopsisHeading = "%nUsage: ", optionListHeading = "%nOptions:%n", subcommands = { + ExitCodeCommand.class, CommandUsedAsParent.class, CompletionReflectionCommand.class, DefaultValueProviderCommand.class, diff --git a/integration-tests/picocli-native/src/test/java/io/quarkus/it/picocli/PicocliTest.java b/integration-tests/picocli-native/src/test/java/io/quarkus/it/picocli/PicocliTest.java index 2022710db4fa2..75542f961f53d 100644 --- a/integration-tests/picocli-native/src/test/java/io/quarkus/it/picocli/PicocliTest.java +++ b/integration-tests/picocli-native/src/test/java/io/quarkus/it/picocli/PicocliTest.java @@ -21,6 +21,16 @@ public class PicocliTest { private String value; + @Test + public void testExitCode(QuarkusMainLauncher launcher) { + LaunchResult result = launcher.launch("exitcode", "--code", Integer.toString(42)); + assertThat(result.exitCode()).isEqualTo(42); + result = launcher.launch("exitcode", "--code", Integer.toString(0)); + assertThat(result.exitCode()).isEqualTo(0); + result = launcher.launch("exitcode", "--code", Integer.toString(2)); + assertThat(result.exitCode()).isEqualTo(2); + } + @Test @Launch({ "test-command", "-f", "test.txt", "-f", "test2.txt", "-f", "test3.txt", "-s", "ERROR", "-h", "SOCKS=5.5.5.5", "-p", "privateValue", "pos1", "pos2" }) diff --git a/integration-tests/picocli/src/main/java/io/quarkus/it/picocli/ExitCodeCommand.java b/integration-tests/picocli/src/main/java/io/quarkus/it/picocli/ExitCodeCommand.java new file mode 100644 index 0000000000000..4678f030a937e --- /dev/null +++ b/integration-tests/picocli/src/main/java/io/quarkus/it/picocli/ExitCodeCommand.java @@ -0,0 +1,17 @@ +package io.quarkus.it.picocli; + +import java.util.concurrent.Callable; + +import picocli.CommandLine; + +@CommandLine.Command(name = "exitcode") +public class ExitCodeCommand implements Callable { + + @CommandLine.Option(names = "--code") + int exitCode; + + @Override + public Integer call() { + return exitCode; + } +} diff --git a/integration-tests/picocli/src/test/java/io/quarkus/it/picocli/TestExitCode.java b/integration-tests/picocli/src/test/java/io/quarkus/it/picocli/TestExitCode.java new file mode 100644 index 0000000000000..7e5b98e7de182 --- /dev/null +++ b/integration-tests/picocli/src/test/java/io/quarkus/it/picocli/TestExitCode.java @@ -0,0 +1,21 @@ +package io.quarkus.it.picocli; + +import static io.quarkus.it.picocli.TestUtils.createConfig; +import static org.assertj.core.api.Assertions.assertThat; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.test.QuarkusProdModeTest; + +public class TestExitCode { + + @RegisterExtension + static final QuarkusProdModeTest config = createConfig("hello-app", ExitCodeCommand.class) + .setCommandLineParameters("--code", "42"); + + @Test + public void simpleTest() { + assertThat(config.getExitCode()).isEqualTo(42); + } +}